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 six
15import math
16
17from PyQt5 import QtGui, QtWidgets, QtCore
18
19import Connection_Widget
20from Model import Common
21from Interface.Property import PropertyInterface
22from Instance_Property_Widget import InstancePropertyWidget
23
24# TODO: Delete itself from all connections when __del__ ed
25
26class InstanceWidget(QtWidgets.QGraphicsWidget, PropertyInterface):
27    """
28    InstanceWidget - a View representation of camkes.ast.Instance.
29    If model changes, update the fields in InstanceWidget.
30    """
31
32    # Constants and private class variables
33    _bounding_rect = None
34    _border_thickness = 7
35
36    @property
37    def velocity(self):
38        if self._velocity is None:
39            self._velocity = QtCore.QPointF(0, 0)
40        return self._velocity
41
42    @velocity.setter
43    def velocity(self, value):
44        assert isinstance(value, QtCore.QPointF)
45        self._velocity = value
46
47    # --- Information about Instance ---
48
49    @property
50    def name(self):
51        if self._name is None:
52            self._name = "Uninitialised widget"
53        return self._name
54
55    @name.setter
56    def name(self, value):
57        assert isinstance(value, six.string_types)
58        self._name = value
59        self.update_ui()
60
61    @property
62    def component_type(self):
63        if self._component_type is None:
64            self._component_type = "Uninitialised widget"
65        return self._component_type
66
67    @component_type.setter
68    def component_type(self, value):
69        assert isinstance(value, six.string_types)
70        self._component_type = value
71        self.update_ui()
72
73    @property
74    def control(self):
75        return self._control
76
77    @control.setter
78    def control(self, value):
79        assert isinstance(value, bool)
80        self._control = value
81        self.update_ui()
82
83    @property
84    def hardware(self):
85        return self._hardware
86
87    @hardware.setter
88    def hardware(self, value):
89        assert isinstance(value, bool)
90        self._hardware = value
91        self.update_ui()
92
93    @property
94    def hidden(self):
95        return self._hidden
96
97    @hidden.setter
98    def hidden(self, value):
99        assert isinstance(value, bool)
100        self._hidden = value
101
102        if value:
103            self.setZValue(3)
104        else:
105            self.setZValue(5)
106
107        for connection in self.connection_list:
108            # This will only set if both source and destination is not hidden
109            connection.hidden = value
110            connection.update()
111
112        self.update()
113
114    # Provides
115    @property
116    def provides(self):
117        if self._provides is None:
118            self._provides = []
119        return self._provides
120
121    # TODO: Handle multiple connections
122    def add_provide(self, name, interface_type, connection=None):
123        assert isinstance(name, six.string_types)
124        assert isinstance(interface_type, six.string_types)
125
126        self.provides.append({'Name': name,
127                              'Interface_type': interface_type,
128                              'Connection_Widget': connection})
129
130        self.update_ui()
131
132    def add_provide_connection(self, interface_name, connection):
133        assert self._provides is not None
134        for dictionary in self.provides:
135            if dictionary['Name'] == interface_name:
136                dictionary['Connection_Widget'] = connection
137                break
138
139    def remove_provide_connection(self, interface_name, connection):
140        assert self._provides is not None
141        for dictionary in self.provides:
142            if dictionary['Name'] == interface_name and dictionary['Connection_Widget'] is connection:
143                dictionary['Connection_Widget'] = None
144                break
145
146    def delete_provide(self, name):
147        raise NotImplementedError
148
149    # Uses
150    @property
151    def uses(self):
152        if self._uses is None:
153            self._uses = []
154        return self._uses
155
156    def add_use(self, name, interface_type, connection=None):
157        assert isinstance(name, six.string_types)
158        assert isinstance(interface_type, six.string_types)
159
160        self.uses.append({'Name': name, 'Interface_type': interface_type, 'Connection_Widget': connection})
161
162        self.update_ui()
163        # TODO NotImplementedError
164
165    def add_use_connection(self, interface_name, connection):
166        assert self._uses is not None
167        for dictionary in self.uses:
168            if dictionary['Name'] == interface_name:
169                dictionary['Connection_Widget'] = connection
170                break
171
172    def remove_use_connection(self, interface_name, connection):
173        assert self._uses is not None
174        for dictionary in self.uses:
175            if dictionary['Name'] == interface_name and dictionary['Connection_Widget'] is connection:
176                dictionary['Connection_Widget'] = None
177                break
178
179    def delete_use(self, name):
180        raise NotImplementedError
181
182    # Emits
183    @property
184    def emits(self):
185        if self._emits is None:
186            self._emits = []
187        return self._emits
188
189    def add_emit(self, name, interface_type, connection=None):
190        assert isinstance(name, six.string_types)
191        assert isinstance(interface_type, six.string_types)
192
193        self.emits.append({'Name': name, 'Interface_type': interface_type, 'Connection_Widget': connection})
194
195        self.update_ui()
196        # TODO NotImplementedError
197
198    def add_emit_connection(self, interface_name, connection):
199        assert self._emits is not None
200        for dictionary in self.emits:
201            if dictionary['Name'] == interface_name:
202                dictionary['Connection_Widget'] = connection
203                break
204
205    def remove_emit_connection(self, interface_name, connection):
206        assert self._emits is not None
207        for dictionary in self.emits:
208            if dictionary['Name'] == interface_name and dictionary['Connection_Widget'] is connection:
209                dictionary['Connection_Widget'] = None
210                break
211
212    def delete_emit(self, name):
213        raise NotImplementedError
214
215    # Consumes
216    @property
217    def consumes(self):
218        if self._consumes is None:
219            self._consumes = []
220        return self._consumes
221
222    def add_consume(self, name, interface_type, optional, connection=None):
223        assert isinstance(name, six.string_types)
224        assert isinstance(interface_type, six.string_types)
225        assert isinstance(optional, bool)
226
227        self.consumes.append({'Name': name, 'Interface_type': interface_type, 'Optional': optional,
228                              'Connection_Widget': connection})
229
230        self.update_ui()
231        # TODO NotImplementedError
232
233    def add_consume_connection(self, interface_name, connection):
234        assert self._consumes is not None
235        for dictionary in self.consumes:
236            if dictionary['Name'] == interface_name:
237                dictionary['Connection_Widget'] = connection
238                break
239
240    def remove_consume_connection(self, interface_name, connection):
241        assert self._consumes is not None
242        for dictionary in self.consumes:
243            if dictionary['Name'] == interface_name and \
244                    dictionary['Connection_Widget'] is connection:
245                dictionary['Connection_Widget'] = None
246                break
247
248    def delete_consume(self, name):
249        raise NotImplementedError
250
251    # Dataport
252    @property
253    def dataport(self):
254        if self._dataport is None:
255            self._dataport = []
256        return self._dataport
257
258    def add_dataport(self, name, interface_type, optional, connection=None):
259        assert isinstance(name, six.string_types)
260        assert isinstance(interface_type, six.string_types)
261        assert isinstance(optional, bool)
262
263        if self._dataport is None:
264            self._dataport = []
265
266        self._dataport.append({'Name': name, 'Interface_type': interface_type, 'Optional': optional,
267                               'Connection_Widget': connection})
268
269        self.update_ui()
270        # TODO NotImplementedError
271
272    def add_dataport_connection(self, interface_name, connection):
273        assert self._dataport is not None
274        for dictionary in self.dataport:
275            if dictionary['Name'] == interface_name:
276                dictionary['Connection_Widget'] = connection
277                break
278
279    def remove_dataport_connection(self, interface_name, connection):
280        assert self._dataport is not None
281        for dictionary in self.dataport:
282            if dictionary['Name'] == interface_name and \
283                            dictionary['Connection_Widget'] is connection:
284                dictionary['Connection_Widget'] = None
285                break
286
287    def delete_dataport(self, name):
288        raise NotImplementedError
289
290    @property
291    def connection_list(self):
292        return self._connections_list
293
294    # TODO: connection overrides, for multiway connection. Eg. eigenConnection
295    def add_connection(self, connection):
296
297        assert isinstance(connection, Connection_Widget.ConnectionWidget)
298
299        if connection.source_instance_widget is self:
300            if connection.source_connection_type == Common.Event:
301                self.add_emit_connection(connection.source_interface_name, connection)
302            elif connection.source_connection_type == Common.Procedure:
303                self.add_use_connection(connection.source_interface_name, connection)
304            elif connection.source_connection_type == Common.Dataport:
305                self.add_dataport_connection(connection.source_interface_name, connection)
306
307        elif connection.dest_instance_widget is self:
308            if connection.dest_connection_type == Common.Event:
309                self.add_consume_connection(connection.dest_interface_name, connection)
310            elif connection.dest_connection_type == Common.Procedure:
311                self.add_provide_connection(connection.dest_interface_name, connection)
312            elif connection.dest_connection_type == Common.Dataport:
313                self.add_dataport_connection(connection.dest_interface_name, connection)
314
315        else:
316            raise NotImplementedError  # Something is wrong
317
318        self._connections_list.append(connection)
319        self.update_connection_position(connection)
320
321    def remove_connection(self, connection):
322        assert isinstance(connection, Connection_Widget.ConnectionWidget)
323
324        if connection.source_instance_widget is self:
325            if connection.source_connection_type == Common.Event:
326                self.remove_emit_connection(connection.source_interface_name, connection)
327            elif connection.source_connection_type == Common.Procedure:
328                self.remove_use_connection(connection.source_interface_name, connection)
329            elif connection.source_connection_type == Common.Dataport:
330                self.remove_dataport_connection(connection.source_interface_name, connection)
331        elif connection.dest_instance_widget is self:
332            if connection.dest_connection_type == Common.Event:
333                self.remove_consume_connection(connection.dest_interface_name, connection)
334            elif connection.dest_connection_type == Common.Procedure:
335                self.remove_provide_connection(connection.dest_interface_name, connection)
336            elif connection.dest_connection_type == Common.Dataport:
337                self.remove_dataport_connection(connection.dest_interface_name, connection)
338        else:
339            raise NotImplementedError  # Something is wrong
340
341        self._connections_list.remove(connection)
342
343    @property
344    def context_menu(self):
345        return self._context_menu
346
347    @context_menu.setter
348    def context_menu(self, value):
349        assert isinstance(value, QtWidgets.QGraphicsProxyWidget)
350        assert isinstance(value.widget(), QtWidgets.QMenu)
351        self._context_menu = value
352
353    @property
354    def property_widget(self):
355        self._property_widget = InstancePropertyWidget(self)
356        return self._property_widget
357    # -------
358
359    # Signals & Slots
360    widget_moved = QtCore.pyqtSignal()
361
362    # --- INITIALISATION
363    def __init__(self, context_menu, preferred_point=None):
364        super(InstanceWidget, self).__init__()
365        # Model
366
367        self._preferred_point = preferred_point
368        self._pinned = False
369        self._velocity = None
370
371        self._name = None
372        self._component_type = None
373        self._control = False
374        self._hardware = False
375        self._provides = None
376        self._uses = None
377        self._emits = None
378        self._consumes = None
379        self._dataport = None
380
381        self._context_menu = None
382        self.context_menu = context_menu
383        self._hidden = False
384        self._property_widget = None
385
386        self._connections_list = []
387
388        # GUI
389        self.color = QtGui.QColor(245,245,245)
390
391        self.setFlag(QtWidgets.QGraphicsWidget.ItemIsMovable)
392
393        self.update_ui()
394
395    # --- UI FUNCTIONS ---
396    def paint(self, painter, style_options, widget=None):
397        """
398        Overridden function, paints the box with name, type and the C H symbols.
399        :param painter:
400        :param style_options:
401        :param widget:
402        :return:
403        """
404
405        assert isinstance(painter, QtGui.QPainter)
406        assert isinstance(style_options, QtWidgets.QStyleOptionGraphicsItem)
407        # assert isinstance(widget, QtWidgets.QWidget)
408
409        super(InstanceWidget, self).paint(painter, style_options, widget)
410
411        painter.setRenderHint(QtGui.QPainter.Antialiasing)
412
413        # -- If hidden, changing alpha values to transparent --
414        color = self.color
415
416        if self.hidden:
417            color.setAlphaF(0.2)
418        else:
419            color.setAlphaF(1)
420
421        # Setting brush color
422        brush = painter.brush()
423        brush.setColor(color)
424        painter.setBrush(brush)
425
426        pen = painter.pen()
427        pen_color = pen.color()
428        if self.hidden:
429            pen_color.setAlphaF(0.2)
430        else:
431            pen_color.setAlphaF(1)
432        pen.setColor(pen_color)
433        painter.setPen(pen)
434
435        rounded_rect = QtGui.QPainterPath()
436        assert isinstance(rounded_rect, QtGui.QPainterPath)
437        # If instance is control or hardware, boundedRect will compensate for that.
438        inner_rect = self.boundingRect().adjusted(0,0,0,0)  # Hacking way to get a copy of rect
439        if self.hardware or self.control:
440            inner_rect.adjust(2,2,-2,-2)
441        rounded_rect.addRoundedRect(inner_rect,5,5)
442
443        painter.fillPath(rounded_rect, color)
444        painter.drawPath(rounded_rect)
445
446        # Draw an outline if the instance is control or hardware
447        # Assumption is, an instance cannot be both control and hardware
448        outline_rect = inner_rect.adjusted(-1,-1,1,1)
449        outline_rect_path = QtGui.QPainterPath()
450        outline_rect_path.addRoundedRect(outline_rect, 5, 5)
451        stroker = QtGui.QPainterPathStroker()
452        stroker.setWidth(5)
453        outline_rounded_rect = stroker.createStroke(outline_rect_path)
454
455        # Draw outline to highlight control components
456        if self.control:
457            # Make a BLUE color pen
458            pen_color.setRed(30)
459            pen_color.setGreen(136)
460            pen_color.setBlue(229)
461            painter.fillPath(outline_rounded_rect, pen_color)
462
463        # Draw outline to highlight hardware components
464        if self.hardware:
465            pen_color.setRed(67)
466            pen_color.setGreen(160)
467            pen_color.setBlue(71)
468            painter.fillPath(outline_rounded_rect, pen_color)
469
470        # TODO IDEA: Update rect with new size
471
472        # Printing instance name
473        font = QtGui.QFont("Helvetica", 15, QtGui.QFont.Normal)
474        painter.setFont(font)
475        font_metrics = painter.fontMetrics()
476        assert isinstance(font_metrics, QtGui.QFontMetrics)
477        bounding_rect_font = painter.boundingRect(QtCore.QRectF(1, 1, 1, 1), QtCore.Qt.AlignCenter, self.name)
478
479        bounding_rect_font.moveTo(inner_rect.center().x() - bounding_rect_font.width() / 2,
480                                  inner_rect.center().y() - font_metrics.ascent())
481
482        painter.drawText(bounding_rect_font, QtCore.Qt.AlignCenter, self.name)
483
484        control_hardware_x_pos = bounding_rect_font.x()
485
486        # Printing component name
487        font.setPointSize(11)
488        painter.setFont(font)
489        bounding_rect_font = painter.boundingRect(QtCore.QRectF(1, 1, 1, 1), QtCore.Qt.AlignCenter, self.component_type)
490
491        bounding_rect_font.moveTo(inner_rect.center().x() - bounding_rect_font.width() / 2,
492                                  inner_rect.center().y() + font_metrics.descent())
493
494        painter.drawText(bounding_rect_font, QtCore.Qt.AlignCenter, self.component_type)
495
496        if bounding_rect_font.x() < control_hardware_x_pos:
497            control_hardware_x_pos = bounding_rect_font.x()
498
499        control_hardware_x_pos -= 5
500
501        # The C
502
503        font.setPointSize(12)
504        painter.setFont(font)
505        font_metrics = painter.fontMetrics()
506        bounding_rect_font = painter.boundingRect(QtCore.QRectF(1, 1, 1, 1), QtCore.Qt.AlignCenter, "C")
507
508        bounding_rect_font.moveTo(control_hardware_x_pos - bounding_rect_font.width(),
509                                  inner_rect.center().y() - font_metrics.ascent())
510        if self.control:
511            painter.drawText(bounding_rect_font, QtCore.Qt.AlignCenter, "C")
512
513        # The H
514        bounding_rect_font = painter.boundingRect(QtCore.QRectF(1, 1, 1, 1), QtCore.Qt.AlignCenter, "H")
515        bounding_rect_font.moveTo(control_hardware_x_pos - bounding_rect_font.width(),
516                                  inner_rect.center().y() + font_metrics.descent())
517        if self.hardware:
518            painter.drawText(bounding_rect_font, QtCore.Qt.AlignCenter, "H")
519
520    def update_ui(self):
521        """
522        Recalculates the expected size of the view, and calls a repaint.
523        :return:
524        """
525
526        # Calculate rect for instance name
527        practise_font = QtGui.QFont("Helvetica", 15, QtGui.QFont.Normal)
528        practise_font_metrics = QtGui.QFontMetrics(practise_font)
529        instance_name_rect = practise_font_metrics.boundingRect(self.name)
530
531        # Calculate rect for component type
532        practise_font.setPointSize(11)
533        practise_font_metrics = QtGui.QFontMetrics(practise_font)
534        component_name_rect = practise_font_metrics.boundingRect(self.component_type)
535
536        # Calculate rects for control and hardware symbols
537        practise_font.setPointSize(12)
538        practise_font_metrics = QtGui.QFontMetrics(practise_font)
539        control_rect = practise_font_metrics.boundingRect("C")
540        hardware_rect = practise_font_metrics.boundingRect("H")
541
542        # Find the max height
543        max_height = 2 * self._border_thickness + instance_name_rect.height() + hardware_rect.height() + 7
544
545        # Find the max width
546        max_width = 2 * self._border_thickness + 2 * control_rect.width() + 10
547        if instance_name_rect.width() > component_name_rect.width():
548            max_width = max_width + instance_name_rect.width()
549        else:
550            max_width = max_width + component_name_rect.width()
551
552        # Set bounding rect to new max width and height
553        self._bounding_rect = QtCore.QRectF(self.scenePos().x(), self.scenePos().y(), max_width, max_height)
554
555        # Adjust for new hardware or control border
556        if self.hardware or self.control:
557            self._bounding_rect.adjust(-2,-2,2,2)
558
559        self.setPreferredSize(self._bounding_rect.width(), self._bounding_rect.height())
560
561        self.update()  # Call a repaint
562
563    def boundingRect(self):
564        """
565        :return: QRect - bounding rectangle of this widget
566        """
567
568        return self._bounding_rect
569
570    def update_connections(self):
571        """
572        Forces all connections and connecting instances to update.
573        :return:
574        """
575
576        for connection in self.connection_list:
577            self.update_connection_position(connection)
578            if connection.source_instance_widget is self:
579                connection.dest_instance_widget.update_connection_position(connection)
580            else:
581                connection.source_instance_widget.update_connection_position(connection)
582
583    def update_connection_position(self, connection):
584        """
585        Updates the touching point between the connection and this widget.
586        :param connection: The connection to be updated
587        :return:
588        """
589
590        assert isinstance(connection, Connection_Widget.ConnectionWidget)
591
592        decrease_angle = None
593
594        # Find the direction of the angle on the other end - if it is set.
595        if connection.source_instance_widget is self:
596            other_widget = connection.dest_instance_widget
597            if connection.dest_angle:
598                decrease_angle = connection.dest_angle >= 0
599        else:
600            other_widget = connection.source_instance_widget
601            if connection.source_angle:
602                decrease_angle = connection.source_angle >= 0
603
604        # --- Find position based on straight line distance between this and other widget ---
605
606        # -- Vector between other and this --
607        assert isinstance(other_widget, InstanceWidget)
608
609        our_pos = self.scenePos()
610        # Get middle of widget
611        our_pos.setX(our_pos.x() + self.boundingRect().width() / 2)
612        our_pos.setY(our_pos.y() + self.boundingRect().height() / 2)
613
614        other_widget_pos = other_widget.scenePos()
615        # Get middle of widget
616        other_widget_pos.setX(other_widget_pos.x() + other_widget.boundingRect().width() / 2)
617        other_widget_pos.setY(other_widget_pos.y() + other_widget.boundingRect().height() / 2)
618
619        vector = other_widget_pos - our_pos
620
621        # -- Finding intersection between vector and edge of this widget --
622        final_pos = self.edge_intersection(our_pos, vector)
623
624        # Check if final_pos is inside other_widget
625        # If inside, use the centre of this widget instead.
626        other_widget_top_left = other_widget.scenePos()
627        other_widget_bottom_right = QtCore.QPointF(other_widget.scenePos().x() + \
628                                                           other_widget.boundingRect().width(),
629                                                   other_widget.scenePos().y() + \
630                                                           other_widget.boundingRect().height())
631        if final_pos.x() >= other_widget_top_left.x() and \
632            final_pos.x() <= other_widget_bottom_right.x() and \
633            final_pos.y() >= other_widget_top_left.y() and \
634            final_pos.y() <= other_widget_bottom_right.y():
635            final_pos = our_pos
636
637        # Find unclashing angle
638        angle = self.find_free_angle(final_pos, connection, decrease_angle)
639
640        # Set our newly found position and angle (at the appropriate side of the connection)
641        if connection.source_instance_widget is self:
642            connection.set_source_pos_angle(final_pos, angle)
643        else:
644            connection.set_dest_pos_angle(final_pos, angle)
645
646    # TODO: Potentially inefficient algorithm
647    def edge_intersection(self, our_pos, vector):
648        """
649        Finding the intersection between the vector + pos , to the edge of the widget
650        :param our_pos: The starting position of the vector (usually centre of widget)
651        :param vector: The vector from the starting position
652        :return: PyQt5.QPointF - The position of the intersection with the edge.
653        """
654
655        # Consider the case where x is bigger than y
656        #            .
657        #         .  .
658        #     .      .
659        # ............
660        # We reduce y, proportional to x, such that x is equal to width of widget.
661
662        # If the x is 0, then it is a horizontal
663        if vector.x() == 0:
664            y_pos = self.boundingRect().height()
665            # If original y is negative, new y must also be negative
666            if vector.y() < 0:
667                y_pos = -y_pos
668        else:
669            # Using ratios to get y value
670            y_pos = vector.y() * math.fabs((self.boundingRect().width() / 2) / vector.x())
671
672        half_height = self.boundingRect().height() / 2 + 1  # Bit of room for rounding
673
674        # If y is within the box then above assumption is correct
675        if -half_height <= y_pos <= half_height:
676            vector.setY(y_pos)
677            if vector.x() < 0:
678                vector.setX(-self.boundingRect().width() / 2)
679            else:
680                vector.setX(self.boundingRect().width() / 2)
681        else:
682            # If y wasn't within the box, then we assumption is wrong, y is bigger than x
683            #      .
684            #      .
685            #    . .
686            #      .
687            #   .  .
688            #      .
689            # ......
690            # We reduce x, proportional to y, such that y is equal to height.
691
692            # If y is 0, then it is vertical
693            if vector.y() == 0:
694                x_pos = self.boundingRect().width()
695                if vector.x() < 0:
696                    x_pos = -x_pos
697            else:
698                # Using ratios to get x value
699                x_pos = vector.x() * math.fabs((self.boundingRect().height() / 2) / vector.y())
700
701            vector.setX(x_pos)
702            if vector.y() < 0:
703                vector.setY(-self.boundingRect().height() / 2)
704            else:
705                vector.setY(self.boundingRect().height() / 2)
706
707        # We got a vector from the center, now we get the final position
708        final_pos = our_pos + vector
709
710        return final_pos
711
712    # TODO: Potentially inefficient algorithm
713    def find_free_angle(self, pos, connection, decrease_angle=None):
714        """
715        Find a angle which doesn't collide with any other connection
716        at the same position.
717        :param pos: Position to find angle
718        :param connection: The current connection we are checking for
719        :param decrease_angle: If a specific direction is required, then use this variable
720                               to specific whether the final angle is positive or negative.
721                               Default is None.
722        """
723        angle = 0
724
725        if decrease_angle is None:
726            decrease_angle = False
727            angle_set = False
728        else:
729            angle_set = True
730
731        # Choose an angle, start with 0 degrees, and search through all connection points,
732        # looking for clashes
733        for compare in self.connection_list:
734            assert isinstance(compare, Connection_Widget.ConnectionWidget)
735
736            if compare is connection:
737                continue  # Not interested in the same connection, find others
738
739            # Get the current position and angle of the potential clashing connection
740            if compare.source_instance_widget is self:
741                compare_pos = compare.source_pos
742                compare_angle = compare.source_angle
743            elif compare.dest_instance_widget is self:
744                compare_pos = compare.dest_pos
745                compare_angle = compare.dest_angle
746            else:
747                raise NotImplementedError  # Something went wrong
748
749            if compare_pos != pos:
750                continue  # Does not clash, continue searching
751
752            # If clashing, find a angle which doesn't clash
753            while compare_angle == angle:
754                if angle_set:
755                    if decrease_angle:
756                        angle -= 35
757                    else:
758                        angle += 35
759                else:
760                    # If angle is not set, try 0, -35, 35, -70, 70 etc
761                    # In order to alternate between positive and negative,
762                    #    use decrease_angle as a toggle
763                    angle = -angle
764                    if decrease_angle:
765                        angle -= 35
766
767                    decrease_angle = not decrease_angle
768
769        return angle
770
771    # --- EVENTS ---
772    def itemChange(self, change, value):
773        """
774        Deals with position changes. Updates connections when ever position changes
775        :param change:
776        :param value:
777        :return:
778        """
779
780        if change == QtWidgets.QGraphicsWidget.ItemPositionHasChanged:
781            self.update_connections()
782
783        return super(InstanceWidget, self).itemChange(change, value)
784
785    def mousePressEvent(self, mouse_event):
786        """
787        Deals with instances being pressed. Right now doesn't do anything special other than
788        printing the name, type and number of connections
789        :param mouse_event:
790        :return:
791        """
792
793        string = " "
794
795        for connection in self.connection_list:
796            string += "%s " % connection.name
797
798        print "%s contains: %s" % (self.name, string)
799
800        no_of_connections = len(self.dataport) + len(self.provides) + len(self.consumes) + len(self.uses) + \
801                            len(self.emits)
802        print "\tNumber of connections is: %s" % str(no_of_connections)
803        print "\tdataport: %s" % str(len(self.dataport))
804        print "\tprovides: %s" % str(len(self.provides))
805        print "\tconsumes: %s" % str(len(self.consumes))
806        print "\tuses: %s" % str(len(self.uses))
807        print "\temits: %s" % str(len(self.emits))
808
809    def mouseMoveEvent(self, mouse_event):
810        """
811        Deals with this instance being clicked and dragged. Emits a signal that component was moved.
812        :param mouse_event:
813        :return:
814        """
815
816        self.widget_moved.emit()
817        super(InstanceWidget, self).mouseMoveEvent(mouse_event)
818
819    def contextMenuEvent(self, event):
820        """
821        Shows a context menu for this instance, asking to either show or hide the component.
822        Uses context menu given by graph widget.
823        :param event:
824        :return:
825        """
826
827        assert isinstance(event, QtWidgets.QGraphicsSceneContextMenuEvent)
828
829        # Get menu widget from proxy widget
830        menu = self.context_menu.widget()
831        assert isinstance(menu, QtWidgets.QMenu)
832
833        # If current hidden, action is "Show" otherwise "Hide"
834        menu.clear()
835        if self.hidden:
836            showComponentAction = menu.addAction("Show component")
837            showComponentAction.triggered.connect(self.show_component)
838        else:
839            hideComponentAction = menu.addAction("Hide component")
840            hideComponentAction.triggered.connect(self.hide_component)
841
842        # Set the current position [of proxy widget] to mouse click position
843        self.context_menu.setPos(event.scenePos())
844        menu.exec_()
845
846    def show_component(self):
847        self.hidden = False
848
849    def hide_component(self):
850        self.hidden = True
851