class DigitalClock(Desklet): def __init__(self): super().__init__() self.seconds = QGraphicsSimpleTextItem(self.root) self.time = QGraphicsSimpleTextItem(self.root) self.date = QGraphicsSimpleTextItem(self.root) def set_style(self, style): super().set_style(style) self.seconds.setBrush(style.foreground_color) self.time.setBrush(style.foreground_color) self.date.setBrush(style.foreground_color) font = QFont(style.font) font.setPixelSize(192 * 0.5) self.seconds.setFont(font) font = QFont(style.font) font.setPixelSize(192 * 0.75) self.time.setFont(font) font = QFont(style.font) font.setPixelSize(56) self.date.setFont(font) self.layout() def set_rect(self, rect): super().set_rect(rect) self.layout() def layout(self): time_fm = QFontMetrics(self.time.font()) seconds_fm = QFontMetrics(self.seconds.font()) x = self.rect.left() y = self.rect.top() self.time.setPos(x, y) self.seconds.setPos(x + time_fm.width("00:00") + 20, y + time_fm.ascent() - seconds_fm.ascent()) self.date.setPos(x, y + time_fm.ascent()) def update(self, now): date = now.strftime("%A, %d. %B %Y") time = now.strftime("%H:%M") seconds = now.strftime("%S") self.time.setText(time) self.seconds.setText(seconds) self.date.setText(date)
def build_bonus_fields(self): for bonus_field in self._bonus_fields: brush = bonus_field["Brush"] pen = bonus_field["Pen"] bonus_fields = [] for coords in bonus_field["Coords"]: label = bonus_field["Name"] if coords == Coords.central(): label = '✸' square = self._board_squares[coords] square.setZValue(2) bonus_fields.append(square) field_name = QGraphicsSimpleTextItem(label) font = field_name.font() font.setPointSize(10) if coords == Coords.central(): font.setPointSize(20) fm = QFontMetrics(font) field_name.setZValue(2.1) field_name.setFont(font) x = coords.x * SQUARE_SIZE + (SQUARE_SIZE - fm.width(label)) / 2 y = coords.y * SQUARE_SIZE + (SQUARE_SIZE - fm.height()) / 2 field_name.setPos(x, y) field_name.setBrush(bonus_field["Label brush"]) self._labels[(x, y)] = field_name self.scene.addItem(field_name) paint_graphic_items(bonus_fields, pen, brush)
def drawHRefs(self): if self.xvmax < self.xvmin or self.awidth <= 0: return minsep = 30 factor = 1 unitincrement = self.awidth/float(self.xvmax-self.xvmin) xmaxint = self.xvmax vx = int(self.xvmin) pstart = self.value2point(vx, self.yvmin) px = pstart.x() pystart = pstart.y() pend = self.value2point(xmaxint, self.yvmin) pxend = pend.x() pyend = pend.y()-2 try: minsep = 10 * max([len(h) for h in self.hheaders]) except Exception: pass while (unitincrement*factor < minsep): provfactor = 2*factor if(unitincrement*provfactor > minsep): factor = provfactor break provfactor = 5*factor if(unitincrement*provfactor > minsep): factor = provfactor break factor = 10*factor # px+=unitincrement*factor # vx +=factor while(px <= pxend): colour = QtGui.QColor(0, 0, 0, 255) PlotLine(px+0.5, pystart+2, px+0.5, pyend, 1.5, colour, self) try: header = self.hheaders[vx] except IndexError: header = vx nlabel = QGraphicsSimpleTextItem( "{}".format(header), self) font = nlabel.font() font.setPixelSize(20) nlabel.setFont(font) nlabelrect = nlabel.boundingRect() nlabel.setPos(px + 0.5 - nlabelrect.width()/2, pystart+3) px += unitincrement*factor vx += factor
def _draw_node(self, node: Node, x: float, y: float): """ Drawing the Node on the map. """ text_item = QGraphicsSimpleTextItem() font = text_item.font() font.setPointSize(font.pointSize() - 2) font.setPixelSize(self._font_size) text_item.setText(str(node.value())) text_item.setPos(x, y) text_item.setFont(font) self._scene.addItem(text_item)
def addRectText(x, w, parent, text="", level=0, tooltip=""): deltaH = LEVEL_HEIGHT if level else 0 r = OutlineRect(0, 0, w, parent.rect().height()-deltaH, parent, title=text) r.setPos(x, deltaH) txt = QGraphicsSimpleTextItem(text, r) f = txt.font() f.setPointSize(8) fm = QFontMetricsF(f) elidedText = fm.elidedText(text, Qt.ElideMiddle, w) txt.setFont(f) txt.setText(elidedText) txt.setPos(r.boundingRect().center() - txt.boundingRect().center()) txt.setY(0) return r
def drawVRefs(self): if self.yvmax < self.yvmin or self.aheight <= 0: return minsep = 30 factor = 1 try: unitincrement = self.aheight/float(self.yvmax-self.yvmin) except ZeroDivisionError: msg = "Division by zero in drawVRefs. Limits are {}-{}" print(msg.format(self.yvmin, self.yvmax)) while (unitincrement*factor < minsep): provfactor = 2*factor if(unitincrement*provfactor > minsep): factor = provfactor break provfactor = 5*factor if(unitincrement*provfactor > minsep): factor = provfactor break factor = 10*factor if (self.yvmin <= 0): vy = int(self.yvmin/factor)*factor else: vy = (int(self.yvmin/factor)+1)*factor pstart = self.value2point(self.xvmin, vy) pxstart = pstart.x() py = pstart.y() pend = self.value2point(self.xvmax, self.yvmax) pxend = pend.x() pyend = pend.y() while(py > pyend): colour = QtGui.QColor(0, 0, 0, 200) if vy == 0: PlotLine(pxstart-2, py, pxend, py, 1.5, QtCore.Qt.black, self) else: PlotLine(pxstart-2, py, pxend, py, 0.5, colour, self) nlabel = QGraphicsSimpleTextItem("{}".format(vy), self) font = nlabel.font() font.setPixelSize(20) nlabel.setFont(font) nlabelrect = nlabel.boundingRect() nlabel.setPos(pxstart - nlabelrect.width() - 5, py-nlabelrect.height()/2) py -= unitincrement*factor vy += factor
class StopWatch(Desklet): def __init__(self): super().__init__() self.timer = Timer() self.timeout_handle = None self.qtimer = QTimer() self.qtimer.timeout.connect(self.my_update) self.label = QGraphicsSimpleTextItem("Stopwatch:", self.root) self.time = QGraphicsSimpleTextItem("00:00", self.root) self.seconds = QGraphicsSimpleTextItem("00'00", self.root) def update(self): t = self.timer.get_time() time = "%02d:%02d" % (t.seconds / (60 * 60), (t.seconds % (60 * 60)) / 60) seconds = "%02d'%02d" % (t.seconds % 60, t.microseconds / 10000) self.time.setText(time) self.seconds.setText(seconds) def set_style(self, style): super().set_style(style) font = QFont(style.font) font.setPixelSize(24) self.time.setFont(font) self.label.setFont(font) font = QFont(style.font) font.setPixelSize(192 / 2) self.time.setFont(font) font = QFont(style.font) font.setPixelSize(192 / 2 * 0.6) self.seconds.setFont(font) self.label.setBrush(self.style.foreground_color) self.time.setBrush(self.style.foreground_color) self.seconds.setBrush(self.style.foreground_color) self.layout() def set_rect(self, rect): super().set_rect(rect) self.layout() def layout(self): x = self.rect.left() y = self.rect.top() fm = QFontMetrics(self.time.font()) rect = fm.boundingRect("00:00") sfm = QFontMetrics(self.seconds.font()) self.time.setPos(x, y + 20) self.seconds.setPos(x + 20 + rect.width(), y + 20 + fm.ascent() - sfm.ascent()) self.label.setPos(x, y) def my_update(self): self.update() def is_running(self): return self.timer.is_running() def start_stop_watch(self): self.timer.start_stop() if self.timer.is_running(): self.qtimer.setInterval(31) self.qtimer.start() else: self.qtimer.stop() def clear_stop_watch(self): self.timer.reset()
def create_info_display(self, x, y, attributes): """ Creates view elements for the info display :param x: x position of the node :param y: y position of the node :param attributes: attributes that will be displayed in the view :return: """ start_height = y + (self.NODE_HEIGHT / 2) # unfold dictionary values at the bottom of the list sorted_attributes = [] for k, v in sorted(attributes.items(), key=lambda tup: isinstance(tup[1], dict)): if isinstance(v, dict): sorted_attributes.append((k, v)) sorted_attributes.extend(v.items()) else: sorted_attributes.append((k, v)) # create property rows for i, (k, v) in enumerate(sorted_attributes): value_text = None value_height = 0 if isinstance(v, dict): # display dictionary key as title text = "{}".format(k) if len(text) > 20: text = text[:20] + "..." key_text = QGraphicsSimpleTextItem(text) f = key_text.font() f.setBold(True) key_text.setFont(f) text_width = key_text.boundingRect().width() else: key_text = QGraphicsSimpleTextItem("{}:".format(k) if k else " ") text = str(v) if len(text) > 20: text = text[:20] + "..." value_text = QGraphicsSimpleTextItem(text) value_height = value_text.boundingRect().height() text_width = key_text.boundingRect().width() + value_text.boundingRect().width() # create box around property attribute_container = QGraphicsRectItem(x, start_height, text_width + 10, max(key_text.boundingRect().height(), value_height) + 10) attribute_container.setBrush(QBrush(Qt.white)) self.total_height += attribute_container.rect().height() key_text.setParentItem(attribute_container) if value_text: value_text.setParentItem(attribute_container) self.max_width = max(self.max_width, attribute_container.rect().width()) attribute_container.setParentItem(self) self.info_display.append(attribute_container) start_height += max(key_text.boundingRect().height(), value_height) + 10 # calculate correct coordinates for positioning of the attribute boxes if self.max_width > self.NODE_MIN_WIDTH - 10: x -= (self.max_width + 10) / 2 y -= self.total_height / 2 self.max_width += 10 else: x -= self.NODE_MIN_WIDTH / 2 y -= self.total_height / 2 self.max_width = self.NODE_MIN_WIDTH h = 0 # position all the elements previously created for attribute_container in self.info_display: rect: QRectF = attribute_container.rect() rect.setX(x) rect_height = rect.height() rect.setY(y + self.NODE_HEIGHT + h) rect.setHeight(rect_height) key_child = attribute_container.childItems()[0] if len(attribute_container.childItems()) == 2: key_child.setX(x + 5) value_child = attribute_container.childItems()[1] value_child.setX(x + self.max_width - value_child.boundingRect().width() - 5) value_child.setY(y + self.NODE_HEIGHT + h + 5) else: key_child.setX(x - key_child.boundingRect().width() / 2 + self.max_width / 2) key_child.setY(y + self.NODE_HEIGHT + h + 5) h += rect.height() rect.setWidth(self.max_width) attribute_container.setRect(rect)
class Node(QGraphicsItem): logger = logging.getLogger('ViewNode') i = 0 NODE_MIN_WIDTH = 100 NODE_MAX_WIDTH = 150 NODE_HEIGHT = 50 NODE_COLOR = (152, 193, 217) # LIGHT BLUE TACTIC_COLOR = (255, 51, 51) # RED STRATEGY_COLOR = (77, 255, 77) # GREEN ROLE_COLOR = (166, 77, 255) # PURPLE KEEPER_COLOR = (255, 255, 26) # YELLOW OTHER_SUBTREE_COLOR = (147, 147, 147) # GREY DECORATOR_COLOR = (51, 51, 255) # DARK BLUE COMPOSITE_COLOR = (255, 153, 0) # ORANGE OTHER_NODE_TYPES_COLOR = (255, 102, 153) # PINK DEFAULT_SIMULATOR_COLOR = Qt.white def __init__(self, x: float, y: float, scene: QGraphicsScene, model_node: ModelNode, title: str = None, parent: QGraphicsItem = None, node_types: NodeTypes = None): """ The constructor for a UI node :param x: x position for the center of the node :param y: y position for the center of the node :param title: title of the node displayed in the ui :param parent: parent of this graphics item """ if title: self.title = title else: # give node a unique title self.title = "node {}".format(Node.i) self.id = model_node.id self.x = x self.y = y Node.i += 1 self.scene = scene self.model_node = model_node self.children = [] self.edges = [] # store node positional data when detaching from parent self.expand_data = None # add node name label centered in the eclipse, elide if title is too long self.node_text = QGraphicsSimpleTextItem() metrics = QFontMetrics(self.node_text.font()) elided_title = metrics.elidedText(self.title, Qt.ElideRight, self.NODE_MAX_WIDTH) self.node_text.setText(elided_title) self.node_text.setAcceptedMouseButtons(Qt.NoButton) self.node_text.setAcceptHoverEvents(False) self.text_width = self.node_text.boundingRect().width() self.text_height = self.node_text.boundingRect().height() self.node_text.setX(x - (self.text_width / 2)) # call super function now we know the node size super(Node, self).__init__(parent) self.node_text.setParentItem(self) # indicates if node is being dragged self.dragging = False self.setCursor(Qt.PointingHandCursor) self.setAcceptHoverEvents(True) # give the node a default color self.brush = QBrush(QColor(*self.NODE_COLOR)) self.simulator_brush = QBrush(self.DEFAULT_SIMULATOR_COLOR) # give node another color if node_types: # check for node types and color them types = node_types.get_node_type_by_name(model_node.title) if len(types) > 0: category, node_type = types[0] if category == 'decorators': self.brush.setColor(QColor(*self.DECORATOR_COLOR)) elif category == 'composites': self.brush.setColor(QColor(*self.COMPOSITE_COLOR)) else: self.brush.setColor(QColor(*self.OTHER_NODE_TYPES_COLOR)) # check for a strategy, role, tactic or keeper if 'name' in model_node.attributes.keys() or 'role' in model_node.attributes.keys(): if model_node.title == 'Tactic': self.brush.setColor(QColor(*self.TACTIC_COLOR)) elif model_node.title == 'Strategy': self.brush.setColor(QColor(*self.STRATEGY_COLOR)) elif model_node.title == 'Keeper': self.brush.setColor(QColor(*self.KEEPER_COLOR)) elif model_node.title == 'Role': self.brush.setColor(QColor(*self.ROLE_COLOR)) else: self.brush.setColor(QColor(*self.OTHER_SUBTREE_COLOR)) self.info_display = [] self.max_width = 0 self.total_height = 0 self.bottom_collapse_expand_button = None self.top_collapse_expand_button = None self._rect = None self.initiate_view() def initiate_view(self, propagate=False): """ Initiates all the children for the current view :param propagate: Propagate initiate view signal to children """ for rect in self.info_display: rect.setParentItem(None) if self.top_collapse_expand_button and self.bottom_collapse_expand_button: self.top_collapse_expand_button.setParentItem(None) self.bottom_collapse_expand_button.setParentItem(None) self.info_display = [] self.max_width = self.text_width + 10 self.total_height = self.NODE_HEIGHT if self.scene.info_mode: model_node = self.scene.gui.tree.nodes[self.id] self.create_info_display(self.x, self.y, model_node.attributes) if self.max_width > self.NODE_MIN_WIDTH - 10: self._rect = QRect(self.x - self.max_width / 2, self.y - self.total_height / 2, self.max_width, self.total_height) else: self._rect = QRect(self.x - self.NODE_MIN_WIDTH / 2, self.y - self.total_height / 2, self.NODE_MIN_WIDTH, self.total_height) # set node size based on children self.node_text.setY(self.y - self.total_height / 2 + self.NODE_HEIGHT / 2 - self.text_height / 2) self.create_expand_collapse_buttons() self.scene.update() if propagate: for c in self.children: c.initiate_view(True) for e in self.edges: e.change_position() def create_expand_collapse_buttons(self): """ Creates the expand/collapse buttons of the node """ # create the bottom collapse/expand button for this node if self.bottom_collapse_expand_button: bottom_collapsed = self.bottom_collapse_expand_button.isCollapsed else: bottom_collapsed = False self.bottom_collapse_expand_button = CollapseExpandButton(self) self.bottom_collapse_expand_button.setParentItem(self) self.bottom_collapse_expand_button.collapse.connect(self.collapse_children) self.bottom_collapse_expand_button.expand.connect(self.expand_children) self.bottom_collapse_expand_button.isCollapsed = bottom_collapsed # position the bottom button at the bottom-center of the node button_x = self.x - (self.bottom_collapse_expand_button.boundingRect().width() / 2) button_y = self.y + self.total_height / 2 - (self.bottom_collapse_expand_button.boundingRect().height() / 2) self.bottom_collapse_expand_button.setPos(button_x, button_y) # hidden by default, the button is only needed if the node has children if not self.children: self.bottom_collapse_expand_button.hide() # create the top collapse/expand button for this node if self.top_collapse_expand_button: top_collapsed = self.top_collapse_expand_button.isCollapsed else: top_collapsed = False self.top_collapse_expand_button = CollapseExpandButton(self) self.top_collapse_expand_button.setParentItem(self) self.top_collapse_expand_button.collapse.connect(self.collapse_upwards) self.top_collapse_expand_button.expand.connect(self.expand_upwards) self.top_collapse_expand_button.isCollapsed = top_collapsed if self.scene.root_ui_node == self or self in self.scene.disconnected_nodes \ or self.scene.reconnecting_node == self: self.top_collapse_expand_button.hide() # position the top button at the top-center of the node button_x = self.x - (self.top_collapse_expand_button.boundingRect().width() / 2) button_y = self.y - self.total_height / 2 - (self.top_collapse_expand_button.boundingRect().height() / 2) self.top_collapse_expand_button.setPos(button_x, button_y) def create_info_display(self, x, y, attributes): """ Creates view elements for the info display :param x: x position of the node :param y: y position of the node :param attributes: attributes that will be displayed in the view :return: """ start_height = y + (self.NODE_HEIGHT / 2) # unfold dictionary values at the bottom of the list sorted_attributes = [] for k, v in sorted(attributes.items(), key=lambda tup: isinstance(tup[1], dict)): if isinstance(v, dict): sorted_attributes.append((k, v)) sorted_attributes.extend(v.items()) else: sorted_attributes.append((k, v)) # create property rows for i, (k, v) in enumerate(sorted_attributes): value_text = None value_height = 0 if isinstance(v, dict): # display dictionary key as title text = "{}".format(k) if len(text) > 20: text = text[:20] + "..." key_text = QGraphicsSimpleTextItem(text) f = key_text.font() f.setBold(True) key_text.setFont(f) text_width = key_text.boundingRect().width() else: key_text = QGraphicsSimpleTextItem("{}:".format(k) if k else " ") text = str(v) if len(text) > 20: text = text[:20] + "..." value_text = QGraphicsSimpleTextItem(text) value_height = value_text.boundingRect().height() text_width = key_text.boundingRect().width() + value_text.boundingRect().width() # create box around property attribute_container = QGraphicsRectItem(x, start_height, text_width + 10, max(key_text.boundingRect().height(), value_height) + 10) attribute_container.setBrush(QBrush(Qt.white)) self.total_height += attribute_container.rect().height() key_text.setParentItem(attribute_container) if value_text: value_text.setParentItem(attribute_container) self.max_width = max(self.max_width, attribute_container.rect().width()) attribute_container.setParentItem(self) self.info_display.append(attribute_container) start_height += max(key_text.boundingRect().height(), value_height) + 10 # calculate correct coordinates for positioning of the attribute boxes if self.max_width > self.NODE_MIN_WIDTH - 10: x -= (self.max_width + 10) / 2 y -= self.total_height / 2 self.max_width += 10 else: x -= self.NODE_MIN_WIDTH / 2 y -= self.total_height / 2 self.max_width = self.NODE_MIN_WIDTH h = 0 # position all the elements previously created for attribute_container in self.info_display: rect: QRectF = attribute_container.rect() rect.setX(x) rect_height = rect.height() rect.setY(y + self.NODE_HEIGHT + h) rect.setHeight(rect_height) key_child = attribute_container.childItems()[0] if len(attribute_container.childItems()) == 2: key_child.setX(x + 5) value_child = attribute_container.childItems()[1] value_child.setX(x + self.max_width - value_child.boundingRect().width() - 5) value_child.setY(y + self.NODE_HEIGHT + h + 5) else: key_child.setX(x - key_child.boundingRect().width() / 2 + self.max_width / 2) key_child.setY(y + self.NODE_HEIGHT + h + 5) h += rect.height() rect.setWidth(self.max_width) attribute_container.setRect(rect) def paint(self, painter: QPainter, style_options: QStyleOptionGraphicsItem, widget=None): """ Paint the basic shape of the node (ellipse or rectangle) :param painter: painter used to paint objects :param style_options: Styling options for the graphics item :param widget: The widget being painted """ painter.setPen(Qt.SolidLine) if self == self.scene.root_ui_node: pen = QPen(Qt.black, 2.0) pen.setStyle(Qt.DotLine) painter.setPen(pen) if self.scene.simulator_mode: brush = self.simulator_brush else: brush = self.brush painter.setBrush(brush) if self.scene.info_mode: painter.drawRect(self.rect().x(), self.rect().y(), self.rect().width(), self.NODE_HEIGHT) else: painter.drawEllipse(self.rect()) def add_child(self, child): """ Add a child node Inheritance looks like: parent > edge > child :param child: Another ui node """ edge = Edge(self, child) edge.setParentItem(self) # edge should stay behind the expand/collapse button edge.stackBefore(self.bottom_collapse_expand_button) self.children.append(child) self.edges.append(edge) # show the expand/collapse button when the first child is added if not self.bottom_collapse_expand_button.isVisible(): self.bottom_collapse_expand_button.show() if not child.top_collapse_expand_button.isVisible(): child.top_collapse_expand_button.show() def remove_child(self, child): """ Removes child from this node (no data changes) :param child: Child of this node """ if child not in self.children: Node.logger.error("Incorrect child can not be removed from wrong parent.") edge = child.parentItem() child.setParentItem(None) self.children.remove(child) self.edges.remove(edge) edge.setParentItem(None) self.scene.removeItem(edge) if not self.children: self.bottom_collapse_expand_button.hide() def nodes_below(self): nodes = [] for c in self.children: nodes.append(c) nodes.extend(c.nodes_below()) return nodes def moveBy(self, x, y): super(Node, self).moveBy(x, y) # move edge correctly with node if self.parentItem() and isinstance(self.parentItem(), Edge): self.parentItem().change_position() def setPos(self, *args): super(Node, self).setPos(*args) # move edge correctly with node if self.parentItem() and isinstance(self.parentItem(), Edge): self.parentItem().change_position() def xoffset(self): """ recursively adds the relative x distances from this node up until the root node. :return: the sum of the relative x distances """ if self.parentItem(): return self.pos().x() + self.parentItem().xoffset() else: return self.pos().x() + self.rect().x() + self.rect().width() / 2 def yoffset(self): """ recursively adds the relative y distances from this node up until the root node. :return: the sum of the relative y distances """ if self.parentItem(): return self.pos().y() + self.parentItem().yoffset() else: return self.pos().y() + self.rect().y() + self.rect().height() / 2 def xpos(self): """ Calculates the x position of this node using the x offset :return: the x position of the node """ return self.xoffset() def ypos(self): """ Calculates the y position of this node using the y offset :return: the y position of the node """ return self.yoffset() def boundingRect(self): return QRectF(self._rect) def rect(self): return self._rect def detach_from_parent(self): """ Detaches node from parent (no data changes) :return: Positional data that can be used to reattach node """ if not self.parentItem() or not self.parentItem().parentItem(): Node.logger.error("The node can't detach from parent, no parent") return # store attach data used to restore the state when attaching xpos, ypos = self.xpos(), self.ypos() root_item = self.scene.root_ui_node parent_node = self.parentItem().parentItem() attach_data = { "abs_pos": QPointF(xpos, ypos), "old_parent": parent_node, "top_level_item": self.topLevelItem(), } parent_node.remove_child(self) # move node to retain correct position self.setPos(0, 0) root_x = root_item.xpos() if root_item else self.scene.node_init_pos[0] root_y = root_item.ypos() if root_item else self.scene.node_init_pos[1] move_x = xpos - root_x - (self.scene.node_init_pos[0] - root_x) move_y = ypos - root_y - (self.scene.node_init_pos[1] - root_y) self.moveBy(move_x, move_y) return attach_data def attach_to_parent(self, data, parent=None): """ Attaches node to parent (no data changes) :param: data: Positional data from detachment used for attaching """ if not parent: parent = data['old_parent'] new_abs_pos = QPointF(self.xpos(), self.ypos()) # reset parent item e = Edge(parent, self) e.setParentItem(parent) parent.children.append(self) parent.edges.append(e) parent.sort_children() parent_abs_pos = QPointF(parent.xpos(), parent.ypos()) # reset relative position to parent self.setPos(new_abs_pos - parent_abs_pos) def collapse_upwards(self): """ Collapses the tree upwards only displaying this node and its children :return: """ self.expand_data = self.detach_from_parent() # hide parent nodes self.expand_data['top_level_item'].hide() def expand_upwards(self): """ Expands the tree upwards displaying all expanded parent nodes :return: """ self.attach_to_parent(self.expand_data) # show expanded parent nodes self.topLevelItem().show() def collapse_children(self): """ Collapses this node's children by hiding all child edges (and therefore the whole subtree) """ for c in self.childItems(): if isinstance(c, Edge): c.hide() def expand_children(self): """ Expands this node's children by showing all child edges previously hidden by the collapse function """ for c in self.childItems(): if isinstance(c, Edge): c.show() def sort_children(self): """ Sort child edges/nodes based on x position :return: The model nodes in order """ # gather all the edges child_edges = [edge for edge in self.childItems() if isinstance(edge, Edge)] # sort edges by x position of the child nodes child_edges.sort(key=lambda c: c.end_node.xpos()) # reset internal structure self.edges.clear() self.children.clear() # add children back in correct order for e in child_edges: e.setParentItem(None) self.edges.append(e) self.children.append(e.end_node) # set the parent of the children in the correct order for e in child_edges: e.setParentItem(self) # return the model nodes in the correct order model_nodes_order = [e.end_node.model_node for e in child_edges] return model_nodes_order def detect_order_change(self): """ Detects if node order has changed and updates model accordingly """ if not self.parentItem(): # sort top level nodes, this prevents alignment issues self.scene.disconnected_nodes = sorted(self.scene.disconnected_nodes, key=lambda n: n.xpos()) else: # parent node of self parent_node = self.parentItem().parentItem() parent_model_node = self.scene.gui.tree.nodes.get(parent_node.id) # own child index node_index = parent_node.children.index(self) # check if node is swapped with left neighbour try: if node_index - 1 >= 0: # can throw IndexError if there is no left neighbour left_node = parent_node.children[node_index - 1] # check if node is swapped if left_node.xpos() > self.xpos(): # sort children of parent sorted_nodes = parent_node.sort_children() # change model tree structure accordingly parent_model_node.children = [n.id for n in sorted_nodes] self.scene.gui.update_tree(parent_model_node) except IndexError: pass # check if node is swapped with right neighbour try: # can throw IndexError if there is no right neighbour right_node = parent_node.children[node_index + 1] # check if node is swapped if right_node.xpos() < self.xpos(): # sort children of parent sorted_nodes = parent_node.sort_children() # change model tree structure accordingly parent_model_node.children = [n.id for n in sorted_nodes] self.scene.gui.update_tree(parent_model_node) except IndexError: pass def delete_self(self): """ Deletes this node and makes children disconnected subtrees/nodes """ for c in self.children[:]: c.detach_from_parent() # add child to disconnected nodes if self in self.scene.disconnected_nodes: index = self.scene.disconnected_nodes.index(self) self.scene.disconnected_nodes.insert(index, c) else: self.scene.disconnected_nodes.insert(0, c) c.top_collapse_expand_button.hide() parent_model_node = None if self.parentItem(): parent_node: Node = self.parentItem().parentItem() parent_node.remove_child(self) parent_model_node = self.scene.gui.tree.nodes.get(parent_node.id) parent_model_node.children.remove(self.id) if self in self.scene.disconnected_nodes: self.scene.disconnected_nodes.remove(self) self.scene.removeItem(self) self.scene.close_property_display() del self.scene.nodes[self.id] # reset root if this is the root if self.scene.gui.tree.root == self.id: self.scene.gui.tree.root = '' # remove node from internal tree structure del self.scene.gui.tree.nodes[self.id] if parent_model_node: self.scene.gui.update_tree(parent_model_node) def delete_subtree(self, delete_parent_relation=True, update_tree=True): """ Deletes node and its children :param delete_parent_relation: Boolean indicating if parent relation should be modified :param update_tree: Boolean indicating if the tree needs an update """ # remove children for c in self.children: c.delete_subtree(delete_parent_relation=False) # remove child reference from parent parent_node = None if delete_parent_relation and self.parentItem(): parent_node: Node = self.parentItem().parentItem() parent_node.remove_child(self) try: self.scene.gui.tree.nodes[parent_node.id].children.remove(self.id) except ValueError: pass self.scene.removeItem(self) self.scene.close_property_display() if self in self.scene.disconnected_nodes: self.scene.disconnected_nodes.remove(self) self.scene.nodes.pop(self.id, None) if self.scene.gui.tree.root == self.id: self.scene.gui.tree.root = '' # remove node from internal tree structure self.scene.gui.tree.nodes.pop(self.id, None) if delete_parent_relation and parent_node and update_tree: node = self.scene.gui.tree.nodes.get(parent_node.id) self.scene.gui.update_tree(node) def reconnect_edge(self): """ Starts edge reconnection process """ if not self.parentItem() and self not in self.scene.disconnected_nodes: Node.logger.error("The edge trying to reconnect does not exist.") else: self.scene.start_reconnect_edge(self) def mousePressEvent(self, m_event): """ Handles a mouse press on a node :param m_event: The mouse press event and its details """ super(Node, self).mousePressEvent(m_event) tree = self.scene.gui.tree.nodes[self.id] if self.scene.view.parent().property_display: self.scene.view.parent().property_display.setParent(None) self.scene.view.parent().property_display.deleteLater() self.scene.view.parent().property_display = view.widgets.TreeViewPropertyDisplay( self.scene.view.parent().graphics_scene, tree.attributes, parent=self.scene.view.parent(), node_id=tree.id, node_title=tree.title) def mouseMoveEvent(self, m_event): """ Handles a mouse move over a node :param m_event: The mouse move event and its details """ super(Node, self).mouseMoveEvent(m_event) if self.dragging: # move the node with the mouse and adjust the edges to the new position dx = m_event.scenePos().x() - m_event.lastScenePos().x() dy = m_event.scenePos().y() - m_event.lastScenePos().y() self.setPos(self.pos().x() + dx, self.pos().y() + dy) # Set correct order for children if node has a parent and the order of disconnected nodes self.detect_order_change() # reposition incoming edge if isinstance(self.parentItem(), Edge): self.parentItem().change_position() def contextMenuEvent(self, menu_event): """ Creates context menu for right clicks on this node :param menu_event: Context about the right click event """ menu = QMenu() reconnect_edge_action = QAction("Reconnect Edge" if self.parentItem() else "Connect Edge") reconnect_edge_action.triggered.connect(self.reconnect_edge) menu.addAction(reconnect_edge_action) delete_action = QAction("Delete Node") delete_action.setToolTip('Delete only this node.') delete_action.triggered.connect(self.delete_self) menu.addAction(delete_action) delete_subtree_action = QAction("Delete Subtree") delete_subtree_action.setToolTip('Delete node and all its children.') delete_subtree_action.triggered.connect(lambda: self.delete_subtree()) menu.addAction(delete_subtree_action) menu.exec(menu_event.screenPos()) menu_event.setAccepted(True)
class CalendarDesklet(Desklet): def __init__(self): super().__init__() self.model = CalendarModel() self.cursor_pos = None self.cursor = QGraphicsRectItem(self.root) self.header = QGraphicsSimpleTextItem(self.root) self.weekdays = [] days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] for day in days: self.weekdays.append(QGraphicsSimpleTextItem(day, self.root)) self.header_line = QGraphicsLineItem(self.root) self.days = [] for _ in range(0, 6 * 7): self.days.append(QGraphicsSimpleTextItem(self.root)) def next_month(self): self.model.next_month() self.layout() def previous_month(self): self.model.previous_month() self.layout() def set_rect(self, rect): super().set_rect(rect) self.layout() def set_style(self, style): super().set_style(style) font = QFont(style.font) font.setPixelSize(48) self.header.setBrush(style.midcolor) self.header.setFont(font) font = QFont(style.font) font.setPixelSize(32) self.header_line.setPen(style.foreground_color) self.cursor.setBrush(style.midcolor) self.cursor.setPen(QPen(Qt.NoPen)) for widget in self.weekdays: widget.setFont(font) widget.setBrush(style.foreground_color) for widget in self.days: widget.setFont(font) widget.setBrush(self.style.foreground_color) self.layout() def layout(self): cell_width = (self.rect.width()) / 7.0 cell_height = (self.rect.height() - 64) / 7.0 x = self.rect.left() y = self.rect.top() fm = QFontMetrics(self.header.font()) rect = fm.boundingRect(self.header.text()) self.header.setPos(x + self.rect.width() / 2 - rect.width() / 2, y) y += fm.height() for row, day in enumerate(self.weekdays): fm = QFontMetrics(day.font()) rect = fm.boundingRect(day.text()) day.setPos(x + row * cell_width + cell_width / 2 - rect.width() / 2, y) y += fm.height() self.header_line.setLine(x, y, x + self.rect.width() - 3, y) y += 8 for n, widget in enumerate(self.days): col = n % 7 row = n // 7 rect = fm.boundingRect(widget.text()) widget.setPos(x + col * cell_width + cell_width / 2 - rect.width() / 2, y + row * cell_height + cell_height / 2 - fm.height() / 2) # if day.month != self.now.month: # widget.setBrush(self.style.midcolor) # else: if self.cursor_pos is not None: self.cursor.setRect(x + self.cursor_pos[0] * cell_width, y + self.cursor_pos[1] * cell_height, cell_width, cell_height) self.cursor.show() else: self.cursor.hide() def update(self, now): self.model.update(now) # update header self.header.setText( date(self.model.year, self.model.month, 1).strftime("%B %Y")) # calculate the date of the top/left calendar entry current_date = date(self.model.year, self.model.month, 1) current_date = current_date - timedelta(current_date.weekday()) self.cursor_pos = None for n, widget in enumerate(self.days): col = n % 7 row = n // 7 if current_date == self.model.today: self.cursor_pos = (col, row) widget.setText("%d" % current_date.day) self.days[n] = widget current_date += timedelta(days=1) self.layout()