Total Complexity | 95 |
Total Lines | 528 |
Duplicated Lines | 0 % |
Complex classes like Orange.canvas.canvas.items.NodeItem often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | """ |
||
773 | class NodeItem(QGraphicsObject): |
||
774 | """ |
||
775 | An widget node item in the canvas. |
||
776 | """ |
||
777 | |||
778 | #: Signal emitted when the scene position of the node has changed. |
||
779 | positionChanged = Signal() |
||
780 | |||
781 | #: Signal emitted when the geometry of the channel anchors changes. |
||
782 | anchorGeometryChanged = Signal() |
||
783 | |||
784 | #: Signal emitted when the item has been activated (by a mouse double |
||
785 | #: click or a keyboard) |
||
786 | activated = Signal() |
||
787 | |||
788 | #: The item is under the mouse. |
||
789 | hovered = Signal() |
||
790 | |||
791 | #: Span of the anchor in degrees |
||
792 | ANCHOR_SPAN_ANGLE = 90 |
||
793 | |||
794 | #: Z value of the item |
||
795 | Z_VALUE = 100 |
||
796 | |||
797 | def __init__(self, widget_description=None, parent=None, **kwargs): |
||
798 | self.__boundingRect = None |
||
799 | QGraphicsObject.__init__(self, parent, **kwargs) |
||
800 | self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) |
||
801 | self.setFlag(QGraphicsItem.ItemHasNoContents, True) |
||
802 | self.setFlag(QGraphicsItem.ItemIsSelectable, True) |
||
803 | self.setFlag(QGraphicsItem.ItemIsMovable, True) |
||
804 | self.setFlag(QGraphicsItem.ItemIsFocusable, True) |
||
805 | |||
806 | # central body shape item |
||
807 | self.shapeItem = None |
||
808 | |||
809 | # in/output anchor items |
||
810 | self.inputAnchorItem = None |
||
811 | self.outputAnchorItem = None |
||
812 | |||
813 | # title text item |
||
814 | self.captionTextItem = None |
||
815 | |||
816 | # error, warning, info items |
||
817 | self.errorItem = None |
||
818 | self.warningItem = None |
||
819 | self.infoItem = None |
||
820 | |||
821 | self.__title = "" |
||
822 | self.__processingState = 0 |
||
823 | self.__progress = -1 |
||
824 | self.__statusMessage = "" |
||
825 | |||
826 | self.__error = None |
||
827 | self.__warning = None |
||
828 | self.__info = None |
||
829 | |||
830 | self.__anchorLayout = None |
||
831 | self.__animationEnabled = False |
||
832 | |||
833 | self.setZValue(self.Z_VALUE) |
||
834 | self.setupGraphics() |
||
835 | |||
836 | self.setWidgetDescription(widget_description) |
||
837 | |||
838 | @classmethod |
||
839 | def from_node(cls, node): |
||
840 | """ |
||
841 | Create an :class:`NodeItem` instance and initialize it from a |
||
842 | :class:`SchemeNode` instance. |
||
843 | |||
844 | """ |
||
845 | self = cls() |
||
846 | self.setWidgetDescription(node.description) |
||
847 | # self.setCategoryDescription(node.category) |
||
848 | return self |
||
849 | |||
850 | @classmethod |
||
851 | def from_node_meta(cls, meta_description): |
||
852 | """ |
||
853 | Create an `NodeItem` instance from a node meta description. |
||
854 | """ |
||
855 | self = cls() |
||
856 | self.setWidgetDescription(meta_description) |
||
857 | return self |
||
858 | |||
859 | def setupGraphics(self): |
||
860 | """ |
||
861 | Set up the graphics. |
||
862 | """ |
||
863 | shape_rect = QRectF(-24, -24, 48, 48) |
||
864 | |||
865 | self.shapeItem = NodeBodyItem(self) |
||
866 | self.shapeItem.setShapeRect(shape_rect) |
||
867 | self.shapeItem.setAnimationEnabled(self.__animationEnabled) |
||
868 | |||
869 | # Rect for widget's 'ears'. |
||
870 | anchor_rect = QRectF(-31, -31, 62, 62) |
||
871 | self.inputAnchorItem = SinkAnchorItem(self) |
||
872 | input_path = QPainterPath() |
||
873 | start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2 |
||
874 | input_path.arcMoveTo(anchor_rect, start_angle) |
||
875 | input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE) |
||
876 | self.inputAnchorItem.setAnchorPath(input_path) |
||
877 | |||
878 | self.outputAnchorItem = SourceAnchorItem(self) |
||
879 | output_path = QPainterPath() |
||
880 | start_angle = self.ANCHOR_SPAN_ANGLE / 2 |
||
881 | output_path.arcMoveTo(anchor_rect, start_angle) |
||
882 | output_path.arcTo(anchor_rect, start_angle, - self.ANCHOR_SPAN_ANGLE) |
||
883 | self.outputAnchorItem.setAnchorPath(output_path) |
||
884 | |||
885 | self.inputAnchorItem.hide() |
||
886 | self.outputAnchorItem.hide() |
||
887 | |||
888 | # Title caption item |
||
889 | self.captionTextItem = NameTextItem(self) |
||
890 | |||
891 | self.captionTextItem.setPlainText("") |
||
892 | self.captionTextItem.setPos(0, 33) |
||
893 | |||
894 | def iconItem(standard_pixmap): |
||
895 | item = GraphicsIconItem(self, icon=standard_icon(standard_pixmap), |
||
896 | iconSize=QSize(16, 16)) |
||
897 | item.hide() |
||
898 | return item |
||
899 | |||
900 | self.errorItem = iconItem(QStyle.SP_MessageBoxCritical) |
||
901 | self.warningItem = iconItem(QStyle.SP_MessageBoxWarning) |
||
902 | self.infoItem = iconItem(QStyle.SP_MessageBoxInformation) |
||
903 | |||
904 | self.prepareGeometryChange() |
||
905 | self.__boundingRect = None |
||
906 | |||
907 | # TODO: Remove the set[Widget|Category]Description. The user should |
||
908 | # handle setting of icons, title, ... |
||
909 | def setWidgetDescription(self, desc): |
||
910 | """ |
||
911 | Set widget description. |
||
912 | """ |
||
913 | self.widget_description = desc |
||
914 | if desc is None: |
||
915 | return |
||
916 | |||
917 | icon = icon_loader.from_description(desc).get(desc.icon) |
||
918 | if icon: |
||
919 | self.setIcon(icon) |
||
920 | |||
921 | if not self.title(): |
||
922 | self.setTitle(desc.name) |
||
923 | |||
924 | if desc.inputs: |
||
925 | self.inputAnchorItem.show() |
||
926 | if desc.outputs: |
||
927 | self.outputAnchorItem.show() |
||
928 | |||
929 | tooltip = NodeItem_toolTipHelper(self) |
||
930 | self.setToolTip(tooltip) |
||
931 | |||
932 | def setWidgetCategory(self, desc): |
||
933 | """ |
||
934 | Set the widget category. |
||
935 | """ |
||
936 | self.category_description = desc |
||
937 | if desc and desc.background: |
||
938 | background = NAMED_COLORS.get(desc.background, desc.background) |
||
939 | color = QColor(background) |
||
940 | if color.isValid(): |
||
941 | self.setColor(color) |
||
942 | |||
943 | def setIcon(self, icon): |
||
944 | """ |
||
945 | Set the node item's icon (:class:`QIcon`). |
||
946 | """ |
||
947 | if isinstance(icon, QIcon): |
||
948 | self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon, |
||
949 | iconSize=QSize(36, 36)) |
||
950 | self.icon_item.setPos(-18, -18) |
||
951 | else: |
||
952 | raise TypeError |
||
953 | |||
954 | def setColor(self, color, selectedColor=None): |
||
955 | """ |
||
956 | Set the widget color. |
||
957 | """ |
||
958 | if selectedColor is None: |
||
959 | selectedColor = saturated(color, 150) |
||
960 | palette = create_palette(color, selectedColor) |
||
961 | self.shapeItem.setPalette(palette) |
||
962 | |||
963 | def setPalette(self, palette): |
||
964 | # TODO: The palette should override the `setColor` |
||
965 | raise NotImplementedError |
||
966 | |||
967 | def setTitle(self, title): |
||
968 | """ |
||
969 | Set the node title. The title text is displayed at the bottom of the |
||
970 | node. |
||
971 | |||
972 | """ |
||
973 | self.__title = title |
||
974 | self.__updateTitleText() |
||
975 | |||
976 | def title(self): |
||
977 | """ |
||
978 | Return the node title. |
||
979 | """ |
||
980 | return self.__title |
||
981 | |||
982 | title_ = Property(str, fget=title, fset=setTitle, |
||
983 | doc="Node title text.") |
||
984 | |||
985 | def setFont(self, font): |
||
986 | """ |
||
987 | Set the title text font (:class:`QFont`). |
||
988 | """ |
||
989 | if font != self.font(): |
||
990 | self.prepareGeometryChange() |
||
991 | self.captionTextItem.setFont(font) |
||
992 | self.__updateTitleText() |
||
993 | |||
994 | def font(self): |
||
995 | """ |
||
996 | Return the title text font. |
||
997 | """ |
||
998 | return self.captionTextItem.font() |
||
999 | |||
1000 | def setAnimationEnabled(self, enabled): |
||
1001 | """ |
||
1002 | Set the node animation enabled state. |
||
1003 | """ |
||
1004 | if self.__animationEnabled != enabled: |
||
1005 | self.__animationEnabled = enabled |
||
1006 | self.shapeItem.setAnimationEnabled(enabled) |
||
1007 | |||
1008 | def animationEnabled(self): |
||
1009 | """ |
||
1010 | Are node animations enabled. |
||
1011 | """ |
||
1012 | return self.__animationEnabled |
||
1013 | |||
1014 | def setProcessingState(self, state): |
||
1015 | """ |
||
1016 | Set the node processing state i.e. the node is processing |
||
1017 | (is busy) or is idle. |
||
1018 | |||
1019 | """ |
||
1020 | if self.__processingState != state: |
||
1021 | self.__processingState = state |
||
1022 | self.shapeItem.setProcessingState(state) |
||
1023 | if not state: |
||
1024 | # Clear the progress meter. |
||
1025 | self.setProgress(-1) |
||
1026 | if self.__animationEnabled: |
||
1027 | self.shapeItem.ping() |
||
1028 | |||
1029 | def processingState(self): |
||
1030 | """ |
||
1031 | The node processing state. |
||
1032 | """ |
||
1033 | return self.__processingState |
||
1034 | |||
1035 | processingState_ = Property(int, fget=processingState, |
||
1036 | fset=setProcessingState) |
||
1037 | |||
1038 | def setProgress(self, progress): |
||
1039 | """ |
||
1040 | Set the node work progress state (number between 0 and 100). |
||
1041 | """ |
||
1042 | if progress is None or progress < 0 or not self.__processingState: |
||
1043 | progress = -1 |
||
1044 | |||
1045 | progress = max(min(progress, 100), -1) |
||
1046 | if self.__progress != progress: |
||
1047 | self.__progress = progress |
||
1048 | self.shapeItem.setProgress(progress) |
||
1049 | self.__updateTitleText() |
||
1050 | |||
1051 | def progress(self): |
||
1052 | """ |
||
1053 | Return the node work progress state. |
||
1054 | """ |
||
1055 | return self.__progress |
||
1056 | |||
1057 | progress_ = Property(float, fget=progress, fset=setProgress, |
||
1058 | doc="Node progress state.") |
||
1059 | |||
1060 | def setStatusMessage(self, message): |
||
1061 | """ |
||
1062 | Set the node status message text. |
||
1063 | |||
1064 | This text is displayed below the node's title. |
||
1065 | |||
1066 | """ |
||
1067 | if self.__statusMessage != message: |
||
1068 | self.__statusMessage = message |
||
1069 | self.__updateTitleText() |
||
1070 | |||
1071 | def statusMessage(self): |
||
1072 | return self.__statusMessage |
||
1073 | |||
1074 | def setStateMessage(self, message): |
||
1075 | """ |
||
1076 | Set a state message to display over the item. |
||
1077 | |||
1078 | Parameters |
||
1079 | ---------- |
||
1080 | message : UserMessage |
||
1081 | Message to display. `message.severity` is used to determine |
||
1082 | the icon and `message.contents` is used as a tool tip. |
||
1083 | |||
1084 | """ |
||
1085 | # TODO: Group messages by message_id not by severity |
||
1086 | # and deprecate set[Error|Warning|Error]Message |
||
1087 | if message.severity == UserMessage.Info: |
||
1088 | self.setInfoMessage(message.contents) |
||
1089 | elif message.severity == UserMessage.Warning: |
||
1090 | self.setWarningMessage(message.contents) |
||
1091 | elif message.severity == UserMessage.Error: |
||
1092 | self.setErrorMessage(message.contents) |
||
1093 | |||
1094 | def setErrorMessage(self, message): |
||
1095 | if self.__error != message: |
||
1096 | self.__error = message |
||
1097 | self.__updateMessages() |
||
1098 | |||
1099 | def setWarningMessage(self, message): |
||
1100 | if self.__warning != message: |
||
1101 | self.__warning = message |
||
1102 | self.__updateMessages() |
||
1103 | |||
1104 | def setInfoMessage(self, message): |
||
1105 | if self.__info != message: |
||
1106 | self.__info = message |
||
1107 | self.__updateMessages() |
||
1108 | |||
1109 | def newInputAnchor(self): |
||
1110 | """ |
||
1111 | Create and return a new input :class:`AnchorPoint`. |
||
1112 | """ |
||
1113 | if not (self.widget_description and self.widget_description.inputs): |
||
1114 | raise ValueError("Widget has no inputs.") |
||
1115 | |||
1116 | anchor = AnchorPoint() |
||
1117 | self.inputAnchorItem.addAnchor(anchor, position=1.0) |
||
1118 | |||
1119 | positions = self.inputAnchorItem.anchorPositions() |
||
1120 | positions = uniform_linear_layout(positions) |
||
1121 | self.inputAnchorItem.setAnchorPositions(positions) |
||
1122 | |||
1123 | return anchor |
||
1124 | |||
1125 | def removeInputAnchor(self, anchor): |
||
1126 | """ |
||
1127 | Remove input anchor. |
||
1128 | """ |
||
1129 | self.inputAnchorItem.removeAnchor(anchor) |
||
1130 | |||
1131 | positions = self.inputAnchorItem.anchorPositions() |
||
1132 | positions = uniform_linear_layout(positions) |
||
1133 | self.inputAnchorItem.setAnchorPositions(positions) |
||
1134 | |||
1135 | def newOutputAnchor(self): |
||
1136 | """ |
||
1137 | Create and return a new output :class:`AnchorPoint`. |
||
1138 | """ |
||
1139 | if not (self.widget_description and self.widget_description.outputs): |
||
1140 | raise ValueError("Widget has no outputs.") |
||
1141 | |||
1142 | anchor = AnchorPoint(self) |
||
1143 | self.outputAnchorItem.addAnchor(anchor, position=1.0) |
||
1144 | |||
1145 | positions = self.outputAnchorItem.anchorPositions() |
||
1146 | positions = uniform_linear_layout(positions) |
||
1147 | self.outputAnchorItem.setAnchorPositions(positions) |
||
1148 | |||
1149 | return anchor |
||
1150 | |||
1151 | def removeOutputAnchor(self, anchor): |
||
1152 | """ |
||
1153 | Remove output anchor. |
||
1154 | """ |
||
1155 | self.outputAnchorItem.removeAnchor(anchor) |
||
1156 | |||
1157 | positions = self.outputAnchorItem.anchorPositions() |
||
1158 | positions = uniform_linear_layout(positions) |
||
1159 | self.outputAnchorItem.setAnchorPositions(positions) |
||
1160 | |||
1161 | def inputAnchors(self): |
||
1162 | """ |
||
1163 | Return a list of all input anchor points. |
||
1164 | """ |
||
1165 | return self.inputAnchorItem.anchorPoints() |
||
1166 | |||
1167 | def outputAnchors(self): |
||
1168 | """ |
||
1169 | Return a list of all output anchor points. |
||
1170 | """ |
||
1171 | return self.outputAnchorItem.anchorPoints() |
||
1172 | |||
1173 | def setAnchorRotation(self, angle): |
||
1174 | """ |
||
1175 | Set the anchor rotation. |
||
1176 | """ |
||
1177 | self.inputAnchorItem.setRotation(angle) |
||
1178 | self.outputAnchorItem.setRotation(angle) |
||
1179 | self.anchorGeometryChanged.emit() |
||
1180 | |||
1181 | def anchorRotation(self): |
||
1182 | """ |
||
1183 | Return the anchor rotation. |
||
1184 | """ |
||
1185 | return self.inputAnchorItem.rotation() |
||
1186 | |||
1187 | def boundingRect(self): |
||
1188 | # TODO: Important because of this any time the child |
||
1189 | # items change geometry the self.prepareGeometryChange() |
||
1190 | # needs to be called. |
||
1191 | if self.__boundingRect is None: |
||
1192 | self.__boundingRect = self.childrenBoundingRect() |
||
1193 | return self.__boundingRect |
||
1194 | |||
1195 | def shape(self): |
||
1196 | # Shape for mouse hit detection. |
||
1197 | # TODO: Should this return the union of all child items? |
||
1198 | return self.shapeItem.shape() |
||
1199 | |||
1200 | def __updateTitleText(self): |
||
1201 | """ |
||
1202 | Update the title text item. |
||
1203 | """ |
||
1204 | text = ['<div align="center">%s' % escape(self.title())] |
||
1205 | |||
1206 | status_text = [] |
||
1207 | |||
1208 | progress_included = False |
||
1209 | if self.__statusMessage: |
||
1210 | msg = escape(self.__statusMessage) |
||
1211 | format_fields = dict(parse_format_fields(msg)) |
||
1212 | if "progress" in format_fields and len(format_fields) == 1: |
||
1213 | # Insert progress into the status text format string. |
||
1214 | spec, _ = format_fields["progress"] |
||
1215 | if spec != None: |
||
1216 | progress_included = True |
||
1217 | progress_str = "{0:.0f}%".format(self.progress()) |
||
1218 | status_text.append(msg.format(progress=progress_str)) |
||
1219 | else: |
||
1220 | status_text.append(msg) |
||
1221 | |||
1222 | if self.progress() >= 0 and not progress_included: |
||
1223 | status_text.append("%i%%" % int(self.progress())) |
||
1224 | |||
1225 | if status_text: |
||
1226 | text += ["<br/>", |
||
1227 | '<span style="font-style: italic">', |
||
1228 | "<br/>".join(status_text), |
||
1229 | "</span>"] |
||
1230 | text += ["</div>"] |
||
1231 | text = "".join(text) |
||
1232 | |||
1233 | # The NodeItems boundingRect could change. |
||
1234 | self.prepareGeometryChange() |
||
1235 | self.__boundingRect = None |
||
1236 | self.captionTextItem.setHtml(text) |
||
1237 | self.captionTextItem.document().adjustSize() |
||
1238 | width = self.captionTextItem.textWidth() |
||
1239 | self.captionTextItem.setPos(-width / 2.0, 33) |
||
1240 | |||
1241 | def __updateMessages(self): |
||
1242 | """ |
||
1243 | Update message items (position, visibility and tool tips). |
||
1244 | """ |
||
1245 | items = [self.errorItem, self.warningItem, self.infoItem] |
||
1246 | |||
1247 | messages = [self.__error, self.__warning, self.__info] |
||
1248 | for message, item in zip(messages, items): |
||
1249 | item.setVisible(bool(message)) |
||
1250 | item.setToolTip(message or "") |
||
1251 | |||
1252 | shown = [item for item in items if item.isVisible()] |
||
1253 | count = len(shown) |
||
1254 | if count: |
||
1255 | spacing = 3 |
||
1256 | rects = [item.boundingRect() for item in shown] |
||
1257 | width = sum(rect.width() for rect in rects) |
||
1258 | width += spacing * max(0, count - 1) |
||
1259 | height = max(rect.height() for rect in rects) |
||
1260 | origin = self.shapeItem.boundingRect().top() - spacing - height |
||
1261 | origin = QPointF(-width / 2, origin) |
||
1262 | for item, rect in zip(shown, rects): |
||
1263 | item.setPos(origin) |
||
1264 | origin = origin + QPointF(rect.width() + spacing, 0) |
||
1265 | |||
1266 | def mousePressEvent(self, event): |
||
1267 | if self.shapeItem.path().contains(event.pos()): |
||
1268 | return QGraphicsObject.mousePressEvent(self, event) |
||
1269 | else: |
||
1270 | event.ignore() |
||
1271 | |||
1272 | def mouseDoubleClickEvent(self, event): |
||
1273 | if self.shapeItem.path().contains(event.pos()): |
||
1274 | QGraphicsObject.mouseDoubleClickEvent(self, event) |
||
1275 | QTimer.singleShot(0, self.activated.emit) |
||
1276 | else: |
||
1277 | event.ignore() |
||
1278 | |||
1279 | def contextMenuEvent(self, event): |
||
1280 | if self.shapeItem.path().contains(event.pos()): |
||
1281 | return QGraphicsObject.contextMenuEvent(self, event) |
||
1282 | else: |
||
1283 | event.ignore() |
||
1284 | |||
1285 | def focusInEvent(self, event): |
||
1286 | self.shapeItem.setHasFocus(True) |
||
1287 | return QGraphicsObject.focusInEvent(self, event) |
||
1288 | |||
1289 | def focusOutEvent(self, event): |
||
1290 | self.shapeItem.setHasFocus(False) |
||
1291 | return QGraphicsObject.focusOutEvent(self, event) |
||
1292 | |||
1293 | def itemChange(self, change, value): |
||
1294 | if change == QGraphicsItem.ItemSelectedChange: |
||
1295 | self.shapeItem.setSelected(value) |
||
1296 | self.captionTextItem.setSelectionState(value) |
||
1297 | elif change == QGraphicsItem.ItemPositionHasChanged: |
||
1298 | self.positionChanged.emit() |
||
1299 | |||
1300 | return QGraphicsObject.itemChange(self, change, value) |
||
1301 | |||
1362 |