class StartArrowTouchArea(AddBelowTouchArea): __qt_type_id__ = next_available_type_id() def contextual_color(self): if self._hovering: return ctrl.cm.hovering(ctrl.cm.ui()) else: return ctrl.cm.ui() def boundingRect(self): """ :return: """ return QtCore.QRectF(-10, -5, 20, 15) def paint(self, painter, option, widget): """ :param painter: :param option: :param widget: :raise: """ if ctrl.pressed is self: pass c = self.contextual_color() painter.setPen(c) draw_plus(painter, -5, 5) #painter.setBrush(c) draw_arrow_shape_from_points(painter, -2, 0, 8, 7, c) if self._hovering: painter.drawRoundedRect(self.boundingRect(), 4, 4)
class RightAddTop(JointedTouchArea): __qt_type_id__ = next_available_type_id() def __init__(self, host, action): super().__init__(host, action) self._align_left = False self.update_end_points() def paint(self, painter, option, widget): """ :param painter: :param option: :param widget: :raise: """ if ctrl.pressed is self: pass c = self.contextual_color() painter.setPen(c) if self._fill_path: painter.fillPath(self._path, c) else: painter.drawPath(self._path) if self._hovering: painter.save() painter.setBrush(ctrl.cm.ui()) painter.rotate(-160) draw_leaf(painter, 0, end_spot_size / 2, end_spot_size) painter.restore() draw_plus(painter, 14, 0)
class RemoveTriangleTouchArea(AddBelowTouchArea): __qt_type_id__ = next_available_type_id() def update_end_points(self): x, y = self.host.centered_scene_position lbr = self.host.label_object.boundingRect() y += self.host.label_object.triangle_y + lbr.top() + end_spot_size + 2 self.end_point = x, y self.start_point = self.end_point self.setPos(x, y) def paint(self, painter, option, widget): """ :param painter: :param option: :param widget: :raise: """ if ctrl.pressed is self: pass c = self.contextual_color() painter.setPen(c) #draw_triangle(painter, 0, 0) draw_x(painter, 0, 0, end_spot_size / 2) if self._hovering: p = QtGui.QPen(c) p.setWidth(2) painter.setPen(p) draw_x(painter, 0, 0, end_spot_size)
class AddBelowTouchArea(TouchArea): __qt_type_id__ = next_available_type_id() def update_end_points(self): x, y = self.host.centered_scene_position y += self.host.height / 2 + end_spot_size self.end_point = x, y self.start_point = self.end_point self.setPos(self.end_point[0], self.end_point[1]) def paint(self, painter, option, widget): """ :param painter: :param option: :param widget: :raise: """ if ctrl.pressed is self: pass c = self.contextual_color() if self._hovering: painter.setBrush(ctrl.cm.ui_tr()) else: painter.setBrush(qt_prefs.no_brush) painter.setPen(c) draw_tailed_leaf(painter, 0, 0, end_spot_size) if self._hovering: painter.setPen(c) draw_plus(painter, 4, 0)
class BranchingTouchArea(TouchArea): """ TouchArea that connects to edges and has /-shape. Used to add/merge nodes in middle of the trees. """ __qt_type_id__ = next_available_type_id() spot_size = end_spot_size * 0.5 def boundingRect(self): """ :return: """ if not self.end_point: self.update_end_points() assert self.end_point # Bounding rect that includes the tail and end spot ellipse ex, ey = 0, 0 sx, sy = sub_xy(self.start_point, self.end_point) ss2 = BranchingTouchArea.spot_size * 2 ss = BranchingTouchArea.spot_size if sx < ex: w = max((ex - sx + ss, ss2)) x = min((sx, ex - ss)) else: w = max((sx - ex + ss, ss2)) x = ex - ss if sy < ey: h = max((ey - sy + ss, ss2)) y = min((sy, ey - ss)) else: h = max((sy - ey + ss, ss2)) y = ey - ss r = QtCore.QRectF(x, y, w, h) return r def drop(self, dropped_node): """ Connect dropped node to host of this TouchArea. Connection depends on which merge area this is: top left, top right, left, right :param dropped_node: """ if isinstance(dropped_node, str): dropped_node = self.make_node_from_string(dropped_node) if not dropped_node: return assert (self.host.start and self.host.end) adjustment = self.host.end.adjustment # host is an edge ctrl.free_drawing.insert_node_between(dropped_node, self.host.start, self.host.end, self._align_left, self.start_point) for node in ctrl.dragged_set: node.adjustment = adjustment ctrl.forest.forest_edited() return 'moved node %s to sibling of %s' % (dropped_node, self.host)
class RightAddSibling(BranchingTouchArea): """ TouchArea that connects to edges and has /-shape. Used to add/merge nodes in middle of the trees. """ __qt_type_id__ = next_available_type_id() def __init__(self, host, action): super().__init__(host, action) self._align_left = False self.update_end_points() def drag(self, event): self._dragging = True self.update_end_points(end_point=to_tuple(event.scenePos())) def update_end_points(self, end_point=None): """ :param end_point: End point can be given or it can be calculated. """ e = self.host sx, sy = to_tuple(e.get_point_at(0.4)) self.start_point = sx, sy if end_point: self.end_point = end_point else: d = e.get_angle_at(0.4) d += 60 # 75 angle = math.radians(-d) dx = math.cos(angle) dy = math.sin(angle) l = 12 x = sx + dx * l y = sy + dy * l self.end_point = x, y self.setPos(self.end_point[0], self.end_point[1]) def paint(self, painter, option, widget): """ :param painter: :param option: :param widget: :raise: """ if ctrl.pressed is self: pass c = self.contextual_color() painter.setPen(c) painter.drawLine(*sub_xy(self.start_point, self.end_point), 0, 0) if self._hovering: painter.save() painter.setBrush(ctrl.cm.ui()) painter.rotate(-160) draw_leaf(painter, 0, BranchingTouchArea.spot_size / 2, BranchingTouchArea.spot_size) painter.restore() draw_plus(painter, 14, 0)
class LeftAddChild(BranchingTouchArea): """ TouchArea that adds children to nodes and has /-shape. Used to add nodes to leaf nodes.""" __qt_type_id__ = next_available_type_id() def __init__(self, host, action): super().__init__(host, action) self._align_left = True def update_end_points(self, end_point=None): """ :param end_point: End point can be given or it can be calculated. """ shape_name = ctrl.settings.get_edge_setting( 'shape_name', edge_type=g.CONSTITUENT_EDGE) self._fill_path = ctrl.settings.get_shape_setting( 'fill', edge_type=g.CONSTITUENT_EDGE) sx, sy = self.host.magnet(7) self.start_point = sx, sy if end_point: self.end_point = end_point else: ex = sx - 20 # 75 ey = sy + 10 self.end_point = ex, ey self.setPos(self.end_point[0], self.end_point[1]) rel_sp = sub_xy(self.start_point, self.end_point) adjust = [] self._path = SHAPE_PRESETS[shape_name].path(rel_sp, (0, 0), alignment=g.LEFT, curve_adjustment=adjust)[0] def paint(self, painter, option, widget): """ :param painter: :param option: :param widget: :raise: """ if ctrl.pressed is self: pass c = self.contextual_color() painter.setPen(c) if self._fill_path: painter.fillPath(self._path, c) else: painter.drawPath(self._path) if self._hovering: painter.save() painter.setBrush(ctrl.cm.ui()) painter.rotate(20) draw_leaf(painter, 0, end_spot_size / 2, end_spot_size) painter.restore() draw_plus(painter, 4, 0)
class MarkerStartPoint(QtWidgets.QGraphicsItem): __qt_type_id__ = next_available_type_id() def __init__(self, parent): QtWidgets.QGraphicsItem.__init__(self, parent) self.setCursor(QtCore.Qt.CrossCursor) self.setAcceptHoverEvents(True) def type(self): """ Qt's type identifier, custom QGraphicsItems should have different type ids if events need to differentiate between them. These are set when the program starts. :return: """ return self.__qt_type_id__ def paint(self, painter, options, QWidget_widget=None): if prefs.touch: p = QtGui.QPen(ctrl.cm.ui_tr()) p.setWidth(2) painter.setPen(p) painter.drawEllipse(-6, -6, 12, 12) else: p = QtGui.QPen(ctrl.cm.ui()) p.setWidthF(0.5) painter.setPen(p) painter.drawRect(-2, -2, 4, 4) def boundingRect(self): if prefs.touch: return QtCore.QRectF(-6, -6, 12, 12) else: return QtCore.QRectF(-2, -2, 4, 4) def mouseMoveEvent(self, event): if ctrl.pressed is self: if ctrl.dragged_set or (event.buttonDownScenePos( QtCore.Qt.LeftButton) - event.scenePos()).manhattanLength() > 6: self.drag(event) ctrl.graph_scene.dragging_over(event.scenePos()) def mouseReleaseEvent(self, event): if ctrl.pressed is self: ctrl.release(self) if ctrl.dragged_set: ctrl.graph_scene.kill_dragging() return None # this mouseRelease is now consumed super().mouseReleaseEvent(event) def drag(self, event): pi = self.parentItem() if pi: pi.set_dragged(True) pi.update_position(event.scenePos()) def drop_to(self, x, y, recipient=None): pass
class ProjectionVisual(QtWidgets.QGraphicsItem): """ Transparent overlay to show which nodes belong to one projection """ __qt_type_id__ = next_available_type_id() def __init__(self, data): super().__init__() self.d = data self.color = ctrl.cm.get(self.d.color_tr_id) #self.show() def type(self): """ Qt's type identifier, custom QGraphicsItems should have different type ids if events need to differentiate between them. These are set when the program starts. :return: """ return self.__qt_type_id__ def boundingRect(self): br = QtCore.QRectF() for chain in self.d.chains: for node in chain: br |= node.sceneBoundingRect() return br def paint(self, painter, style, QWidget_widget=None): painter.setBrush(self.color) painter.setPen(QtCore.Qt.NoPen) for chain in self.d.chains: vis_chain = [x for x in chain if x.is_visible()] if len(vis_chain) < 2: continue forward = [] back = [] sx = 0 sy = 0 start_x, start_y = vis_chain[0].current_scene_position # shape will be one continuous filled polygon, so when we iterate # through nodes it needs to go through we make list of positions for # polygon going there (forward) and for its return trip (back). for node in vis_chain: sx, sy = node.current_scene_position # r = node.sceneBoundingRect() # p.addEllipse(r) forward.append((sx - 5, sy)) back.append((sx + 5, sy)) back.reverse() p = QtGui.QPainterPath(QtCore.QPointF(start_x, start_y + 5)) for x, y in forward + [(sx, sy - 5)] + back: p.lineTo(x, y) painter.fillPath(p, self.color)
class DeleteArrowTouchArea(TouchArea): __qt_type_id__ = next_available_type_id() def paint(self, painter, option, widget): """ :param painter: :param option: :param widget: :raise: """ if ctrl.pressed is self: pass painter.setPen(self.contextual_color()) draw_x(painter, 0, 0, end_spot_size)
class ConnectGlossTouchArea(AddBelowTouchArea): __qt_type_id__ = next_available_type_id() def __init__(self, host, action): super().__init__(host, action) self.set_tip("Add gloss for node") def drop(self, dropped_node): """ :param dropped_node: """ if isinstance(dropped_node, str): dropped_node = self.make_node_from_string(dropped_node) if not dropped_node: return ctrl.free_drawing.add_gloss_to_node(dropped_node, self.host) ctrl.forest.forest_edited() return 'added gloss %s to %s' % (dropped_node, self.host)
class AddTriangleTouchArea(AddBelowTouchArea): __qt_type_id__ = next_available_type_id() def paint(self, painter, option, widget): """ :param painter: :param option: :param widget: :raise: """ if ctrl.pressed is self: pass c = self.contextual_color() painter.setPen(c) draw_triangle(painter, 0, 0) if self._hovering: painter.setPen(c) draw_triangle(painter, 0, -5, 6)
class GlowRing(QtWidgets.QGraphicsEllipseItem): """ Decoration for radial menus """ __qt_type_id__ = next_available_type_id() def __init__(self, parent, radius=40): QtWidgets.QGraphicsEllipseItem.__init__(self, QtCore.QRectF(0, 0, 0, 0), parent) pen = QtGui.QPen(ctrl.cm.ui()) pen.setWidth(4) self.setPen(pen) glow = QtWidgets.QGraphicsBlurEffect(parent) glow.setBlurRadius(7) glow.setEnabled(True) self.setGraphicsEffect(glow) self._radius = 0 self._max_radius = radius self._step_size = radius / 6.0 def type(self): """ Qt's type identifier, custom QGraphicsItems should have different type ids if events need to differentiate between them. These are set when the program starts. :return: """ return self.__qt_type_id__ def grow(self): """ """ self._radius += self._step_size self.setRect(-self._radius, -self._radius, 2 * self._radius, 2 * self._radius) def shrink(self): """ """ self.radius -= self.step_size self.setRect(-self.radius, -self.radius, 2 * self.radius, 2 * self.radius)
class ActivityMarker(QtWidgets.QGraphicsRectItem, UIGraphicsItem): """ Blinky thing to announce that computing is going on. """ __qt_type_id__ = next_available_type_id() def __init__(self, role=0, ui_key=None): QtWidgets.QGraphicsRectItem.__init__(self, QtCore.QRectF( 0, 0, 4, 4)) # , scene = parent) UIGraphicsItem.__init__(self, ui_key=ui_key) self.role = int(role) self.setZValue(100) self.setBrush(ctrl.cm.get('accent%s' % str(self.role + 1))) self.setPen(qt_prefs.no_pen) # QtCore.Qt.NoPen self.setPos(5 + self.role * 10, 5) def type(self): """ Qt's type identifier, custom QGraphicsItems should have different type ids if events need to differentiate between them. These are set when the program starts. :return: """ return self.__qt_type_id__ def update_position(self): """ stay always in initial position """ pass
class AddConstituentTouchArea(TouchArea): __qt_type_id__ = next_available_type_id() def click(self, event=None): """ :param event: """ self._dragging = False if self._drag_hint: return False self.host.open_embed() return True def drop(self, dropped_node): """ Connect dropped node to host of this TouchArea. Connection depends on which merge area this is: top left, top right, left, right :param dropped_node: """ if isinstance(dropped_node, str): self.make_node_from_string(dropped_node) ctrl.forest.forest_edited()
class ChildTouchArea(TouchArea): """ TouchArea that adds children to nodes and has /-shape. Used to add nodes to leaf nodes. """ __qt_type_id__ = next_available_type_id() def __init__(self, host, action): super().__init__(host, action) self.update_end_points() def boundingRect(self): """ :return: """ if not self.end_point: self.update_end_points() assert self.end_point # Bounding rect that includes the tail and end spot ellipse ex, ey = 0, 0 sx, sy = sub_xy(self.start_point, self.end_point) e2 = end_spot_size * 2 if sx < ex: w = max((ex - sx + end_spot_size, e2)) x = min((sx, ex - end_spot_size)) else: w = max((sx - ex + end_spot_size, e2)) x = ex - end_spot_size if sy < ey: h = max((ey - sy + end_spot_size, e2)) y = min((sy, ey - end_spot_size)) else: h = max((sy - ey + end_spot_size, e2)) y = ey - end_spot_size r = QtCore.QRectF(x, y, w, h) return r def drag(self, event): self._dragging = True self.update_end_points(end_point=to_tuple(event.scenePos())) def drop(self, dropped_node): """ Connect dropped node to host of this TouchArea. Connection depends on which merge area this is: top left, top right, left, right :param dropped_node: """ if isinstance(dropped_node, str): dropped_node = self.make_node_from_string(dropped_node) if not dropped_node: return # host is an edge ctrl.free_drawing.insert_node_between(dropped_node, self.host.start, self.host.end, self._align_left, self.start_point) for node in ctrl.dragged_set: node.adjustment = self.host.end.adjustment message = 'moved node %s to sibling of %s' % (dropped_node, self.host) ctrl.forest.forest_edited() return message
class ConstituentNode(Node): """ ConstituentNode is enriched with few elements that have no syntactic meaning but help with reading the trees aliases, indices and glosses. """ __qt_type_id__ = next_available_type_id() display_name = ('Constituent', 'Constituents') display = True width = 20 height = 20 is_constituent = True node_type = g.CONSTITUENT_NODE wraps = 'constituent' editable = { } # Uses custom ConstituentNodeEmbed instead of template-based NodeEditEmbed default_style = { 'plain': { 'color_id': 'content1', 'font_id': g.MAIN_FONT, 'font-size': 10 }, 'fancy': { 'color_id': 'content1', 'font_id': g.MAIN_FONT, 'font-size': 10 } } default_edge = g.CONSTITUENT_EDGE # Touch areas are UI elements that scale with the trees: they can be # temporary shapes suggesting to drag or click here to create the # suggested shape. # touch_areas_when_dragging and touch_areas_when_selected use the same # format. # 'condition': there are some general conditions implemented in UIManager, # but condition can refer to method defined for node instance. When used # for when-dragging checks, the method will be called with two parameters # 'dragged_type' and 'dragged_host'. # 'place': there are some general places defined in UIManager. The most # important is 'edge_up': in this case touch areas are associated with # edges going up. When left empty, touch area is associated with the node. touch_areas_when_dragging = { g.LEFT_ADD_TOP: { 'condition': ['is_top_node', 'dragging_constituent', 'free_drawing_mode'] }, g.RIGHT_ADD_TOP: { 'condition': ['is_top_node', 'dragging_constituent', 'free_drawing_mode'] }, g.LEFT_ADD_SIBLING: { 'place': 'edge_up', 'condition': ['dragging_constituent', 'free_drawing_mode'] }, g.RIGHT_ADD_SIBLING: { 'place': 'edge_up', 'condition': ['dragging_constituent', 'free_drawing_mode'] }, g.TOUCH_CONNECT_COMMENT: { 'condition': 'dragging_comment' }, g.TOUCH_CONNECT_FEATURE: { 'condition': ['dragging_feature', 'free_drawing_mode'] }, g.TOUCH_CONNECT_GLOSS: { 'condition': 'dragging_gloss' } } touch_areas_when_selected = { g.LEFT_ADD_TOP: { 'condition': ['is_top_node', 'free_drawing_mode'], 'action': 'add_top_left' }, g.RIGHT_ADD_TOP: { 'condition': ['is_top_node', 'free_drawing_mode'], 'action': 'add_top_right' }, g.MERGE_TO_TOP: { 'condition': ['not:is_top_node', 'free_drawing_mode'], 'action': 'merge_to_top' }, g.INNER_ADD_SIBLING_LEFT: { 'condition': ['inner_add_sibling', 'free_drawing_mode'], 'place': 'edge_up', 'action': 'inner_add_sibling_left' }, g.INNER_ADD_SIBLING_RIGHT: { 'condition': ['inner_add_sibling', 'free_drawing_mode'], 'place': 'edge_up', 'action': 'inner_add_sibling_right' }, g.UNARY_ADD_CHILD_LEFT: { 'condition': ['has_one_child', 'free_drawing_mode'], 'action': 'unary_add_child_left' }, g.UNARY_ADD_CHILD_RIGHT: { 'condition': ['has_one_child', 'free_drawing_mode'], 'action': 'unary_add_child_right' }, g.LEAF_ADD_SIBLING_LEFT: { 'condition': ['is_leaf', 'free_drawing_mode'], 'action': 'leaf_add_sibling_left' }, g.LEAF_ADD_SIBLING_RIGHT: { 'condition': ['is_leaf', 'free_drawing_mode'], 'action': 'leaf_add_sibling_right' }, g.ADD_TRIANGLE: { 'condition': 'can_have_triangle', 'action': 'add_triangle' }, g.REMOVE_TRIANGLE: { 'condition': 'is_triangle_host', 'action': 'remove_triangle' } } buttons_when_selected = { g.REMOVE_MERGER: { 'condition': ['is_unnecessary_merger', 'free_drawing_mode'], 'action': 'remove_merger' }, g.NODE_EDITOR_BUTTON: { 'action': 'toggle_node_edit_embed' }, g.REMOVE_NODE: { 'condition': ['not:is_unnecessary_merger', 'free_drawing_mode'], 'action': 'remove_node' }, #g.QUICK_EDIT_LABEL: {}, # 'condition': 'is_quick_editing' } def __init__(self, label=''): """ Most of the initiation is inherited from Node """ Node.__init__(self) self.heads = [] # ### Projection -- see also preferences that govern if these are used self.can_project = True self.projecting_to = set() self.index = '' self.label = label self.autolabel = '' self.gloss = '' self.is_trace = False self.merge_order = 0 self.select_order = 0 self.in_projections = [] # ### Cycle index stores the order when node was originally merged to structure. # going up in trees, cycle index should go up too def after_init(self): """ After_init is called in 2nd step in process of creating objects: 1st wave creates the objects and calls __init__, and then iterates through and sets the values. 2nd wave calls after_inits for all created objects. Now they can properly refer to each other and know their values. :return: None """ self.update_gloss() self.update_label_shape() self.update_label() self.update_visibility() self.update_status_tip() self.announce_creation() if prefs.glow_effect: self.toggle_halo(True) ctrl.forest.store(self) @staticmethod def create_synobj(label, forest): """ ConstituentNodes are wrappers for Constituents. Exact implementation/class of constituent is defined in ctrl. :return: """ if not label: label = forest.get_first_free_constituent_name() c = ctrl.syntax.Constituent(label) c.after_init() return c def load_values_from_parsernode(self, parsernode): """ Update constituentnode with values from parsernode :param parsernode: :return: """ def remove_dot_label(inode, row_n): for i, part in enumerate(list(inode.parts)): if isinstance(part, str): if part.startswith('.'): inode.parts[i] = part[1:] return True elif isinstance(part, ICommandNode) and part.command == 'qroof': self.triangle_stack = [self] continue else: return remove_dot_label(part, row_n) if parsernode.index: self.index = parsernode.index rows = parsernode.label_rows # Remove dotlabel for i, row in enumerate(list(rows)): if isinstance(row, str): if row.startswith('.'): rows[i] = row[1:] break stop = remove_dot_label(row, i) if stop: break # △ self.label = join_lines(rows) # now as rows are in one INode / string, we can extract the triangle part and put it to # end. It is different to qtree's way of handling triangles, but much simpler for us in # long run. triangle_part = extract_triangle(self.label, remove_from_original=True) if triangle_part: assert isinstance(self.label, ITextNode) self.label.parts.append('\n') self.label.parts.append(triangle_part) if self.index: base = as_html(self.label) if base.strip().startswith('t<sub>'): self.is_trace = True def get_syntactic_label(self): if self.syntactic_object: return self.syntactic_object.label # Other properties @property def gloss_node(self): """ :return: """ gs = self.get_children(visible=True, of_type=g.GLOSS_EDGE) if gs: return gs[0] def has_ordered_children(self): if self.syntactic_object: return getattr(self.syntactic_object, 'is_ordered', False) return True def update_label_shape(self): self.label_object.label_shape = ctrl.settings.get('label_shape') def should_show_gloss_in_label(self) -> bool: return ctrl.settings.get('lock_glosses_to_label') == 1 def update_status_tip(self) -> None: """ Hovering status tip """ if self.label: label = f'Label: "{as_text(self.label)}" ' else: label = '' syn_label = self.get_syn_label() if syn_label: syn_label = f' Constituent: "{as_text(syn_label)}" ' else: syn_label = '' if self.index: index = f' Index: "{self.index}"' else: index = '' if self.is_trace: name = "Trace" elif self.is_leaf(): name = "Leaf " # elif self.is_top_node(): # name = "Set %s" % self.set_string() # "Root constituent" else: #name = f"Set {self.set_string()}" name = "Set " if self.use_adjustment: adjustment = f' w. adjustment ({self.adjustment[0]:.1f}, {self.adjustment[1]:.1f})' else: adjustment = '' heads = ', '.join([as_text(x.label) for x in self.heads]) self.status_tip = f"{name} ({label}{syn_label}{index} pos: ({self.current_scene_position[0]:.1f}, " \ f"{self.current_scene_position[1]:.1f}){adjustment} head: {heads})" def short_str(self): label = as_text(self.label) if label: lines = label.splitlines() if len(lines) > 3: label = f'{lines[0]} ...\n{lines[-1]}' syn_label = as_text(self.get_syn_label()) if label and syn_label: return f'{label} ({syn_label})' else: return label or syn_label or "no label" def set_string(self): """ This can be surprisingly expensive to calculate :return: """ if self.syntactic_object and hasattr(self.syntactic_object, 'set_string'): return self.syntactic_object.set_string() else: return self._set_string() def _set_string(self): parts = [] for child in self.get_children(similar=True, visible=False): parts.append(str(child._set_string())) if parts: return '{%s}' % ', '.join(parts) else: return self.label def __str__(self): label = as_text(self.label, single_line=True) syn_label = as_text(self.get_syn_label(), single_line=True) if label and syn_label: return f'CN {label} ({syn_label})' else: return f'CN {label or syn_label or "no label"}' def get_syn_label(self): if self.syntactic_object: return self.syntactic_object.label return '' def compose_html_for_viewing(self, peek_into_synobj=True): """ This method builds the html to display in label. For convenience, syntactic objects can override this (going against the containment logic) by having their own 'compose_html_for_viewing' -method. This is so that it is easier to create custom implementations for constituents without requiring custom constituentnodes. Note that synobj's compose_html_for_viewing receives the node object as parameter, so you can replicate the behavior below and add your own to it. :param peek_into_synobj: allow syntactic object to override this method. If synobj in turn needs the result from this implementation (e.g. to append something to it), you have to turn this off to avoid infinite loop. See example plugins. :return: """ # Allow custom syntactic objects to override this if peek_into_synobj and hasattr(self.syntactic_object, 'compose_html_for_viewing'): return self.syntactic_object.compose_html_for_viewing(self) html = [] label_text_mode = ctrl.settings.get('label_text_mode') l = '' if label_text_mode == g.NODE_LABELS: if self.label: l = self.label elif self.syntactic_object: l = self.syntactic_object.label elif label_text_mode == g.NODE_LABELS_FOR_LEAVES: if self.label: l = self.label elif self.syntactic_object and self.is_leaf(only_similar=True, only_visible=False): l = self.syntactic_object.label elif label_text_mode == g.SYN_LABELS: if self.syntactic_object: l = self.syntactic_object.label elif label_text_mode == g.SYN_LABELS_FOR_LEAVES: if self.syntactic_object and self.is_leaf(only_similar=True, only_visible=False): l = self.syntactic_object.label elif label_text_mode == g.SECONDARY_LABELS: if self.syntactic_object: l = self.syntactic_object.get_secondary_label() elif label_text_mode == g.XBAR_LABELS: l = self.get_autolabel() separate_triangle = bool(self.is_cosmetic_triangle() and self.triangle_stack[-1] is self) l_html = as_html(l, omit_triangle=separate_triangle, include_index=self.index) if l_html: html.append(l_html) if self.gloss and self.should_show_gloss_in_label(): if html: html.append('<br/>') html.append(as_html(self.gloss)) if html and html[-1] == '<br/>': html.pop() # Lower part lower_html = '' if separate_triangle: qroof_content = extract_triangle(l) if qroof_content: lower_html = as_html(qroof_content) return ''.join(html), lower_html def compose_html_for_editing(self): """ This is used to build the html when quickediting a label. It should reduce the label into just one field value that is allowed to be edited, in constituentnode this is either label or synobj's label. This can be overridden in syntactic object by having 'compose_html_for_editing' -method there. The method returns a tuple, (field_name, setter, html). :return: """ # Allow custom syntactic objects to override this if self.syntactic_object and hasattr(self.syntactic_object, 'compose_html_for_editing'): return self.syntactic_object.compose_html_for_editing(self) label_text_mode = ctrl.settings.get('label_text_mode') if label_text_mode == g.NODE_LABELS or label_text_mode == g.NODE_LABELS_FOR_LEAVES: if self.label: if self.triangle_stack: lower_part = extract_triangle(self.label) return 'node label', as_html(self.label, omit_triangle=True) + \ '<br/>' + as_html(lower_part or '') else: return 'node label', as_html(self.label) elif self.syntactic_object: return 'syntactic label', as_html(self.syntactic_object.label) else: return '', '', '' elif label_text_mode == g.SYN_LABELS or label_text_mode == g.SYN_LABELS_FOR_LEAVES: if self.syntactic_object: return 'syntactic label', as_html(self.syntactic_object.label) else: return '', '' def parse_edited_label(self, label_name, value): success = False if self.syntactic_object and hasattr(self.syntactic_object, 'parse_edited_label'): success = self.syntactic_object.parse_edited_label( label_name, value) if not success: if label_name == 'node label': self.poke('label') self.label = value return True elif label_name == 'syntactic label': self.syntactic_object.label = value return True elif label_name == 'index': self.index = value return False def as_bracket_string(self): """ returns a simple bracket string representation """ if self.label: children = list(self.get_children(similar=True, visible=False)) if children: return '[.%s %s ]' % \ (self.label, ' '.join((c.as_bracket_string() for c in children))) else: return str(self.label) else: inside = ' '.join( (x.as_bracket_string() for x in self.get_children(similar=True, visible=False))) if inside: return '[ ' + inside + ' ]' elif self.syntactic_object: return str(self.syntactic_object) else: return '-' def get_attribute_nodes(self, label_key=''): """ :param label_k ey: :return: """ atts = [ x.end for x in self.edges_down if x.edge_type == g.ATTRIBUTE_EDGE ] if label_key: for a in atts: if a.attribute_label == label_key: return a else: return atts def get_autolabel(self): return self.autolabel def is_unnecessary_merger(self): """ This merge can be removed, if it has only one child :return: """ return len(list(self.get_children(similar=True, visible=False))) == 1 # Conditions ########################## # These are called from templates with getattr, and may appear unused for IDE's analysis. # Check their real usage with string search before removing these. def inner_add_sibling(self): """ Node has child and it is not unary child. There are no other reasons preventing adding siblings :return: bool """ return self.get_children(similar=True, visible=False) and not self.is_unary() def has_one_child(self): return len(self.get_children(similar=True, visible=False)) == 1 def can_be_projection_of_another_node(self): """ Node can be projection from other nodes if it has other nodes below it. It may be necessary to move this check to syntactic level at some point. :return: """ if ctrl.settings.get('use_projection'): if self.is_leaf(only_similar=True, only_visible=False): return False else: return True else: return False def set_heads(self, head): """ Set projecting head to be Node, list of Nodes or empty. Notice that this doesn't affect syntactic objects. :param head: :return: """ if isinstance(head, list): self.heads = list(head) elif isinstance(head, Node): self.heads = [head] elif not head: self.heads = [] else: raise ValueError def synobj_to_node(self): """ Update node's values from its synobj. Subclasses implement this. :return: """ self.synheads_to_heads() def synheads_to_heads(self): """ Make sure that node's heads reflect synobjs heads. :return: """ self.heads = [] if self.syntactic_object: synlabel = self.syntactic_object.label parts = self.syntactic_object.parts if len(parts) == 0: self.heads = [self] if len(parts) == 1: if parts[0].label == synlabel: self.heads = [ctrl.forest.get_node(parts[0])] else: self.heads = [self] elif len(parts) == 2: if parts[0].label == synlabel: self.heads = [ctrl.forest.get_node(parts[0])] elif parts[1].label == synlabel: self.heads = [ctrl.forest.get_node(parts[1])] elif synlabel == f"({parts[0].label}, {parts[1].label})": self.heads = [ ctrl.forest.get_node(parts[0]), ctrl.forest.get_node(parts[1]) ] elif synlabel == f"({parts[1].label}, {parts[0].label})": self.heads = [ ctrl.forest.get_node(parts[1]), ctrl.forest.get_node(parts[0]) ] @property def contextual_color(self): """ Drawing color that is sensitive to node's state :return: QColor """ if ctrl.is_selected(self): base = ctrl.cm.selection() elif self.in_projections: base = ctrl.cm.get(self.in_projections[0].color_id) else: base = self.color if self.drag_data: return ctrl.cm.lighter(base) elif ctrl.pressed is self: return ctrl.cm.active(base) elif self._hovering: return ctrl.cm.hovering(base) else: return base # ### Features ######################################### def update_gloss(self, value=None): """ """ if not self.syntactic_object: return syn_gloss = self.gloss gloss_node = self.gloss_node if not ctrl.undo_disabled: if gloss_node and not syn_gloss: ctrl.free_drawing.delete_node(gloss_node) elif syn_gloss and not gloss_node: ctrl.free_drawing.create_gloss_node(host=self) elif syn_gloss and gloss_node: gloss_node.update_label() # ### Labels ############################################# # things to do with traces: # if renamed and index is removed/changed, update chains # if moved, update chains # if copied, make sure that copy isn't in chain # if deleted, update chains # any other operations? def is_empty_node(self): """ Empty nodes can be used as placeholders and deleted or replaced without structural worries """ return (not (self.syntactic_object or self.label or self.index)) and self.is_leaf() # ## Indexes and chains ################################### def is_chain_head(self): """ :return: """ if self.index: return not (self.is_leaf() and self.label == 't') return False ### UI support def dragging_constituent(self): """ Check if the currently dragged item is constituent and can connect with me :return: """ return self.is_dragging_this_type(g.CONSTITUENT_NODE) def dragging_feature(self): """ Check if the currently dragged item is feature and can connect with me :return: """ return self.is_dragging_this_type(g.FEATURE_NODE) def dragging_gloss(self): """ Check if the currently dragged item is gloss and can connect with me :return: """ return self.is_dragging_this_type(g.GLOSS_NODE) def dragging_comment(self): """ Check if the currently dragged item is comment and can connect with me :return: """ return self.is_dragging_this_type(g.COMMENT_NODE) # ### Features ######################################### def get_features(self): """ Returns FeatureNodes """ return self.get_children(visible=True, of_type=g.FEATURE_EDGE) def get_features_as_string(self): """ :return: """ features = [f.syntactic_object for f in self.get_features()] feature_strings = [str(f) for f in features] return ', '.join(feature_strings) # ### Checks for callable actions #### def can_top_merge(self): """ :return: """ top = self.get_top_node() return self is not top and self not in top.get_children(similar=True, visible=False) # ### Dragging ##################################################################### # ## Most of this is implemented in Node def prepare_children_for_dragging(self, scene_pos): """ Implement this if structure is supposed to drag with the node :return: """ children = ctrl.forest.list_nodes_once(self) for tree in self.trees: dragged_index = tree.sorted_constituents.index(self) for i, node in enumerate(tree.sorted_constituents): if node is not self and i > dragged_index and node in children: node.start_dragging_tracking(host=False, scene_pos=scene_pos) ################################# # ### Parents & Children #################################################### def is_projecting_to(self, other): """ :param other: """ pass # # def paint(self, painter, option, widget=None): # """ Painting is sensitive to mouse/selection issues, but usually with # :param painter: # :param option: # :param widget: # nodes it is the label of the node that needs complex painting """ # super().paint(painter, option, widget=widget) # ############## # # # # Save support # # # # ############## # label = SavedField("label") index = SavedField("index") gloss = SavedField("gloss", if_changed=update_gloss) heads = SavedField("heads")
class JointedTouchArea(TouchArea): """ TouchArea that connects to nodes and has ^-shape. Used to add nodes to top of the trees. """ __qt_type_id__ = next_available_type_id() def boundingRect(self): """ :return: """ if not self.end_point: self.update_end_points() assert self.end_point # Bounding rect that includes the tail and end spot ellipse rel_sp = sub_xy(self.start_point, self.end_point) sx, sy = rel_sp ex, ey = 0, 0 e2 = end_spot_size * 2 if sx < ex: w = max((ex - sx + end_spot_size, e2)) x = min((sx, ex - end_spot_size)) else: w = max((sx - ex + end_spot_size, e2)) x = ex - end_spot_size if sy < ey: h = max((ey - sy + end_spot_size, e2)) y = min((sy, ey - end_spot_size)) else: h = max((sy - ey + end_spot_size, e2)) y = ey - end_spot_size r = QtCore.QRectF(x, y, w, h) return r.united(self._path.controlPointRect()) def shape(self): """ Shape is used for collisions and it shouldn't go over the originating node. So use only the last half, starting from the "knee" of the shape. :return: """ path = QtGui.QPainterPath() # Bounding rect that includes the tail and end spot ellipse rel_sp = sub_xy(self.start_point, self.end_point) sx, sy = rel_sp sx /= 2.0 ex, ey = 0, 0 e2 = end_spot_size * 2 if sx < ex: w = max((ex - sx + end_spot_size, e2)) x = min((sx, ex - end_spot_size)) else: w = max((sx - ex + end_spot_size, e2)) x = ex - end_spot_size if sy < ey: h = max((ey - sy + end_spot_size, e2)) y = min((sy, ey - end_spot_size)) else: h = max((sy - ey + end_spot_size, e2)) y = ey - end_spot_size r = QtCore.QRectF(x, y, w, h) path.addRect(r) return path def drag(self, event): self._dragging = True self.update_end_points(end_point=to_tuple(event.scenePos())) def update_end_points(self, end_point=None): """ :param end_point: End point can be given or it can be calculated. """ shape_name = ctrl.settings.get_edge_setting( 'shape_name', edge_type=g.CONSTITUENT_EDGE) self._fill_path = ctrl.settings.get_shape_setting( 'fill', edge_type=g.CONSTITUENT_EDGE) sx, sy = self.host.magnet(2) self.start_point = sx, sy hw_ratio = float(prefs.edge_height - (ConstituentNode.height / 2)) / (prefs.edge_width or 1) if not end_point: good_width = max((prefs.edge_width * 2, self.host.width / 2 + ConstituentNode.width)) if self._align_left: self.end_point = sx - good_width, sy else: self.end_point = sx + good_width, sy self.setPos(self.end_point[0], self.end_point[1]) rel_sp = sub_xy(self.start_point, self.end_point) sx, sy = rel_sp ex, ey = 0, 0 line_middle_point = sx / 2.0, sy - hw_ratio * abs(sx) adjust = [] if self._align_left: self._path = SHAPE_PRESETS[shape_name].path( line_middle_point, (sx, sy), alignment=g.RIGHT, curve_adjustment=adjust)[0] self._path.moveTo(sx, sy) path2 = SHAPE_PRESETS[shape_name].path(line_middle_point, (ex, ey), alignment=g.LEFT, curve_adjustment=adjust)[0] else: self._path = SHAPE_PRESETS[shape_name].path( line_middle_point, (ex, ey), alignment=g.RIGHT, curve_adjustment=adjust)[0] self._path.moveTo(ex, ey) path2 = SHAPE_PRESETS[shape_name].path(line_middle_point, (sx, sy), alignment=g.LEFT, curve_adjustment=adjust)[0] self._path |= path2 def drop(self, dropped_node): """ Connect dropped node to host of this TouchArea. Connection depends on which merge area this is: top left, top right, left, right :param dropped_node: """ if isinstance(dropped_node, str): dropped_node = self.make_node_from_string(dropped_node) if not dropped_node: return # host is a node assert isinstance(self.host, Node) ctrl.free_drawing.merge_to_top(self.host, dropped_node, merge_to_left=self._align_left, pos=self.start_point) for node in ctrl.dragged_set: node.adjustment = self.host.adjustment ctrl.forest.forest_edited() return 'moved node %s to sibling of %s' % (dropped_node, self.host)
class RightAddInnerSibling(RightAddSibling): __qt_type_id__ = next_available_type_id()
class CommentNode(Node): """ Node to display comments, annotations etc. syntactically inert information """ __qt_type_id__ = next_available_type_id() width = 20 height = 20 node_type = g.COMMENT_NODE display_name = ('Comment', 'Comments') display = True is_syntactic = False can_be_in_groups = False editable = { 'text': dict(name='', prefill='comment', tooltip='freeform text, invisible for processing', input_type='expandingtext') } default_style = { 'fancy': { 'color_id': 'accent4', 'font_id': g.MAIN_FONT, 'font-size': 14 }, 'plain': { 'color_id': 'accent4', 'font_id': g.MAIN_FONT, 'font-size': 14 } } default_edge = g.COMMENT_EDGE touch_areas_when_dragging = { g.DELETE_ARROW: { 'condition': 'dragging_my_arrow' }, g.TOUCH_CONNECT_COMMENT: { 'condition': 'dragging_comment' }, } touch_areas_when_selected = { g.DELETE_ARROW: { 'condition': 'has_arrow', 'action': 'delete_arrow' }, g.ADD_ARROW: { 'action': 'start_arrow_from_node' } } def __init__(self, label='comment'): self.image_object = None Node.__init__(self) if not label: label = 'comment' self.resizable = True self.label = label self.physics_x = False self.physics_y = False self.image_path = None self.image = None self.pos_relative_to_host = -50, -50 self.preferred_host = None def after_init(self): Node.after_init(self) if self.user_size: w, h = self.user_size self.set_user_size(w, h) @property def hosts(self): """ A comment can be associated with nodes. The association uses the general connect/disconnect mechanism, but 'hosts' is a shortcut to get the nodes. :return: list of Nodes """ return self.get_parents(visible=False, of_type=g.COMMENT_EDGE) @property def text(self): """ The text of the comment. Uses the generic node.label as storage. :return: str or ITextNode """ return self.label @text.setter def text(self, value): """ The text of the comment. Uses the generic node.label as storage. :param value: str or ITextNode """ self.label = value def has_arrow(self): return bool(self.edges_down) def set_image_path(self, pixmap_path): if pixmap_path and (self.image_path != pixmap_path or not self.image): self.image_path = pixmap_path self.image = QtGui.QPixmap() success = self.image.load(pixmap_path) if success: if self.image_object: print('removing old image object') self.image_object.hide() self.image_object.setParentItem(None) self.image_object = QtWidgets.QGraphicsPixmapItem( self.image, self) self.image_object.setPos(self.image.width() / -2, self.image.height() / -2) self.text = '' self.update_label_visibility() self.update_bounding_rect() else: self.image = None if self.image_object: print('removing old image object 2') self.image_object.hide() self.image_object.setParentItem(None) self.image_object = None def set_user_size(self, width, height): if width < 1 or height < 1: return self.user_size = (width, height) if self.image_object: scaled = self.image.scaled(width, height, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) self.image_object.prepareGeometryChange() self.image_object.setPixmap(scaled) self.image_object.setPos(-scaled.width() / 2, -scaled.height() / 2) # Update ui items around the label (or node hosting the label) ctrl.ui.update_position_for(self) elif self.label_object: self.label_object.resize_label() def dragging_my_arrow(self): return True def __str__(self): return 'comment: %s' % self.text def update_bounding_rect(self): """ :return: """ if self.image_object: my_class = self.__class__ if self.user_size is None: user_width, user_height = 0, 0 else: user_width, user_height = self.user_size lbr = self.image_object.boundingRect() lbw = lbr.width() lbh = lbr.height() lbx = self.image_object.x() lby = self.image_object.y() self.label_rect = QtCore.QRectF(lbx, lby, lbw, lbh) self.width = max((lbw, my_class.width, user_width)) self.height = max((lbh, my_class.height, user_height)) y = self.height / -2 x = self.width / -2 self.inner_rect = QtCore.QRectF(x, y, self.width, self.height) w4 = (self.width - 2) / 4.0 w2 = (self.width - 2) / 2.0 h2 = (self.height - 2) / 2.0 self._magnets = [(-w2, -h2), (-w4, -h2), (0, -h2), (w4, -h2), (w2, -h2), (-w2, 0), (w2, 0), (-w2, h2), (-w4, h2), (0, h2), (w4, h2), (w2, h2)] if ctrl.ui.selection_group and self in ctrl.ui.selection_group.selection: ctrl.ui.selection_group.update_shape() return self.inner_rect else: return super().update_bounding_rect() def paint(self, painter, option, widget=None): """ Painting is sensitive to mouse/selection issues, but usually with :param painter: :param option: :param widget: nodes it is the label of the node that needs complex painting """ if self.drag_data: p = QtGui.QPen(self.contextual_color) #b = QtGui.QBrush(ctrl.cm.paper()) #p.setColor(ctrl.cm.hover()) p.setWidth(1) painter.setPen(p) #painter.setBrush(self.drag_data.background) painter.drawRect(self.inner_rect) painter.setBrush(QtCore.Qt.NoBrush) elif self._hovering: p = QtGui.QPen(self.contextual_color) #p.setColor(ctrl.cm.hover()) p.setWidth(1) painter.setPen(p) painter.drawRect(self.inner_rect) elif ctrl.pressed is self or ctrl.is_selected(self): p = QtGui.QPen(self.contextual_color) p.setWidth(1) painter.setPen(p) painter.drawRect(self.inner_rect) elif self.has_empty_label() and self.node_alone(): p = QtGui.QPen(self.contextual_color) p.setStyle(QtCore.Qt.DotLine) p.setWidth(1) painter.setPen(p) painter.drawRect(self.inner_rect) def move(self, md): """ Override usual movement if comment is connected to some node. If so, try to keep the given position relative to that node. :return: """ if self.preferred_host: x, y = self.preferred_host.current_scene_position dx, dy = self.pos_relative_to_host self.current_position = self.scene_position_to_tree_position( (x + dx, y + dy)) return False, False else: return super().move(md) def drop_to(self, x, y, recipient=None): """ :param recipient: :param x: :param y: :return: action finished -message (str) """ message = super().drop_to(x, y, recipient=recipient) if self.preferred_host: x, y = self.preferred_host.current_scene_position mx, my = self.current_scene_position self.pos_relative_to_host = mx - x, my - y message = "Adjusted comment to be at %s, %s relative to '%s'" % ( mx - x, my - y, self.preferred_host) return message def on_connect(self, other): print('on_connect called, hosts:', self.hosts) if other in self.hosts: self.preferred_host = other x, y = other.current_scene_position mx, my = self.current_scene_position self.pos_relative_to_host = mx - x, my - y def on_disconnect(self, other): print('on_disconnect called') if other is self.preferred_host: for item in self.hosts: if item != other: self.preferred_host = item x, y = item.current_scene_position mx, my = self.current_scene_position self.pos_relative_to_host = mx - x, my - y return self.preferred_host = None def can_connect_with(self, other): return other not in self.hosts def dragging_comment(self): """ Check if the currently dragged item is comment and can connect with me :return: """ return self.is_dragging_this_type(g.COMMENT_NODE) # ############## # # # # Save support # # # # ############## # image_path = SavedField("image_path", if_changed=set_image_path)
class LeftAddInnerSibling(LeftAddSibling): __qt_type_id__ = next_available_type_id()
class LeftAddLeafSibling(LeftAddChild): __qt_type_id__ = next_available_type_id()
class TouchArea(UIGraphicsItem, QtWidgets.QGraphicsObject): """ Mouse sensitive areas connected to either nodes or edges between them. """ __qt_type_id__ = next_available_type_id() clicked = QtCore.pyqtSignal() def __init__(self, host, action): """ :param ConstituentNode host: :param string action: """ UIGraphicsItem.__init__(self, host=host) QtWidgets.QGraphicsObject.__init__(self) self._dragging = False self._path = None self.start_point = None self.end_point = None self.z_value = 160 self.setZValue(self.z_value) self.status_tip = "" # Drawing flags defaults self._fill_path = False self._align_left = False self._below_node = False self.focusable = True self._visible = True self._hovering = False self._drag_hint = False self.setAcceptHoverEvents(True) self.setAcceptDrops(True) self.update_end_points() self.action = action self.setFlag(QtWidgets.QGraphicsObject.ItemIsSelectable) self.setCursor(QtCore.Qt.PointingHandCursor) if action: action.connect_element(self) if action and action.tip: self.set_tip(action.tip_with_shortcut()) def type(self): """ Qt's type identifier, custom QGraphicsItems should have different type ids if events need to differentiate between them. These are set when the program starts. :return: """ return self.__qt_type_id__ def is_visible(self): """ :return: """ return self._visible def set_tip(self, tip): self.status_tip = tip if ctrl.main.use_tooltips: self.setToolTip(self.status_tip) def contextual_color(self): if self._hovering: return ctrl.cm.hovering(ctrl.cm.ui()) else: return ctrl.cm.ui() #return ctrl.cm.ui_tr() def boundingRect(self): """ :return: """ if not self.end_point: self.update_end_points() assert self.end_point # Just the bounding rect of end spot ellipse return QtCore.QRectF(-end_spot_size, -end_spot_size, end_spot_size + end_spot_size, end_spot_size + end_spot_size) def sensitive_area(self): """ :return: """ return self.boundingRect() def update_position(self): """ """ pass # if not self in ctrl.dragged: self.update_end_points() def drag(self, event): self._dragging = True ep = to_tuple(event.scenePos()) self.end_point = ep self.start_point = ep self.setPos(ep[0], ep[1]) self._path = None def update_end_points(self): """ """ if not self.host: return if isinstance(self.host, Edge): x, y = self.host.end_point self.end_point = x, y else: x, y = self.host.current_scene_position self.end_point = x, y self.start_point = self.end_point self.setPos(self.end_point[0], self.end_point[1]) self._path = None def __repr__(self): return '<toucharea %s>' % self.ui_type def remove(self): """ remove item from the scene but otherwise keep it intact """ sc = self.scene() if sc: sc.removeItem(self) def make_node_from_string(self, string): """ Try to create a node from given string :param string: str :return: """ command_identifier, *args = string.split(':') if command_identifier == 'kataja' and args: command, *args = args if command == "new_node": node_type = args[0] try: node_type = int(node_type) except TypeError: pass if hasattr(self.host, 'current_position'): x, y = self.host.current_scene_position elif hasattr(self.host, 'start_point'): x, y = self.host.start_point else: return if hasattr(self.host, 'height'): h = self.host.height else: h = 0 pos = QtCore.QPointF(x, y + h) return ctrl.free_drawing.create_node(pos=pos, node_type=node_type) else: print('received unknown command:', command, args) else: print('received just some string: ', string) def mousePressEvent(self, event): ctrl.press(self) super().mousePressEvent(event) def mouseMoveEvent(self, event): if ctrl.pressed is self: if ctrl.dragged_set or ( event.buttonDownScenePos(QtCore.Qt.LeftButton) - event.scenePos()).manhattanLength() > 6: self.drag(event) ctrl.graph_scene.dragging_over(event.scenePos()) def mouseReleaseEvent(self, event): if ctrl.pressed is self: ctrl.release(self) if ctrl.dragged_set: self._dragging = False ctrl.graph_scene.kill_dragging() ctrl.ui.update_selections( ) # drag operation may have changed visible affordances ctrl.main.action_finished() # @UndefinedVariable else: # This is regular click on 'pressed' object self.click(event) self.update() return None # this mouseRelease is now consumed super().mouseReleaseEvent(event) def click(self, event=None): """ :type event: QMouseEvent """ self._dragging = False if self._drag_hint: return False ctrl.deselect_objects() self.clicked.emit() return True # self, N, R, merge_to_left, new_node_pos, merger_node_pos): def calculate_if_can_merge(self, dragged, top, node_list): """ :param dragged: :param top: :param node_list: :return: """ host = self.host if host is ctrl.dragged_focus: return False elif host in ctrl.dragged_set: return False elif host is ctrl.pressed: return False return True def dragged_over_by(self, dragged): """ :param dragged: """ if ctrl.drag_hovering_on is self: self.hovering = True return True elif self.accepts_drops(dragged): ctrl.set_drag_hovering(self) return True else: return False def accepts_drops(self, dragged): """ :param dragged: :return: """ return self.calculate_if_can_merge(dragged, None, None) @property def hovering(self): """ :return: """ return self._hovering @hovering.setter def hovering(self, value): """ :param value: """ if value and not self._hovering: self._hovering = True ctrl.set_status(self.status_tip) self.setZValue(1000) elif (not value) and self._hovering: self._hovering = False ctrl.remove_status(self.status_tip) self.setZValue(self.z_value) self.update() def hoverEnterEvent(self, event): """ :param event: """ if (not self._hovering) and not ctrl.pressed: self.hovering = True QtWidgets.QGraphicsObject.hoverEnterEvent(self, event) def hoverLeaveEvent(self, event): """ :param event: """ if self._hovering: self.hovering = False QtWidgets.QGraphicsObject.hoverLeaveEvent(self, event) def dragEnterEvent(self, event): self.dragged_over_by(event.mimeData().text()) event.accept() QtWidgets.QGraphicsObject.dragEnterEvent(self, event) def dragLeaveEvent(self, event): self.hovering = False event.accept() QtWidgets.QGraphicsObject.dragLeaveEvent(self, event) def dropEvent(self, event): self.hovering = False event.accept() message = self.drop(event.mimeData().text()) QtWidgets.QGraphicsObject.dropEvent(self, event) ctrl.main.action_finished(message)
class MergeToTop(BranchingTouchArea): """ TouchArea that connects to nodes and has \-shape. """ __qt_type_id__ = next_available_type_id() def __init__(self, host, action): super().__init__(host, action) self._align_left = True def update_end_points(self, end_point=None): """ :param end_point: End point can be given or it can be calculated. """ sx, sy = self.host.magnet(0) self.start_point = sx, sy if end_point: self.end_point = end_point else: ex = sx - 20 # 75 ey = sy - 10 self.end_point = ex, ey self.setPos(self.end_point[0], self.end_point[1]) def paint(self, painter, option, widget): """ :param painter: :param option: :param widget: :raise: """ if ctrl.pressed is self: pass c = self.contextual_color() painter.setPen(c) dx = self.start_point[0] - self.end_point[0] dy = self.start_point[1] - self.end_point[1] l = QtCore.QLineF(dx, dy, 0, 0) if self._hovering: if len(self.host.trees) != 1: painter.drawLine(l) painter.save() painter.setBrush(ctrl.cm.ui()) painter.rotate(20) draw_leaf(painter, 0, end_spot_size / 2, end_spot_size) painter.restore() draw_plus(painter, 4, 0) return else: top = list(self.host.trees)[0].top lmx, lmy = top.magnet(5) scene_point = QtCore.QPointF(lmx, lmy) end_point = self.mapFromScene(scene_point) path = QtGui.QPainterPath(QtCore.QPointF(dx, dy)) path.quadTo(QtCore.QPointF(end_point.x() - 200, end_point.y()), end_point) painter.drawPath(path) l = QtCore.QLineF( QtCore.QPointF(end_point.x() - 200, end_point.y()), end_point) else: painter.drawLine(l) l2x = l.p2().x() l2 = l.p2() l2y = l.p2().y() head_size = 8.0 back = head_size / -2 # Draw the arrows if there's enough room. ll = l.length() if ll >= 1 and ll + back > 0: angle = math.acos(l.dx() / ll) # acos has to be <= 1.0 else: return prop = back / ll if l.dy() >= 0: angle = (math.pi * 2.0) - angle destArrowP1 = QtCore.QPointF( (math.sin(angle - math.pi / 3) * head_size) + l2x, (math.cos(angle - math.pi / 3) * head_size) + l2y) destArrowP2 = QtCore.QPointF( (math.sin(angle - math.pi + math.pi / 3) * head_size) + l2x, (math.cos(angle - math.pi + math.pi / 3) * head_size) + l2y) l2c = QtCore.QPointF(l.dx() * prop + l2x, l.dy() * prop + l2y) painter.setBrush(c) painter.drawPolygon( QtGui.QPolygonF([l2, destArrowP1, l2c, destArrowP2])) def drop(self, dropped_node): """ Connect dropped node to host of this TouchArea. Connection depends on which merge area this is: top left, top right, left, right :param dropped_node: """ pass def accepts_drops(self, dragged): return False
class Tree(Movable): """ Container for nodes that form a single trees. It allows operations that affect all nodes in one trees, e.g. translation of position. :param top: """ __qt_type_id__ = next_available_type_id() display_name = ('Tree', 'Trees') def __init__(self, top=None, numeration=False): Movable.__init__(self) self.top = top if is_constituent(top): self.sorted_constituents = [top] else: self.sorted_constituents = [] if top: self.sorted_nodes = [top] else: self.sorted_nodes = [] self.numeration = numeration self.current_position = 100, 100 self.drag_data = None self.tree_changed = True self._cached_bounding_rect = None self.setZValue(100) def __repr__(self): if self.numeration: suffix = " (numeration)" else: suffix = '' return "Tree '%s' and %s nodes.%s" % (self.top, len( self.sorted_nodes), suffix) def __contains__(self, item): return item in self.sorted_nodes def after_init(self): self.recalculate_top() self.update_items() self.announce_creation() def after_model_update(self, updated_fields, transition_type): """ Compute derived effects of updated values in sensible order. :param updated_fields: field keys of updates :param transition_type: 0:edit, 1:CREATED, -1:DELETED :return: None """ super().after_model_update(updated_fields, transition_type) if transition_type != g.DELETED: self.update_items() def rebuild(self): self.recalculate_top() self.update_items() def recalculate_top(self): """ Verify that self.top is the topmost element of the trees. Doesn't handle consequences, e.g. it may now be that there are two identical trees at the top and doesn't update the constituent and node lists. :return: new top """ passed = set() def walk_to_top(node): """ Recursive walk upwards :param node: :return: """ passed.add(node) for parent in node.get_parents(similar=False, visible=False): if parent not in passed: return walk_to_top(parent) return node if self.top: # hopefully it is a short walk self.top = walk_to_top(self.top) elif self.sorted_nodes: # take the long way if something strange has happened to top self.top = walk_to_top(self.sorted_nodes[-1]) else: self.top = None # hopefully this trees gets deleted. return self.top def is_empty(self): """ Empty trees should be deleted when found :return: """ return bool(self.top) def is_valid(self): """ If trees top has parents, the trees needs to be recalculated or it is otherwise unusable before fixed. :return: """ return not self.top.get_parents(similar=False, visible=False) def add_node(self, node): """ Add this node to given trees and possibly set it as parent for this graphicsitem. :param node: Node :return: """ node.poke('trees') node.trees.add(self) node.update_graphics_parent() def remove_node(self, node, recursive_down=False): """ Remove node from trees and remove the (graphicsitem) parenthood-relation. :param node: Node :param recursive_down: bool -- do recursively remove child nodes from tree too :return: """ if self in node.trees: node.poke('trees') node.trees.remove(self) node.update_graphics_parent() if recursive_down: for child in node.get_children(similar=False, visible=False): legit = False for parent in child.get_parents(similar=False, visible=False): if self in parent.trees: legit = True if not legit: self.remove_node(child, recursive_down=True) def add_to_numeration(self, node): def add_children(node): if node not in self.sorted_nodes: self.sorted_nodes.append(node) if self not in node.trees: self.add_node(node) for child in node.get_children(similar=False, visible=False): if child: # undoing object creation may cause missing edge ends add_children(child) add_children(node) @time_me def update_items(self): """ Check that all children of top item are included in this trees and create the sorted lists of items. Make sure there is a top item before calling this! :return: """ if self.numeration: to_be_removed = set() for item in self.sorted_nodes: for tree in item.trees: if tree is not self: to_be_removed.add(item) break for item in to_be_removed: self.remove_node(item) return sorted_constituents = [] sorted_nodes = [] used = set() def add_children(node): """ Add node to this trees. :param node: :return: """ if node not in used: used.add(node) if is_constituent(node): sorted_constituents.append(node) sorted_nodes.append(node) if self not in node.trees: self.add_node(node) for child in node.get_children(similar=False, visible=False): if child: # undoing object creation may cause missing edge ends add_children(child) old_nodes = set(self.sorted_nodes) if is_constituent(self.top): add_children(self.top) self.sorted_constituents = sorted_constituents for i, item in enumerate(self.sorted_constituents): item.z_value = 10 + i item.setZValue(item.z_value) self.sorted_nodes = sorted_nodes to_be_removed = old_nodes - set(sorted_nodes) for item in to_be_removed: self.remove_node(item) def is_higher_in_tree(self, node_a, node_b): """ Compare two nodes, if node_a is higher, return True. Return False if not. Return None if nodes are not in the same trees -- cannot compare. (Be careful with the result, handle None and False differently.) :param node_a: :param node_b: :return: """ if node_a in self and node_b in self: return self.sorted_nodes.index(node_a) < self.sorted_nodes.index( node_b) else: return None def start_dragging_tracking(self, host=False, scene_pos=None): """ Add this *Tree* to entourage of dragged nodes. These nodes will maintain their relative position to drag pointer while dragging. :return: None """ self.drag_data = TreeDragData(self, is_host=host, mousedown_scene_pos=scene_pos) def boundingRect(self): if self.tree_changed or not self._cached_bounding_rect: if not self.sorted_nodes: return QtCore.QRectF() min_x, min_y = 10000, 10000 max_x, max_y = -10000, -10000 for node in self.sorted_nodes: if node.is_visible() and not node.locked_to_node: nbr = node.future_children_bounding_rect() if node.physics_x or node.physics_y: x, y = node.x(), node.y() else: x, y = node.target_position x1, y1, x2, y2 = nbr.getCoords() x1 += x y1 += y x2 += x y2 += y if x1 < min_x: min_x = x1 if x2 > max_x: max_x = x2 if y1 < min_y: min_y = y1 if y2 > max_y: max_y = y2 self._cached_bounding_rect = QtCore.QRectF(min_x, min_y, max_x - min_x, max_y - min_y) self.tree_changed = False return self._cached_bounding_rect else: return self._cached_bounding_rect def current_scene_bounding_rect(self): if not self.sorted_nodes: return QtCore.QRectF() min_x, min_y = 10000, 10000 max_x, max_y = -10000, -10000 for node in self.sorted_nodes: if node.is_visible(): nbr = node.sceneBoundingRect() x1, y1, x2, y2 = nbr.getCoords() if x1 < min_x: min_x = x1 if x2 > max_x: max_x = x2 if y1 < min_y: min_y = y1 if y2 > max_y: max_y = y2 return QtCore.QRectF(min_x, min_y, max_x - min_x, max_y - min_y) # def normalize_positions(self): # print('tree normalising positions') # tx, ty = self.top.target_position # for node in self.sorted_constituents: # nx, ny = node.target_position # node.move_to(nx - tx, ny - ty) def paint(self, painter, QStyleOptionGraphicsItem, QWidget_widget=None): if self.numeration: # or True: br = self.boundingRect() painter.drawRect(br) #painter.drawText(br.topLeft() + QtCore.QPointF(2, 10), str(self)) top = SavedField("top")
class RightAddUnaryChild(RightAddChild): __qt_type_id__ = next_available_type_id()
class LeftAddUnaryChild(LeftAddChild): __qt_type_id__ = next_available_type_id()
class UIEmbed(UIWidget, QtWidgets.QWidget): """ UIEmbeds are UI elements that are drawn on the main graphics view: they are contextual panels that need more UI-capabilities like focus, selection and text editing than would be practical to do with GraphicsItems. The benefits of UIEmbeds are that these do not scale with graphicsitems, and these are styled automatically from palette, as long as they get updated properly. These are also not counted in GraphicsScene, so these won't cause additional code there. UIEmbed implements the basic functions of all embeds: showing them, updating their positions, close buttons, updating colors. The approach is similar to UIPanels. :param parent: :param ui_manager: """ __qt_type_id__ = next_available_type_id() unique = True def __init__(self, parent, host, text): UIWidget.__init__(self, host) QtWidgets.QWidget.__init__(self, parent) self._palette = None self.update_colors() self._drag_diff = None self.moved_by_hand = False self.top_row_layout = QtWidgets.QHBoxLayout() #close_button = QtWidgets.QPushButton("x") close_button = PanelButton(pixmap=qt_prefs.close_icon, tooltip='Close', parent=self, size=12, color_key='content1') close_button.setMaximumWidth(16) self.ui_manager.connect_element_to_action(close_button, 'close_embed') self.top_row_layout.addWidget(close_button) self.top_row_layout.setAlignment(QtCore.Qt.AlignLeft) self.top_row_layout.addSpacing(8) self.top_title = QtWidgets.QLabel(text) self.top_row_layout.addWidget(self.top_title) self.assumed_width = 300 self.assumed_height = 100 self._magnet = QtCore.QPoint(0, 0), 1 # Effect will be disabled if QTextEdit is used. self.setAutoFillBackground(True) self.setBackgroundRole(QtGui.QPalette.Window) self.hide() # Remember to add top_row_layout to your layout def type(self): """ Qt's type identifier, custom QGraphicsItems should have different type ids if events need to differentiate between them. These are set when the program starts. :return: """ return self.__qt_type_id__ @property def graphic_item(self): """ if this _Widget_ has UI graphic items associated in scene, e.g. target reticles or so. :return: """ return None def update_embed(self, focus_point=None): self.update_colors() self.update_fields() if focus_point: self.update_position(focus_point=focus_point) def update_size(self): self.setFixedSize(self.layout().minimumSize()) def update_fields(self): """ Subclasses implement this if there are elements to update :return: """ pass def margin_x(self): """ Margin around embed edit provides some empty space so that the focus point of embed is not occluded by the embed. e.g. if focus point=(0,0), with margins=(6,6) embed's top left corner would be at (6, 6) and point (0, 0) would still be visible. :return: """ return 6 def margin_y(self): """ Margin around embed edit provides some empty space so that the focus point of embed is not occluded by the embed. e.g. if focus point=(0,0), with margins=(6,6) embed's top left corner would be at (6, 6) and point (0, 0) would still be visible. :return: """ return 6 def update_position(self, focus_point=None): """ Position embedded editor to graphics view. The idea is that the embed edit shouldn't occlude the focus point (object being edited or the point on screen where new object will be created, but it should try to fit as whole into the graphics view. So this method computes which corner or edge of editor should be placed closest to focus point and moves the embed accordingly. :param focus_point: :return: """ if self.moved_by_hand: return if not focus_point: if self.host: focus_point = self.host.scenePos() else: return self.update_size() ew = prefs.edge_width eh = prefs.edge_height view = ctrl.graph_view my_rect = self.geometry() w = my_rect.width() h = my_rect.height() view_rect = view.geometry() if self.host: scene_br = self.host.sceneBoundingRect() scene_br.adjust(-ew, -eh, ew, eh) elif focus_point: scene_br = QtCore.QRectF(-ew, -eh, ew * 2, eh * 2) scene_br.moveCenter(focus_point.toPoint()) node_rect = view.mapFromScene(scene_br).boundingRect() # do nothing if we already have a good enough position. For user it is better if the # panel stays in place than if it jumps around. if view_rect.contains( my_rect, proper=True) and not node_rect.intersects(my_rect): return ncy = node_rect.center().y() if node_rect.right() + w < view_rect.right(): if node_rect.right() + w + 50 < view_rect.right(): x = node_rect.right() + 50 else: x = view_rect.right() - w else: if node_rect.left() - w - 50 > 4: x = node_rect.left() - w - 50 else: x = 4 if ncy - h / 2 > 25 and \ ncy + h / 2 < view_rect.height(): y = ncy - h / 2 else: y = h / 2 - node_rect.height() / 2 self.move(x, y) self.updateGeometry() def magnet(self): return self._magnet def update_colors(self): key = None if self.host and hasattr(self.host, 'get_color_id'): key = self.host.get_color_id() if key: self._palette = ctrl.cm.get_accent_palette(key) self.setPalette(self._palette) def wake_up(self): self.fade_in() self.raise_() self.focus_to_main() def focus_to_main(self): pass def mousePressEvent(self, event): self._drag_diff = event.pos() def mouseMoveEvent(self, event): self.move(self.mapToParent(event.pos()) - self._drag_diff) self.moved_by_hand = True QtWidgets.QWidget.mouseMoveEvent(self, event) def resizeEvent(self, event): QtWidgets.QWidget.resizeEvent(self, event) def event(self, e): if e.type() == QtCore.QEvent.PaletteChange: self.update_colors() return QtWidgets.QWidget.event(self, e)
class ControlPoint(UIGraphicsItem, QtWidgets.QGraphicsItem): """ """ __qt_type_id__ = next_available_type_id() def __init__(self, edge, index=-1, role=''): UIGraphicsItem.__init__(self, host=edge, role=role) QtWidgets.QGraphicsItem.__init__(self) print('creating control_point, role is: ', role) if prefs.touch: self._wh = 12 self._xy = -6 self.round = True self.setCursor(Qt.PointingHandCursor) else: self._wh = 4 self._xy = -2 self.round = True self.setCursor(Qt.CrossCursor) self._index = index self.focusable = True self.pressed = False self._hovering = False self.being_dragged = False self.setAcceptHoverEvents(True) self.setFlag(QtWidgets.QGraphicsObject.ItemIsMovable) self.setFlag(QtWidgets.QGraphicsObject.ItemIsSelectable) self.setZValue(52) self._compute_position() self.status_tip = "" if self.role == g.START_POINT: self.status_tip = "Drag to move the starting point" elif self.role == g.END_POINT: self.status_tip = "Drag to move the ending point" elif self.role == g.CURVE_ADJUSTMENT: self.status_tip = "Drag to adjust the curvature of this line" elif self.role == g.LABEL_START: self.status_tip = "Drag along the line to adjust the anchor point of label" if prefs.touch: self._wh = 6 self._xy = -3 if ctrl.main.use_tooltips: self.setToolTip(self.status_tip) self.show() def type(self): """ Qt's type identifier, custom QGraphicsItems should have different type ids if events need to differentiate between them. These are set when the program starts. :return: """ return self.__qt_type_id__ def show(self): """ Assign as a watcher if necessary and make visible :return: None """ if self.role == g.LABEL_START: ctrl.add_watcher(self, 'edge_label') def hide(self): """ Remove from watchers' list when control point is hidden :return: None """ if self.role == g.LABEL_START: ctrl.remove_from_watch(self) def _compute_position(self): """ :return: """ if self.role == g.CURVE_ADJUSTMENT and self._index < len( self.host.adjusted_control_points): p = Pf(*self.host.adjusted_control_points[self._index]) elif self.role == g.START_POINT: p = Pf(self.host.start_point[0], self.host.start_point[1]) elif self.role == g.END_POINT: p = Pf(self.host.end_point[0], self.host.end_point[1]) elif self.role == g.LABEL_START and self.host.label_item: c = self.host.label_item.get_label_start_pos() p = Pf(c.x(), c.y()) else: return False self.setPos(p) return True def boundingRect(self): """ :return: """ return QtCore.QRectF(self._xy, self._xy, self._wh, self._wh) def update_position(self): """ """ ok = self._compute_position() self.update() return ok def _compute_adjust(self): x, y = to_tuple(self.pos()) assert (self._index != -1) p = self.host.control_points[self._index] return int(x - p[0]), int(y - p[1]) # print 'computed curve_adjustment:', self.curve_adjustment def _compute_adjust_from_pos(self, scene_pos): x, y = to_tuple(scene_pos) assert (self._index != -1) cx, cy = self.host.control_points[self._index] x_adjust = int(x - cx) y_adjust = int(y - cy) if self._index == 0: sx, sy = self.host.start_point else: sx, sy = self.host.end_point sx_to_cx = cx - sx sy_to_cy = cy - sy line_rad = math.atan2(sy_to_cy, sx_to_cx) line_dist = math.hypot(sx_to_cx, sy_to_cy) adj_rad = math.atan2(y_adjust, x_adjust) adj_dist = math.hypot(x_adjust, y_adjust) if line_dist != 0: relative_dist = adj_dist / line_dist else: relative_dist = adj_dist relative_rad = adj_rad - line_rad return relative_dist, relative_rad def click(self, event=None): """ Clicking a control point usually does nothing. These are more for dragging. :param event: some kind of mouse event :return: bool """ pass return True # consumes click def drag(self, event): """ Dragging a control point at least requires to update its coordinates and announcing the host object that things are a'changing. How this will be announced depends on control point's _role_. :param event: some kind of mouse event :return: None """ scenepos = event.scenePos() if self.role == g.LABEL_START: d, point = self.host.get_closest_path_point(scenepos) self.host.label_item.label_start = d else: self.setPos(scenepos) if self.role == g.CURVE_ADJUSTMENT: rdist, rrad = self._compute_adjust_from_pos(scenepos) self.host.adjust_control_point(self._index, rdist, rrad) elif self.role == g.START_POINT: self.host.set_start_point(event.scenePos()) self.host.make_path() self.host.update() elif self.role == g.END_POINT: self.host.set_end_point(event.scenePos()) self.host.make_path() self.host.update() self.being_dragged = True def drop_to(self, x, y, recipient=None): """ Dragging ends, possibly by dropping over another object. :param x: scene x coordinate :param y: scene y coordinate :param recipient: object that receives the dropped _control point_ """ if recipient: # recipient.accept_drop(self) if self.role == g.START_POINT: self.host.connect_start_to(recipient) elif self.role == g.END_POINT: self.host.connect_end_to(recipient) def mousePressEvent(self, event): ctrl.press(self) super().mousePressEvent(event) def mouseMoveEvent(self, event): if ctrl.pressed is self: if self.being_dragged or ( event.buttonDownScenePos(QtCore.Qt.LeftButton) - event.scenePos()).manhattanLength() > 6: self.drag(event) ctrl.graph_scene.dragging_over(event.scenePos()) def mouseReleaseEvent(self, event): if ctrl.pressed is self: ctrl.release(self) if self.being_dragged: x, y = to_tuple(event.scenePos()) self.drop_to(x, y, recipient=ctrl.drag_hovering_on) self.being_dragged = False ctrl.graph_scene.kill_dragging() ctrl.ui.update_selections( ) # drag operation may have changed visible affordances ctrl.main.action_finished() # @UndefinedVariable return None # this mouseRelease is now consumed super().mouseReleaseEvent(event) def hoverEnterEvent(self, event): """ Trigger and update hover effects. :param event: somekind of qt mouse event? """ self._hovering = True ctrl.set_status(self.status_tip) QtWidgets.QGraphicsItem.hoverEnterEvent(self, event) def hoverLeaveEvent(self, event): """ Remove hover effects for this piece. :param event: somekind of qt mouse event? """ self._hovering = False ctrl.remove_status(self.status_tip) QtWidgets.QGraphicsItem.hoverLeaveEvent(self, event) def paint(self, painter, option, widget=None): """ :param painter: :param option: :param widget: """ cm = ctrl.cm if self.round: if self.pressed: p = QtGui.QPen(cm.active(cm.ui_tr())) elif self._hovering: p = QtGui.QPen(cm.hovering(cm.ui_tr())) else: p = QtGui.QPen(cm.ui_tr()) if self.role == g.START_POINT or self.role == g.END_POINT: p.setWidth(4) painter.setPen(p) painter.drawEllipse(self._xy, self._xy, self._wh, self._wh) elif self.role == g.LABEL_START: p.setWidth(1) painter.setPen(p) painter.drawRect(self._xy, self._xy, self._wh, self._wh) else: p.setWidth(2) painter.setPen(p) painter.drawEllipse(self._xy, self._xy, self._wh, self._wh) # # if self.pressed: # painter.setBrush(cm.active(cm.ui_tr())) # elif self._hovering: # painter.setBrush(cm.hovering(cm.ui_tr())) # else: # painter.setBrush(cm.ui_tr()) # painter.setPen(qt_prefs.no_pen) # painter.drawEllipse(self._xy, self._xy, self._wh, self._wh) else: if self.pressed: pen = cm.active(cm.selection()) elif self._hovering: pen = cm.hovering(cm.selection()) else: pen = cm.ui() painter.setPen(pen) painter.drawRect(self._xy, self._xy, self._wh, self._wh) def watch_alerted(self, obj, signal, field_name, value): print(obj, signal, field_name, value)
class RightAddLeafSibling(RightAddChild): __qt_type_id__ = next_available_type_id()