def show_text(self, text, size): self.update() window_bg = QGraphicsRectItem() window_bg.setRect(-OFSET[0], -OFSET[1], SCREEN_WIDTH, SCREEN_HEIGHT) window_bg.setBrush(QBrush(Qt.black)) self.addItem(window_bg) self.text = QGraphicsTextItem(text) self.text.setDefaultTextColor(Qt.white) self.text.setPos(window_bg.rect().center() - QPoint(120, size)) self.text.setFont(QFont('Press Start', int(size))) self.addItem(self.text)
class PickingStation(VisualizerGraphicItem): def __init__(self, ID=0, x=0, y=0): super(self.__class__, self).__init__(ID, x, y) self._kind_name = 'pickingStation' self._items = [] self._graphics_item = QGraphicsRectItem(self) self._items.append(QGraphicsRectItem(self._graphics_item)) self._items.append(QGraphicsRectItem(self._graphics_item)) self._text = QGraphicsTextItem(self._graphics_item) def set_rect(self, rect): scale = config.get('display', 'id_font_scale') bold = config.get('display', 'id_font_bold') self._text.setFont(QFont('', rect.width() * 0.08 * scale)) self._text.setPos(rect.x(), rect.y() + 0.6 * rect.height()) self._text.setDefaultTextColor( QColor(config.get('display', 'id_font_color'))) if self._display_mode == 0: if bold: self._text.setHtml('<b>P(' + str(self._id) + ')</b>') else: self._text.setHtml('P(' + str(self._id) + ')') self._graphics_item.setRect(rect.x(), rect.y(), rect.width(), rect.height()) self._items[0].setRect(rect.x() + rect.width() / 5, rect.y(), rect.width() / 5, rect.height()) self._items[1].setRect(rect.x() + rect.width() / 5 * 3, rect.y(), rect.width() / 5, rect.height()) elif self._display_mode == 1: self._text.setPlainText('') self._graphics_item.setRect(rect.x(), rect.y(), rect.width(), rect.height()) self._items[0].setRect(rect.x() + rect.width() / 5, rect.y(), rect.width() / 5, rect.height()) self._items[1].setRect(rect.x() + rect.width() / 5 * 3, rect.y(), rect.width() / 5, rect.height()) def determine_color(self, number, count, pattern=None): color = self._colors[0] color2 = self._colors[1] color.setAlpha(150) color2.setAlpha(150) brush = QBrush(color) brush2 = QBrush(color2) self._graphics_item.setBrush(brush) self._items[0].setBrush(brush2) self._items[1].setBrush(brush2) def get_rect(self): return self._graphics_item.rect()
def get_elements(self, cell, height): result = [] current = [] rectangles = self.doc.elementsByTagName('rect') for i in range(rectangles.size()): rect = rectangles.item(i).toElement() fill = '#aaaaaa' item = QGraphicsRectItem(float(rect.attribute('x')), float(rect.attribute('y')), float(rect.attribute('width')), float(rect.attribute('height'))) item.setBrush(QBrush(QColor(fill))) result.append(item) if rect.attribute('id') == str(cell): side_fill = fill size = item.rect().getRect() for j in range(1, height+1): x = size[0] - 12 * j y = size[1] - 20 * j if j == height: side_fill = '#ff0000' top = QGraphicsRectItem(x, y, size[2], size[3]) top.setBrush(QBrush(QColor(fill).darker(200))) current.append(top) path = QPainterPath() path.moveTo(x + size[2], y + size[3]) path.lineTo(x + size[2] + 12, y + size[3] + 20) path.lineTo(x + 12, y + size[3] + 20) path.lineTo(x, y + size[3]) front = QGraphicsPathItem(path) front.setBrush(QBrush(QColor(side_fill))) current.append(front) path = QPainterPath() path.moveTo(x + size[2], y + size[3]) path.lineTo(x + size[2] + 12, y + size[3] + 20) path.lineTo(x + size[2] + 12, y + 20) path.lineTo(x + size[2], y) right = QGraphicsPathItem(path) right.setBrush(QBrush(QColor(side_fill).lighter())) current.append(right) return result + current
def draw_board(self, size=Cons.SIZE): """ Draw playing board """ self.board_size = size self.c_size, self.r_size = Cons.SIZE_DICT[self.board_size] self.digits = Cons.DIGITS[:self.board_size] Tile.board_size = self.board_size Tile.digits = self.digits self.content = [["0"] * self.board_size for _ in range(self.board_size)] self.sudoku = [["0"] * self.board_size for _ in range(self.board_size)] self.tiles = [[None] * self.board_size for _ in range(self.board_size)] self.poss_tiles = [[None] * self.board_size for _ in range(self.board_size)] board_w = self.board_size * (Cons.WIDTH + Cons.MARGIN) + Cons.MARGIN # width of playing board board_h = self.board_size * (Cons.HEIGHT + Cons.MARGIN) + Cons.MARGIN # height of playing board board_x = (Cons.WINDOW_SIZE[0] - board_w) / 2 # x-coordinate of scrabble board board_y = (Cons.WINDOW_SIZE[1] - board_h) / 2 # y-coordinate of scrabble board board = [board_x, board_y, board_w, board_h] # board rect details self.cells = [] self.background_item = QGraphicsRectItem(board[0], board[1], board[2], board[3]) self.background_item.persistent = False self.background_item.setBrush(Qt.black) for row in range(self.board_size): for col in range(self.board_size): left_adj = Cons.INT_CELLS if col % self.c_size else 0 top_adj = Cons.INT_CELLS if row % self.r_size else 0 width_adj = Cons.INT_CELLS if col % self.c_size else 0 height_adj = Cons.INT_CELLS if row % self.r_size else 0 square = QGraphicsRectItem(board[0] + (Cons.MARGIN + Cons.WIDTH) * col + Cons.MARGIN - left_adj, board[1] + (Cons.MARGIN + Cons.HEIGHT) * row + Cons.MARGIN - top_adj, Cons.WIDTH + width_adj, Cons.HEIGHT + height_adj, parent=self.background_item) self.cells.append(square.rect()) setattr(square, 'cell', (row, col)) colour = Cons.NORMAL_COLOUR square.setBrush(QColor(colour[0], colour[1], colour[2])) square.setToolTip("Square") square.persistent = False self.scene.addItem(self.background_item) self.set_digits()
def on_actItem_Rect(self): # 添加一个矩形 _triggered rect = QRectF(-50, -25, 100, 50) item = QGraphicsRectItem(rect) # x,y 为左上角的图元局部坐标,图元中心点为0,0 item.rect = rect item.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsFocusable) item.brush = Qt.yellow item.setBrush(QBrush(item.brush)) item.style = Qt.SolidLine #item.setTransform(QTransform()) self.view.frontZ = self.view.frontZ + 1 item.setZValue(self.view.frontZ) item.setPos(-50 + (QtCore.qrand() % 100), -50 + (QtCore.qrand() % 100)) self.view.seqNum = self.view.seqNum + 1 item.setData(self.view.ItemId, self.view.seqNum) item.setData(self.view.ItemDesciption, "矩形") self.scene.addItem(item) self.scene.clearSelection() item.setSelected(True)
class MyScene(QGraphicsScene): def __init__(self, data, parent=None): QGraphicsScene.__init__(self, parent) self.data = data self.rec = QGraphicsRectItem() self.plot: MyItem(data) = None self.bounding_rect = QGraphicsRectItem() self.setBackgroundBrush(QColor('#14161f')) self.addItem(self.bounding_rect) self.printed = False def mouseMoveEvent(self, event: 'QGraphicsSceneMouseEvent'): print() print("rec rect : ", self.rec.rect()) print("Scene rect : ", self.sceneRect()) print("ItemBounding rect : ", self.itemsBoundingRect()) print("transform : ", self.plot.transform().m11(), ", ", self.plot.transform().m22()) item = self.itemAt(event.scenePos(), self.plot.transform()) if item and isinstance(item, MyItem): print() print('collides path : ', self.rec.collidesWithPath(item.mapToScene(item.path))) print('collides item : ', self.rec.collidesWithItem(item)) super().mouseMoveEvent(event) def print_bound(self, rect): self.bounding_rect.setPen(QPen(Qt.green)) self.bounding_rect.setRect(rect.x() + 5, rect.y() + 5, rect.width() - 10, rect.height() - 10)
class Robot(VisualizerGraphicItem): def __init__(self, ID=0, x=0, y=0): super(self.__class__, self).__init__(ID, x, y) self._kind_name = 'robot' self._carries = None self._initial_carries = None self._tasks = [] self._graphics_item = QGraphicsRectItem(self) self._text = QGraphicsTextItem(self) self.setZValue(1.0) def set_position(self, x, y): super(self.__class__, self).set_position(x, y) if self._carries is not None: self._carries.set_position(x, y) def set_starting_position(self, x, y): super(self.__class__, self).set_starting_position(x, y) if self._initial_carries is not None: self._initial_carries.set_starting_position(x, y) def set_carries(self, shelf): if shelf == self._carries: return old = self._carries self._carries = shelf if old != None: old.set_carried(None) if self._carries != None: self._carries.set_carried(self) def set_initial_carries(self, shelf): self._initial_carries = shelf def set_rect(self, rect): scale = config.get('display', 'id_font_scale') bold = config.get('display', 'id_font_bold') self._text.setFont(QFont('', rect.width() * 0.08 * scale)) self._text.setPos(rect.x(), rect.y() + 0.2 * rect.height()) self._text.setDefaultTextColor( QColor(config.get('display', 'id_font_color'))) if self._display_mode == 0: if bold: self._text.setHtml('<b>R(' + str(self._id) + ')</b>') else: self._text.setHtml('R(' + str(self._id) + ')') self._graphics_item.setRect( rect.x() + 0.25 * rect.width(), rect.y() + 0.25 * rect.height(), rect.width() * 0.5, rect.height() * 0.5, ) elif self._display_mode == 1: self._text.setPlainText('') self._graphics_item.setRect(rect.x() + 0.05 * rect.width(), rect.y() + 0.05 * rect.height(), rect.width() * 0.9, rect.height() * 0.9) if self._carries is not None: self._carries.set_rect(rect) def add_task(self, task): if task is None: return if task in self._tasks: return self._tasks.append(task) task.set_robot(self) def parse_init_value(self, name, value): result = super(self.__class__, self).parse_init_value(name, value) if result <= 0: return result if name == 'carries': shelf = self._model.get_item('shelf', value, True, True) self.set_initial_carries(shelf) self.set_carries(shelf) return 0 return 1 def restart(self): super(self.__class__, self).restart() self.set_carries(self._initial_carries) def to_init_str(self): s = super(self.__class__, self).to_init_str() if self._initial_carries is not None: s += ("init(object(robot," + str(self._id) + "),value(carries," + str(self._initial_carries.get_id()) + ")).") return s def do_action(self, time_step): if time_step >= len(self._actions): return 0 #break, if no action is defined if self._actions[time_step] == None: return 0 #break, if no action is defined if self._model is None: return -3 try: action = self._actions[time_step].arguments[0] value = self._actions[time_step].arguments[1] except: return -1 if action.name == 'move': if len(value.arguments) != 2: return -1 try: move_x = value.arguments[0].number move_y = value.arguments[1].number self.set_position(self._position[0] + move_x, self._position[1] + move_y) except: self.set_position(self._position[0], self._position[1]) for task in self._tasks: for checkpoint in task.get_checkpoints(): checkpoint = checkpoint[0] pos = checkpoint.get_position() if pos[0] == self._position[0] and pos[ 1] == self._position[1]: task.visit_checkpoint(checkpoint) break return 1 elif action.name == 'pickup': shelf = self._model.filter_items(item_kind='shelf', position=self._position, return_first=True)[0] if shelf is None: return -2 self.set_carries(shelf) return 2 elif action.name == 'putdown': if self._carries == None: return -2 self.set_carries(None) return 3 elif action.name == 'deliver' and len(value.arguments) > 2: try: if self._carries is not None: self._carries.remove_product(value.arguments[1], value.arguments[2].number) order = self._model.filter_items(item_kind='order', ID=value.arguments[0], return_first=True)[0] if order is None: return -2 order.deliver(value.arguments[1], value.arguments[2].number, time_step) except: return -3 return 4 elif action.name == 'deliver' and len(value.arguments) > 1: try: if self._carries is not None: self._carries.remove_product(value.arguments[1], 0) order = self._model.filter_items(item_kind='order', ID=value.arguments[0], return_first=True)[0] if order is None: return -2 order.deliver(value.arguments[1], 0, time_step) except: return -3 return 5 return 0 def undo_action(self, time_step): if time_step >= len(self._actions): return 0 #break, if no action is defined if self._actions[time_step] == None: return 0 #break, if no action is defined if self._model is None: return -3 try: action = self._actions[time_step].arguments[0] value = self._actions[time_step].arguments[1] except: return -1 if action.name == 'move': if len(value.arguments) != 2: return -1 for task in self._tasks: for checkpoint in task.get_checkpoints(): checkpoint = checkpoint[0] pos = checkpoint.get_position() if pos[0] == self._position[0] and pos[ 1] == self._position[1]: task.unvisit_checkpoint(checkpoint) break try: move_x = value.arguments[0].number move_y = value.arguments[1].number self.set_position(self._position[0] - move_x, self._position[1] - move_y) except: self.set_position(self._position[0], self._position[1]) if self._carries is not None: self._carries.set_position(self._position[0], self._position[1]) return 1 elif action.name == 'putdown': shelf = self._model.filter_items(item_kind='shelf', position=self._position, return_first=True)[0] if shelf is None: return -2 self.set_carries(shelf) return 3 elif action.name == 'pickup': if self._carries == None: return -2 self.set_carries(None) return 2 elif action.name == 'deliver' and len(value.arguments) > 2: try: if self._carries is not None: self._carries.remove_product(value.arguments[1], -value.arguments[2].number) order = self._model.filter_items(item_kind='order', ID=value.arguments[0], return_first=True)[0] if order is None: return -2 order.deliver(value.arguments[1], -value.arguments[2].number, time_step) except: return -3 return 4 elif action.name == 'deliver' and len(value.arguments) > 1: try: if self._carries is not None: self._carries.remove_product(value.arguments[1], 0) order = self._model.filter_items(item_kind='order', ID=value.arguments[0], return_first=True)[0] if order is None: return -2 order.deliver(value.arguments[1], 0, time_step) except: return -3 return 5 return 0 def determine_color(self, number, count, pattern=None): color = calculate_color(self._colors[0], self._colors[1], (float)(number) / count) brush = QBrush(color) self._graphics_item.setBrush(brush) def get_rect(self): if self._display_mode == 0: rect = self._graphics_item.rect() width = rect.width() * 2 height = rect.height() * 2 rect.setLeft(rect.x() - 0.25 * width) rect.setTop(rect.y() - 0.25 * height) rect.setWidth(width) rect.setHeight(height) return rect elif self._display_mode == 1: rect = self._graphics_item.rect() width = rect.width() / 0.9 height = rect.height() / 0.9 rect.setLeft(rect.x() - 0.05 * width) rect.setTop(rect.y() - 0.05 * height) rect.setWidth(width) rect.setHeight(height) return rect def get_carries(self): return self._carries def get_initial_carries(self): return self._initial_carries def edit_position_to(self, x, y): if (x, y) == self._position: return item2 = self._model.filter_items(item_kind=self._kind_name, position=(x, y), return_first=True)[0] shelf = self._model.filter_items('shelf', position=self._position, return_first=True)[0] shelf2 = self._model.filter_items('shelf', position=(x, y), return_first=True)[0] if shelf2 is not None and shelf is not None: if shelf.get_carried() is not None or shelf2.get_carried( ) is not None: shelf.set_position(x, y) shelf.set_starting_position(x, y) shelf2.set_position(self._position[0], self._position[1]) shelf2.set_starting_position(self._position, self._position[1]) if item2 is not None: item2.set_position(self._position[0], self._position[1]) item2.set_starting_position(self._position[0], self._position[1]) self.set_position(x, y) self.set_starting_position(x, y)
def refresh(self): if not self._mdlPlots or not self._mdlOutline or not self._mdlPersos: pass LINE_HEIGHT = 18 SPACING = 3 TEXT_WIDTH = self.sldTxtSize.value() CIRCLE_WIDTH = 10 LEVEL_HEIGHT = 12 s = self.scene s.clear() # Get Max Level (max depth) root = self._mdlOutline.rootItem def maxLevel(item, level=0, max=0): if level > max: max = level for c in item.children(): m = maxLevel(c, level + 1) if m > max: max = m return max MAX_LEVEL = maxLevel(root) # Generate left entries # (As of now, plot only) plotsID = self._mdlPlots.getPlotsByImportance() trackedItems = [] fm = QFontMetrics(s.font()) max_name = 0 for importance in plotsID: for ID in importance: name = self._mdlPlots.getPlotNameByID(ID) ref = references.plotReference(ID, searchable=True) trackedItems.append((ID, ref, name)) max_name = max(fm.width(name), max_name) ROWS_HEIGHT = len(trackedItems) * (LINE_HEIGHT + SPACING ) TITLE_WIDTH = max_name + 2 * SPACING # Add Folders and Texts outline = OutlineRect(0, 0, 0, ROWS_HEIGHT + SPACING + MAX_LEVEL * LEVEL_HEIGHT) s.addItem(outline) outline.setPos(TITLE_WIDTH + SPACING, 0) refCircles = [] # a list of all references, to be added later on the lines # A Function to add a rect with centered elided text 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 # A function to returns an item's width, by counting its children def itemWidth(item): if item.isFolder(): r = 0 for c in item.children(): r += itemWidth(c) return r or TEXT_WIDTH else: return TEXT_WIDTH def listItems(item, rect, level=0): delta = 0 for child in item.children(): w = itemWidth(child) if child.isFolder(): parent = addRectText(delta, w, rect, child.title(), level, tooltip=child.title()) parent.setToolTip(references.tooltip(references.textReference(child.ID()))) listItems(child, parent, level + 1) else: rectChild = addRectText(delta, TEXT_WIDTH, rect, "", level, tooltip=child.title()) rectChild.setToolTip(references.tooltip(references.textReference(child.ID()))) # Find tracked references in that scene (or parent folders) for ID, ref, name in trackedItems: result = [] c = child while c: result += references.findReferencesTo(ref, c, recursive=False) c = c.parent() if result: ref2 = result[0] # Create a RefCircle with the reference c = RefCircle(TEXT_WIDTH / 2, - CIRCLE_WIDTH / 2, CIRCLE_WIDTH, ID=ref2) # Store it, with the position of that item, to display it on the line later on refCircles.append((ref, c, rect.mapToItem(outline, rectChild.pos()))) delta += w listItems(root, outline) OUTLINE_WIDTH = itemWidth(root) # Add Plots i = 0 itemsRect = s.addRect(0, 0, 0, 0) itemsRect.setPos(0, MAX_LEVEL * LEVEL_HEIGHT + SPACING) for ID, ref, name in trackedItems: color = randomColor() # Rect r = QGraphicsRectItem(0, 0, TITLE_WIDTH, LINE_HEIGHT, itemsRect) r.setPen(QPen(Qt.NoPen)) r.setBrush(QBrush(color)) r.setPos(0, i * LINE_HEIGHT + i * SPACING) i += 1 # Text txt = QGraphicsSimpleTextItem(name, r) txt.setPos(r.boundingRect().center() - txt.boundingRect().center()) # Line line = PlotLine(0, 0, OUTLINE_WIDTH + SPACING, 0) line.setPos(TITLE_WIDTH, r.mapToScene(r.rect().center()).y()) s.addItem(line) line.setPen(QPen(color, 5)) line.setToolTip(self.tr("Plot: ") + name) # We add the circles / references to text, on the line for ref2, circle, pos in refCircles: if ref2 == ref: circle.setParentItem(line) circle.setPos(pos.x(), 0) # self.view.fitInView(0, 0, TOTAL_WIDTH, i * LINE_HEIGHT, Qt.KeepAspectRatioByExpanding) # KeepAspectRatio self.view.setSceneRect(0, 0, 0, 0)
class RectItemInserter(ItemInserter): def __init__(self, labeltool, scene, default_properties=None, prefix="", commit=True): ItemInserter.__init__(self, labeltool, scene, default_properties, prefix, commit) self._aiming = True self._helpLines = None self._helpLinesPen = QPen(Qt.green, 2, Qt.DashLine) self._init_pos = None def mousePressEvent(self, event, image_item): self._aiming = False if self._helpLines is not None and self._helpLines.scene() is not None: self._scene.removeItem(self._helpLines) self._helpLines = None pos = event.scenePos() self._init_pos = pos self._item = QGraphicsRectItem(QRectF(pos.x(), pos.y(), 0, 0)) self._item.setPen(self.pen()) self._scene.addItem(self._item) event.accept() def mouseMoveEvent(self, event, image_item): if self._aiming: if self._helpLines is not None and self._helpLines.scene( ) is not None: self._scene.removeItem(self._helpLines) self._helpLines = QGraphicsItemGroup() group = self._helpLines verticalHelpLine = QGraphicsLineItem(event.scenePos().x(), 0, event.scenePos().x(), self._scene.height()) horizontalHelpLine = QGraphicsLineItem(0, event.scenePos().y(), self._scene.width(), event.scenePos().y()) horizontalHelpLine.setPen(self._helpLinesPen) verticalHelpLine.setPen(self._helpLinesPen) group.addToGroup(verticalHelpLine) group.addToGroup(horizontalHelpLine) self._scene.addItem(self._helpLines) else: if self._item is not None: assert self._init_pos is not None rect = QRectF(self._init_pos, event.scenePos()).normalized() self._item.setRect(rect) event.accept() def mouseReleaseEvent(self, event, image_item): if self._item is not None: if self._item.rect().width() > 1 and \ self._item.rect().height() > 1: rect = self._item.rect() self._ann.update({ self._prefix + 'x': rect.x(), self._prefix + 'y': rect.y(), self._prefix + 'width': rect.width(), self._prefix + 'height': rect.height() }) self._ann.update(self._default_properties) if self._commit: image_item.addAnnotation(self._ann) if self._item is not None and self._item.scene() is not None: self._scene.removeItem(self._item) self.annotationFinished.emit() self._init_pos = None self._item = None self._aiming = True self._scene.views()[0].viewport().setCursor(Qt.CrossCursor) event.accept() def allowOutOfSceneEvents(self): return True def abort(self): if self._helpLines is not None and self._helpLines.scene() is not None: self._scene.removeItem(self._helpLines) self._helpLines = None if self._item is not None and self._item.scene() is not None: self._scene.removeItem(self._item) self._item = None self._init_pos = None ItemInserter.abort(self)
def refresh(self): if not self._mdlPlots or not self._mdlOutline or not self._mdlCharacter: return if not self.isVisible(): return LINE_HEIGHT = 18 SPACING = 3 TEXT_WIDTH = self.sldTxtSize.value() CIRCLE_WIDTH = 10 LEVEL_HEIGHT = 12 s = self.scene s.clear() # Get Max Level (max depth) root = self._mdlOutline.rootItem def maxLevel(item, level=0, max=0): if level > max: max = level for c in item.children(): m = maxLevel(c, level + 1) if m > max: max = m return max MAX_LEVEL = maxLevel(root) # Get the list of tracked items (array of references) trackedItems = [] if self.actPlots.isChecked(): trackedItems += self.plotReferences() if self.actCharacters.isChecked(): trackedItems += self.charactersReferences() ROWS_HEIGHT = len(trackedItems) * (LINE_HEIGHT + SPACING) fm = QFontMetrics(s.font()) max_name = 0 for ref in trackedItems: name = references.title(ref) max_name = max(fm.width(name), max_name) TITLE_WIDTH = max_name + 2 * SPACING # Add Folders and Texts outline = OutlineRect(0, 0, 0, ROWS_HEIGHT + SPACING + MAX_LEVEL * LEVEL_HEIGHT) s.addItem(outline) outline.setPos(TITLE_WIDTH + SPACING, 0) refCircles = [ ] # a list of all references, to be added later on the lines # A Function to add a rect with centered elided text 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 # A function to returns an item's width, by counting its children def itemWidth(item): if item.isFolder(): r = 0 for c in item.children(): r += itemWidth(c) return r or TEXT_WIDTH else: return TEXT_WIDTH def listItems(item, rect, level=0): delta = 0 for child in item.children(): w = itemWidth(child) if child.isFolder(): parent = addRectText(delta, w, rect, child.title(), level, tooltip=child.title()) parent.setToolTip( references.tooltip(references.textReference( child.ID()))) listItems(child, parent, level + 1) else: rectChild = addRectText(delta, TEXT_WIDTH, rect, "", level, tooltip=child.title()) rectChild.setToolTip( references.tooltip(references.textReference( child.ID()))) # Find tracked references in that scene (or parent folders) for ref in trackedItems: result = [] # Tests if POV scenePOV = False # Will hold true of character is POV of the current text, not containing folder if references.type(ref) == references.CharacterLetter: ID = references.ID(ref) c = child while c: if c.POV() == ID: result.append(c.ID()) if c == child: scenePOV = True c = c.parent() # Search in notes/references c = child while c: result += references.findReferencesTo( ref, c, recursive=False) c = c.parent() if result: ref2 = result[0] # Create a RefCircle with the reference c = RefCircle(TEXT_WIDTH / 2, -CIRCLE_WIDTH / 2, CIRCLE_WIDTH, ID=ref2, important=scenePOV) # Store it, with the position of that item, to display it on the line later on refCircles.append( (ref, c, rect.mapToItem(outline, rectChild.pos()))) delta += w listItems(root, outline) OUTLINE_WIDTH = itemWidth(root) # Add Tracked items i = 0 itemsRect = s.addRect(0, 0, 0, 0) itemsRect.setPos(0, MAX_LEVEL * LEVEL_HEIGHT + SPACING) # Set of colors for plots (as long as they don't have their own colors) colors = [ "#D97777", "#AE5F8C", "#D9A377", "#FFC2C2", "#FFDEC2", "#D2A0BC", "#7B0F0F", "#7B400F", "#620C3D", "#AA3939", "#AA6C39", "#882D61", "#4C0000", "#4C2200", "#3D0022", ] for ref in trackedItems: if references.type(ref) == references.CharacterLetter: color = self._mdlCharacter.getCharacterByID( references.ID(ref)).color() else: color = QColor(colors[i % len(colors)]) # Rect r = QGraphicsRectItem(0, 0, TITLE_WIDTH, LINE_HEIGHT, itemsRect) r.setPen(QPen(Qt.NoPen)) r.setBrush(QBrush(color)) r.setPos(0, i * LINE_HEIGHT + i * SPACING) r.setToolTip(references.tooltip(ref)) i += 1 # Text name = references.title(ref) txt = QGraphicsSimpleTextItem(name, r) txt.setPos(r.boundingRect().center() - txt.boundingRect().center()) # Line line = PlotLine(0, 0, OUTLINE_WIDTH + SPACING, 0) line.setPos(TITLE_WIDTH, r.mapToScene(r.rect().center()).y()) s.addItem(line) line.setPen(QPen(color, 5)) line.setToolTip(references.tooltip(ref)) # We add the circles / references to text, on the line for ref2, circle, pos in refCircles: if ref2 == ref: circle.setParentItem(line) circle.setPos(pos.x(), 0) # self.view.fitInView(0, 0, TOTAL_WIDTH, i * LINE_HEIGHT, Qt.KeepAspectRatioByExpanding) # KeepAspectRatio self.view.setSceneRect(0, 0, 0, 0)
class RectZoomMoveView(QChartView): """ Filter data to be displayed in rectangular body """ rangeSig = pyqtSignal(list) def __init__(self, parent=None): super(RectZoomMoveView, self).__init__(parent) self.setChart(QChart()) self.chart().setMargins(QMargins(5, 5, 5, 5)) self.chart().setContentsMargins(-10, -10, -10, -10) self.chart().setTitle(" ") self.relationState = True # Define two rectangles for background and drawing respectively self.parentRect = QGraphicsRectItem(self.chart()) self.parentRect.setFlag(QGraphicsItem.ItemClipsChildrenToShape, True) self.parentRect.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.RangeItem = RectRangeItem(parent=self.parentRect) self.RangeItem.setZValue(998) pen = QPen(Qt.gray) pen.setWidth(1) self.parentRect.setPen(pen) self.parentRect.setZValue(997) self.scene().addItem(self.parentRect) self.scene().addItem(self.RangeItem) self.RangeItem.hide() self.m_chartRectF = QRectF() self.m_rubberBandOrigin = QPointF(0, 0) self.dataLength = 0 self.RangeItem.selectedChange.connect(self.changeFromRectItem) self.BtnsWidget = ViewButtonsWidget(self) self.BtnsWidget.refreshBtn.clicked.connect(self.updateView) self.BtnsWidget.RelationSig.connect(self.setRelationState) self.BtnsWidget.dateRangeEdit.dateRangeSig.connect(self.changDateRect) def changDateRect(self, daterange): if self.chartTypes == "Bar": v = 3 else: v = 2 l = len(self.RangeItem.rangePoints) if l > 2: try: num = self.mintimeData.date().daysTo(daterange[0]) left = self.RangeItem.rangePoints[num] num = self.mintimeData.date().daysTo(daterange[1]) right = self.RangeItem.rangePoints[num] rect = self.chart().plotArea() rect.setLeft(left) rect.setRight(right) except: rect = self.chart().plotArea() self.RangeItem.setRect(rect) self.RangeItem.updateHandlesPos() else: try: num = self.mintimeData.date().daysTo(daterange[0]) left = self.RangeItem.rangePoints[num] num = self.mintimeData.date().daysTo(daterange[0]) right = self.RangeItem.rangePoints[num] rect = self.chart().plotArea() rect.setLeft(left) rect.setRight(right) except: rect = self.chart().plotArea() self.RangeItem.setRect(rect) self.RangeItem.updateHandlesPos() def lineSpace(self, start, end, num): res = [] if self.chartTypes == "Bar": step = (end - start) / (num) for i in range(num + 2): res.append(start + i * step) else: step = (end - start) / (num - 1) for i in range(num + 1): res.append(start + i * step) return res def getRangePoints(self): count = self.zoomSeries.count() rect = self.chart().plotArea() left = rect.left() right = rect.right() if count == 0: self.RangeItem.rangePoints = [left, right] else: # Get coordinate position for each node self.RangeItem.rangePoints = self.lineSpace(left, right, count) def setRangeColor(self, color): self.RangeItem.setRangeColor(color) def setRelationState(self, state): self.relationState = state def initSeries(self, chartTypes="Bar"): self.chartTypes = chartTypes axisX = QDateTimeAxis() axisX.setFormat("yyyy-MM-dd") self.zoomSeries = QLineSeries(self.chart()) self.chart().addSeries(self.zoomSeries) self.chart().setAxisY(QValueAxis(), self.zoomSeries) self.chart().setAxisX(axisX, self.zoomSeries) self.initView() def clearAll(self): # Clear all series and axes self.chart().removeAllSeries() axess = self.chart().axes() for axes in axess: self.chart().removeAxis(axes) def setData(self, timeData, valueData, chartTypes="Bar"): axisX = QDateTimeAxis() axisX.setFormat("yyyy-MM-dd") if self.chartTypes == "Bar": # Clear all series self.clearAll() self.zoomSeries = QLineSeries(self.chart()) barSeries = QBarSeries(self.chart()) barset = QBarSet("data") barSeries.setBarWidth(0.8) barSeries.append(barset) for td, vd in zip(timeData, valueData): self.zoomSeries.append(td.toMSecsSinceEpoch(), vd) barset.append(valueData) self.zoomSeries.hide() self.chart().addSeries(self.zoomSeries) self.chart().addSeries(barSeries) self.chart().setAxisY(QValueAxis(), self.zoomSeries) axisX.setRange(min(timeData), max(timeData)) self.chart().setAxisX(axisX, self.zoomSeries) elif self.chartTypes == "Scatter": # Clear all series self.clearAll() self.zoomSeries = QLineSeries(self.chart()) scattSeries = QScatterSeries(self.chart()) scattSeries.setMarkerSize(8) for td, vd in zip(timeData, valueData): self.zoomSeries.append(td.toMSecsSinceEpoch(), vd) scattSeries.append(td.toMSecsSinceEpoch(), vd) self.zoomSeries.hide() self.chart().addSeries(self.zoomSeries) self.chart().addSeries(scattSeries) self.chart().setAxisY(QValueAxis(), self.zoomSeries) axisX.setRange(min(timeData), max(timeData)) self.chart().setAxisX(axisX, self.zoomSeries) elif self.chartTypes in ["Line", "PLine"]: self.clearAll() if self.chartTypes == "Line": self.zoomSeries = QLineSeries(self.chart()) else: self.zoomSeries = QSplineSeries(self.chart()) for td, vd in zip(timeData, valueData): self.zoomSeries.append(td.toMSecsSinceEpoch(), vd) self.chart().addSeries(self.zoomSeries) self.chart().setAxisY(QValueAxis(), self.zoomSeries) axisX.setRange(min(timeData), max(timeData)) self.chart().setAxisX(axisX, self.zoomSeries) elif self.chartTypes == "Area": self.clearAll() self.zoomSeries = QLineSeries() self.zoomSeries.setColor(QColor("#666666")) for td, vd in zip(timeData, valueData): self.zoomSeries.append(td.toMSecsSinceEpoch(), vd) areaSeries = QAreaSeries(self.zoomSeries, None) self.chart().addSeries(self.zoomSeries) self.chart().addSeries(areaSeries) self.chart().setAxisY(QValueAxis(), areaSeries) axisX.setRange(min(timeData), max(timeData)) self.chart().setAxisX(axisX, areaSeries) self.zoomSeries.hide() self.mintimeData = min(timeData) self.maxtimeData = max(timeData) self.BtnsWidget.dateRangeEdit.setDateRange([ self.mintimeData.toString("yyyy-MM-dd"), self.maxtimeData.toString("yyyy-MM-dd"), ]) self.updateView() def resetView(self): rect = self.chart().plotArea() self.parentRect.setRect(rect) topRight = self.chart().plotArea().topRight() x = int(topRight.x()) y = int(topRight.y()) self.BtnsWidget.setGeometry(QRect(x - 420, 0, 420, 23)) self.RangeItem.setRect(rect) self.RangeItem.show() self.save_current_rubber_band() self.RangeItem.updateHandlesPos() self.apply_nice_numbers() self.getRangePoints() self.sendRang() def initView(self): self.RangeItem.hide() # Hide y-axis if self.chart().axisY(): self.chart().axisY().setVisible(False) if self.chart().axisX(): self.chart().axisX().setGridLineVisible(False) self.m_chartRectF = QRectF() self.m_rubberBandOrigin = QPointF(0, 0) self.getRangePoints() def updateView(self): self.RangeItem.hide() # Hide y-axis if self.chart().axisY(): self.chart().axisY().setVisible(False) if self.chart().axisX(): self.chart().axisX().setGridLineVisible(False) self.m_chartRectF = QRectF() self.m_rubberBandOrigin = QPointF(0, 0) self.resetView() # Map points to chart def point_to_chart(self, pnt): scene_point = self.mapToScene(pnt) chart_point = self.chart().mapToValue(scene_point) return chart_point # Map chart to points def chart_to_view_point(self, char_coord): scene_point = self.chart().mapToPosition(char_coord) view_point = self.mapFromScene(scene_point) return view_point # Save positions of rectangles def save_current_rubber_band(self): rect = self.RangeItem.rect() chart_top_left = self.point_to_chart(rect.topLeft().toPoint()) self.m_chartRectF.setTopLeft(chart_top_left) chart_bottom_right = self.point_to_chart(rect.bottomRight().toPoint()) self.m_chartRectF.setBottomRight(chart_bottom_right) # Respond to change in positions of rectangles def changeFromRectItem(self, rectIndex): self.save_current_rubber_band() self.sendRang(rectIndex) def sendRang(self, rectIndex=[]): if self.RangeItem.rect() != self.parentRect.rect(): self.BtnsWidget.setPalActive() else: self.BtnsWidget.setPalDisActive() if self.chartTypes == "Bar": v = 3 else: v = 2 if rectIndex == []: maxData = QDateTime.fromMSecsSinceEpoch( self.zoomSeries.at(len(self.RangeItem.rangePoints) - v).x()) minData = QDateTime.fromMSecsSinceEpoch(self.zoomSeries.at(0).x()) else: minData = max(rectIndex[0], 0) maxData = min(rectIndex[1], len(self.RangeItem.rangePoints) - v) minData = QDateTime.fromMSecsSinceEpoch( self.zoomSeries.at(minData).x()) maxData = QDateTime.fromMSecsSinceEpoch( self.zoomSeries.at(maxData).x()) if minData > maxData: if self.RangeItem.handleSelected is None: self.resetView() else: self.BtnsWidget.dateRangeEdit.setDate([ minData.toString("yyyy-MM-dd"), maxData.toString("yyyy-MM-dd") ]) if self.relationState: self.rangeSig.emit([ minData.toString("yyyy-MM-dd HH:mm:ss"), maxData.toString("yyyy-MM-dd HH:mm:ss"), ]) # Change positions of rectangles in scaling def resizeEvent(self, event): super().resizeEvent(event) rect = self.chart().plotArea() self.parentRect.setRect(rect) self.getRangePoints() topRight = self.chart().plotArea().topRight() x = int(topRight.x()) y = int(topRight.y()) self.BtnsWidget.setGeometry(QRect(x - 420, 0, 420, 23)) if self.RangeItem.isVisible(): self.restore_rubber_band() self.save_current_rubber_band() self.RangeItem.updateHandlesPos() else: self.RangeItem.setRect(self.parentRect.rect()) self.RangeItem.show() self.RangeItem.setRect(self.parentRect.rect()) self.save_current_rubber_band() self.RangeItem.updateHandlesPos() self.apply_nice_numbers() # Restore to original positions of rectangles def restore_rubber_band(self): view_top_left = self.chart_to_view_point(self.m_chartRectF.topLeft()) view_bottom_right = self.chart_to_view_point( self.m_chartRectF.bottomRight()) self.m_rubberBandOrigin = view_top_left height = self.chart().plotArea().height() rect = QRectF() rect.setTopLeft(view_top_left) rect.setBottomRight(view_bottom_right) rect.setHeight(height) self.RangeItem.setRect(rect) # Adjust display coordinates of axes automatically def apply_nice_numbers(self): axes_list = self.chart().axes() for value_axis in axes_list: if value_axis: pass
class OverlayGraphics(QGraphicsView): def __init__(self): super(OverlayGraphics, self).__init__() self.setStyleSheet("background:transparent") # ビューの背景透明化 self.overlayScene = QGraphicsScene() self.setScene(self.overlayScene) self.overlayScene.setSceneRect(QRectF(self.rect())) self.createItem() self.luRect = QRect() self.rbRect = QRect() self.isSelected = False self.setTargetMode(False) self.hide() # 初期状態は非表示 def createItem(self): # ターゲットマーカの作成 self.target_circle = QGraphicsEllipseItem( QtCore.QRectF(-10, -10, 20, 20)) self.target_circle.setBrush(QBrush(Qt.red)) self.target_circle.setPen(QPen(Qt.black)) self.overlayScene.addItem(self.target_circle) self.setTargetMode(False) # モーダルの作成:モーダルはターゲット位置に追従する self.pop_rect = QGraphicsRectItem(QtCore.QRectF(0, 0, 100, 60), self.target_circle) self.pop_rect.setBrush(QBrush(Qt.gray)) self.pop_rect.setPen(QPen(Qt.gray)) self.pop_rect.setOpacity(0.8) # 透明度を設定 self.operate_text = QGraphicsSimpleTextItem("", self.pop_rect) self.operate_text.setScale(1.7) self.sub_operate_text = QGraphicsSimpleTextItem("", self.pop_rect) self.sub_operate_text.setScale(1.7) self.setTargetPos(400, 180, DirectionEnum.VERTICAL.value) # オーバレイヤのサイズが変わると呼び出される.シーンのサイズをビューの大きさに追従(-5 はマージン) def resizeEvent(self, event): self.overlayScene.setSceneRect( QRectF(0, 0, self.size().width() - 5, self.size().height() - 5)) # ウィンドウ左上からの位置(画面位置からウインドウ左上の位置を引く) def setTargetPos(self, x_pos, y_pos, direction): self.target_circle.setPos(QPointF(x_pos, y_pos)) self.setPopPos(direction) def setPopTextPos(self, text1, text2): lentext1 = len(text1) lentext2 = len(text2) if lentext2 == 0: self.operate_text.setPos( (self.pop_rect.rect().size().width() / 2) - (lentext1 / 2 * 14), 15) else: self.operate_text.setPos( (self.pop_rect.rect().size().width() / 2) - (lentext1 / 2 * 14), 5) self.sub_operate_text.setPos( (self.pop_rect.rect().size().width() / 2) - (lentext2 / 2 * 14), 30) def setPopPos(self, direction): if direction == DirectionEnum.VERTICAL.value: # モーダルを右に表示しきれない場合に左に表示 if self.target_circle.pos().x() > self.overlayScene.width( ) - self.pop_rect.rect().size().width() * 1.5: self.pop_rect.setPos( -self.pop_rect.rect().size().width() * 1.5, -self.pop_rect.rect().size().height() / 2) else: # 右に表示 self.pop_rect.setPos(self.pop_rect.rect().size().width() / 2, -self.pop_rect.rect().size().height() / 2) else: # モーダルを下に表示しきれない場合に上に表示 if self.target_circle.pos().y() > self.overlayScene.height( ) - self.pop_rect.rect().size().height() * 1.5: self.pop_rect.setPos( -self.pop_rect.rect().size().width() / 2, -self.pop_rect.rect().size().height() * 1.5) else: # 下に表示 self.pop_rect.setPos(-self.pop_rect.rect().size().width() / 2, self.pop_rect.rect().size().height() / 2) def setTargetMode(self, active): self.targetMode = active if active: self.target_circle.setRect(-10, -10, 20, 20) else: # ターゲットを非表示にする self.target_circle.setRect(0, 0, 0, 0) # def targetVisible(self, visible): # if visible: # self.target_circle.setRect(-10, -10, 20, 20) # else: # self.target_circle.setRect(0, 0, 0, 0) def feedbackShow(self, text1, text2, direction): if self.isSelected: self.operate_text.setText(text1) self.sub_operate_text.setText(text2) # ターゲットモードがアクティブでないとき,ターゲットマーカの位置は選択セルに依存 if not self.targetMode: if direction == DirectionEnum.HORIZON.value: x_pos = (self.luRect.left() + self.rbRect.right()) / 2 + 20 y_pos = self.rbRect.bottom( ) - self.rbRect.height() / 2 + 20 else: x_pos = self.rbRect.right() - self.rbRect.width() / 2 + 20 y_pos = (self.luRect.top() + self.rbRect.bottom()) / 2 + 20 self.setTargetPos(x_pos, y_pos, direction) self.setPopTextPos(text1, text2) self.show()
class VideoScene(QGraphicsScene): regionSelected = Signal(QRect) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.videoItem = QGraphicsVideoItem() self.videoItem.setPos(0, 0) self.videoItem.nativeSizeChanged.connect(self.videoSizeChanged) self.addItem(self.videoItem) self.rectangle = QGraphicsRectItem() self.rectangle.setPen(QColor(0, 122, 217)) self.addItem(self.rectangle) def videoSizeChanged(self, size: QSizeF): logging.debug("%s", size) if not size.isEmpty(): self.videoItem.setSize(size) self.setSceneRect(self.videoItem.boundingRect()) self.clearSelection() def clearSelection(self) -> None: self.rectangle.setRect(QRectF()) self.regionSelected.emit(self.selection) @property def selection(self) -> QRect: return self.rectangle.rect().toRect() @staticmethod def coerceInside(point: QPointF, rect: QRectF): return QPointF( min(max(point.x(), rect.left()), rect.right()), min(max(point.y(), rect.top()), rect.bottom()), ) def coerceInsideVideo(self, point: QPointF): return self.coerceInside(point, self.videoItem.boundingRect()) def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: self.startPos = self.coerceInsideVideo(event.scenePos()) return super().mousePressEvent(event) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: endPos = self.coerceInsideVideo(event.scenePos()) self.rectangle.setRect(QRectF(self.startPos, endPos).normalized()) return super().mouseMoveEvent(event) def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: logging.info("Selection: %s", self.selection) super().mouseReleaseEvent(event) self.regionSelected.emit(self.selection) def paintGrid(self, painter: QPainter, rect: QRectF, gridSize: int): left, top, right, bottom = rect.getCoords() x_min = (int(left - 1) // gridSize + 1) * gridSize y_min = (int(top - 1) // gridSize + 1) * gridSize x_max = (int(right) // gridSize) * gridSize y_max = (int(bottom) // gridSize) * gridSize # logging.debug( # "x=(%s<=%s..%s<=%s) y=(%s<=%s..%s<=%s)", # *(left, x_min, x_max, right), # *(top, y_min, y_max, bottom) # ) painter.setPen(QPen(Qt.gray, 2)) painter.drawLines(QLineF(0, top, 0, bottom), QLineF(left, 0, right, 0)) painter.setPen(QPen(Qt.gray, 1)) painter.drawLines( QLineF(x, top, x, bottom) for x in range(x_min, x_max + 1, gridSize) if x) painter.drawLines( QLineF(left, y, right, y) for y in range(y_min, y_max + 1, gridSize) if y) def drawBackground(self, painter: QPainter, rect: QRectF) -> None: super().drawBackground(painter, rect) self.paintGrid(painter, rect, 20)
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 NodeTemplateItem(): ''' This represents one node template on the diagram. A node template can be on many diagrams This class creates the rectangle graphics item and the text graphics item and adds them to the scene. ''' def __init__(self, scene, x, y, nodeTemplateDict=None, NZID=None): self.scene = scene self.logMsg = None self.x = x self.y = y self.nodeTemplateDict = nodeTemplateDict # self.name = self.nodeTemplateDict.get("name", "") THIS HAS BEEN REPLACED BY THE name FUNCTION - SEE BELOW self.diagramType = "Node Template" self.displayText = None self.model = self.scene.parent.model self.gap = 100 self.relList = [] # assign a unique key if it doesn't already have one if NZID == None: self.NZID = str(uuid.uuid4()) else: self.NZID = NZID # init graphics objects to none self.TNode = None self.TNtext = None # draw the node template on the diagram self.drawIt() def name(self, ): return self.nodeTemplateDict.get("name", "") def getX(self, ): return self.TNode.boundingRect().x() def getY(self, ): return self.TNode.boundingRect().y() def getHeight(self, ): return self.TNode.boundingRect().height() def getWidth(self, ): return self.TNode.boundingRect().width() def getRelList(self, ): '''return a list of all relationitems that are inbound or outbound from this node template. do not include self referencing relationships ''' return [ diagramItem for key, diagramItem in self.scene.parent.itemDict.items() if diagramItem.diagramType == "Relationship Template" and ( diagramItem.startNZID == self.NZID or diagramItem.endNZID == self.NZID) ] def getPoint(self, offset=None): ''' This function is used by the template diagram to calculate the location to drop a node template on the diagram ''' if offset is None: return QPointF(self.x, self.y) else: return QPointF(self.x + offset, self.y + offset) def getFormat(self, ): ''' determine if the Node Template has a template format or should use the project default format ''' # get the node Template custom format customFormat = self.nodeTemplateDict.get("TNformat", None) if not customFormat is None: # get the template custom format self.nodeFormat = TNodeFormat(formatDict=customFormat) else: # get the project default format self.nodeFormat = TNodeFormat( formatDict=self.model.modelData["TNformat"]) def clearItem(self, ): if (not self.TNode is None and not self.TNode.scene() is None): self.TNode.scene().removeItem(self.TNode) if (not self.TNtext is None and not self.TNtext.scene() is None): self.TNtext.scene().removeItem(self.TNtext) def drawIt(self, ): # get current format as it may have changed self.getFormat() # create the qgraphicsItems if they don't exist if self.TNode is None: # create the rectangle self.TNode = QGraphicsRectItem(QRectF( self.x, self.y, self.nodeFormat.formatDict["nodeWidth"], self.nodeFormat.formatDict["nodeHeight"]), parent=None) self.TNode.setZValue(NODELAYER) self.TNode.setFlag(QGraphicsItem.ItemIsMovable, True) self.TNode.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.TNode.setFlag(QGraphicsItem.ItemIsSelectable, True) self.TNode.setSelected(True) self.TNode.setData(1, self.NZID) # get with self.INode.data(1) self.TNode.setData(ITEMTYPE, NODETEMPLATE) # create the text box self.TNtext = QGraphicsTextItem("", parent=None) self.TNtext.setPos(self.x, self.y) self.TNtext.setFlag(QGraphicsItem.ItemIsMovable, True) self.TNtext.setFlag(QGraphicsItem.ItemIsSelectable, False) self.TNtext.setData(NODEID, self.NZID) self.TNtext.setData(ITEMTYPE, NODETEMPLATETEXT) self.TNtext.setZValue(NODELAYER) # save the location self.x = self.TNode.sceneBoundingRect().x() self.y = self.TNode.sceneBoundingRect().y() # generate the html and resize the rectangle self.formatItem() # add the graphics items to the scene self.scene.addItem(self.TNode) self.scene.addItem(self.TNtext) else: # generate the html and resize the rectangle self.formatItem() def formatItem(self, ): # configure the formatting aspects of the qgraphics item pen = self.nodeFormat.pen() brush = self.nodeFormat.brush() self.TNode.setBrush(brush) self.TNode.setPen(pen) # generate the HTML genHTML = self.generateHTML() self.TNtext.prepareGeometryChange() # print("before html bounding rectangle width:{}".format(self.TNtext.boundingRect().width())) # print("before html text width:{}".format(self.TNtext.textWidth())) self.TNtext.setTextWidth( -1 ) # reset the width to unkonwn so it will calculate a new width based on the new html self.TNtext.setHtml(genHTML) # print("after html bounding rectangle width:{}".format(self.TNtext.boundingRect().width())) # print("after html text width:{}".format(self.TNtext.textWidth())) # make sure minimum width of 120 if self.TNtext.boundingRect().width() < 120: self.TNtext.setTextWidth(120) else: self.TNtext.setTextWidth( self.TNtext.boundingRect().width() ) # you have to do a setTextWidth to get the html to render correctly. # set the rectangle item to the same size as the formatted html self.TNode.prepareGeometryChange() currentRect = self.TNode.rect() # insure minimum height of 120 if self.TNtext.boundingRect().height() < 120: currentRect.setHeight(120) else: currentRect.setHeight(self.TNtext.boundingRect().height()) currentRect.setWidth(self.TNtext.boundingRect().width()) self.TNode.setRect(currentRect) def generateHTML(self, ): ''' Generate the HTML that formats the node template data inside the rectangle ''' # generate the html prefix = "<!DOCTYPE html><html><body>" # head = "<head><style>table, th, td {border: 1px solid black; border-collapse: collapse;}</style></head>" suffix = "</body></html>" # blankRow = "<tr><td><left>{}</left></td><td><left>{}</left></td><td><left>{}</left></td><td><left>{}</left></td></tr>".format("", "", "", "") name = "<center><b>{}</b></center>".format( self.nodeTemplateDict.get("name", "")) lbls = self.genLblHTML() props = self.genPropHTML() genHTML = "{}{}<hr>{}<br><hr>{}{}".format(prefix, name, lbls, props, suffix) # print("{} html: {}".format(self.name(), genHTML)) return genHTML def genLblHTML(self): # html = '<table width="90%">' html = '<table style="width:90%;border:1px solid black;">' if len(self.nodeTemplateDict.get("labels", [])) > 0: for lbl in self.nodeTemplateDict.get("labels", []): if lbl[NODEKEY] == Qt.Checked: nk = "NK" else: nk = " " if lbl[REQUIRED] == Qt.Checked: rq = "R" else: rq = "" html = html + '<tr align="left"><td width="15%"><left>{}</left></td><td width="65%"><left>{}</left></td><td width="10%"><left>{}</left></td><td width="10%"><left>{}</left></td></tr>'.format( nk, lbl[LABEL], "", rq) html = html + "</table>" else: html = '<tr align="left"><td width="15%"><left>{}</left></td><td width="65%"><left>{}</left></td><td width="10%"><left>{}</left></td><td width="10%"><left>{}</left></td></tr>'.format( " ", "NO{}LABELS".format(" "), "", "") html = html + "</table>" return html def genPropHTML(self): # PROPERTY, DATATYPE, PROPREQ, DEFAULT, EXISTS, UNIQUE, PROPNODEKEY html = '<table style="width:90%;border:1px solid black;">' if len(self.nodeTemplateDict.get("properties", [])) > 0: for prop in self.nodeTemplateDict.get("properties", []): if prop[PROPNODEKEY] == Qt.Checked: nk = "NK" else: nk = " " if prop[PROPREQ] == Qt.Checked: rq = "R" else: rq = "" if prop[EXISTS] == Qt.Checked: ex = "E" else: ex = "" if prop[UNIQUE] == Qt.Checked: uq = "U" else: uq = "" html = html + '<tr align="left"><td width="15%"><left>{}</left></td><td width="65%"><left>{}</left></td><td width="10%"><left>{}</left></td><td width="10%"><left>{}</left></td><td width="10%"><left>{}</left></td></tr>'.format( nk, prop[PROPERTY], rq, ex, uq) html = html + "</table>" else: html = html + '<tr align="left"><td width="15%"><left>{}</left></td><td width="65%"><left>{}</left></td><td width="10%"><left>{}</left></td><td width="10%"><left>{}</left></td></tr>'.format( " ", "NO{}PROPERTIES".format(" "), "", "", "") html = html + "</table>" return html def moveIt(self, dx, dy): ''' Move the node rectangle and the node textbox to the delta x,y coordinate. ''' # print("before moveIt: sceneboundingrect {} ".format( self.INode.sceneBoundingRect())) self.TNode.moveBy(dx, dy) self.x = self.TNode.sceneBoundingRect().x() self.y = self.TNode.sceneBoundingRect().y() self.TNtext.moveBy(dx, dy) # print("after moveIt: sceneboundingrect {} ".format( self.INode.sceneBoundingRect())) # now redraw all the relationships self.drawRels() def drawRels(self, ): '''Redraw all the relationship lines connected to the Node Template Rectangle''' # get a list of the relationship items connected to this node template self.relList = self.getRelList() # assign the correct inbound/outbound side for the rel for rel in self.relList: if rel.endNodeItem.NZID != rel.startNodeItem.NZID: # ignore bunny ears rel.assignSide() # get a set of all the nodes and sides involved nodeSet = set() for rel in self.relList: if rel.endNodeItem.NZID != rel.startNodeItem.NZID: # ignore bunny ears nodeSet.add((rel.endNodeItem, rel.inboundSide)) nodeSet.add((rel.startNodeItem, rel.outboundSide)) # tell each node side to assign rel locations for nodeSide in nodeSet: nodeSide[0].assignPoint(nodeSide[1]) ############################################ # now tell them all to redraw for rel in self.relList: rel.drawIt2() def calcOffset(self, index, totRels): offset = [-60, -40, -20, 0, 20, 40, 60] offsetStart = [3, 2, 2, 1, 1, 0, 0] if totRels > 7: totRels = 7 return offset[offsetStart[totRels - 1] + index] def assignPoint(self, side): # go through all the rels on a side and assign their x,y coord for that side self.relList = self.getRelList() sideList = [ rel for rel in self.relList if ((rel.startNZID == self.NZID and rel.outboundSide == side) or ( rel.endNZID == self.NZID and rel.inboundSide == side)) ] totRels = len(sideList) if totRels > 0: if side == R: # calc center of the side x = self.x + self.getWidth() y = self.y + self.getHeight() / 2 # sort the rels connected to this side by the y value sideList.sort(key=self.getSortY) # assign each of them a position on the side starting in the center and working out in both directions for index, rel in enumerate(sideList): if rel.startNZID == self.NZID: rel.outboundPoint = QPointF( x, y + (self.calcOffset(index, totRels))) if rel.endNZID == self.NZID: rel.inboundPoint = QPointF( x, y + (self.calcOffset(index, totRels))) elif side == L: x = self.x y = self.y + self.getHeight() / 2 sideList.sort(key=self.getSortY) for index, rel in enumerate(sideList): if rel.startNZID == self.NZID: rel.outboundPoint = QPointF( x, y + (self.calcOffset(index, totRels))) if rel.endNZID == self.NZID: rel.inboundPoint = QPointF( x, y + (self.calcOffset(index, totRels))) elif side == TOP: x = self.x + self.getWidth() / 2 y = self.y sideList.sort(key=self.getSortX) for index, rel in enumerate(sideList): if rel.startNZID == self.NZID: rel.outboundPoint = QPointF( x + (self.calcOffset(index, totRels)), y) if rel.endNZID == self.NZID: rel.inboundPoint = QPointF( x + (self.calcOffset(index, totRels)), y) elif side == BOTTOM: x = self.x + self.getWidth() / 2 y = self.y + self.getHeight() sideList.sort(key=self.getSortX) for index, rel in enumerate(sideList): if rel.startNZID == self.NZID: rel.outboundPoint = QPointF( x + (self.calcOffset(index, totRels)), y) if rel.endNZID == self.NZID: rel.inboundPoint = QPointF( x + (self.calcOffset(index, totRels)), y) else: print("error, no side") def getSortY(self, rel): # if this node is the start node then return the end node's Y if rel.startNZID == self.NZID: return rel.endNodeItem.TNode.sceneBoundingRect().center().y() # if this node is the end node then return the start node's Y if rel.endNZID == self.NZID: return rel.startNodeItem.TNode.sceneBoundingRect().center().y() # this should never happen return 0 def getSortX(self, rel): # if this node is the start node then return the end node's X if rel.startNZID == self.NZID: return rel.endNodeItem.TNode.sceneBoundingRect().center().x() # if this node is the end node then return the start node's X if rel.endNZID == self.NZID: return rel.startNodeItem.TNode.sceneBoundingRect().center().x() # this should never happen return 0 def getObjectDict(self, ): ''' This function returns a dictionary with all the data that represents this node template item. The dictionary is added to the Instance Diagram dictionary.''' objectDict = {} objectDict["NZID"] = self.NZID objectDict["name"] = self.nodeTemplateDict.get("name", "") objectDict["displayText"] = self.displayText objectDict["x"] = self.TNode.sceneBoundingRect().x() objectDict["y"] = self.TNode.sceneBoundingRect().y() objectDict["diagramType"] = self.diagramType objectDict["labels"] = self.nodeTemplateDict.get("labels", []) objectDict["properties"] = self.nodeTemplateDict.get("properties", []) return objectDict def setLogMethod(self, logMethod=None): if logMethod is None: if self.logMsg is None: self.logMsg = self.noLog else: self.logMsg = logMethod def noLog(self, msg): return
def refresh(self): if not self._mdlPlots or not self._mdlOutline or not self._mdlCharacter: return if not self.isVisible(): return LINE_HEIGHT = 18 SPACING = 3 TEXT_WIDTH = self.sldTxtSize.value() CIRCLE_WIDTH = 10 LEVEL_HEIGHT = 12 s = self.scene s.clear() # Get Max Level (max depth) root = self._mdlOutline.rootItem def maxLevel(item, level=0, max=0): if level > max: max = level for c in item.children(): m = maxLevel(c, level + 1) if m > max: max = m return max MAX_LEVEL = maxLevel(root) # Get the list of tracked items (array of references) trackedItems = [] if self.actPlots.isChecked(): trackedItems += self.plotReferences() if self.actCharacters.isChecked(): trackedItems += self.charactersReferences() ROWS_HEIGHT = len(trackedItems) * (LINE_HEIGHT + SPACING ) fm = QFontMetrics(s.font()) max_name = 0 for ref in trackedItems: name = references.title(ref) max_name = max(fm.width(name), max_name) TITLE_WIDTH = max_name + 2 * SPACING # Add Folders and Texts outline = OutlineRect(0, 0, 0, ROWS_HEIGHT + SPACING + MAX_LEVEL * LEVEL_HEIGHT) s.addItem(outline) outline.setPos(TITLE_WIDTH + SPACING, 0) refCircles = [] # a list of all references, to be added later on the lines # A Function to add a rect with centered elided text 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 # A function to returns an item's width, by counting its children def itemWidth(item): if item.isFolder(): r = 0 for c in item.children(): r += itemWidth(c) return r or TEXT_WIDTH else: return TEXT_WIDTH def listItems(item, rect, level=0): delta = 0 for child in item.children(): w = itemWidth(child) if child.isFolder(): parent = addRectText(delta, w, rect, child.title(), level, tooltip=child.title()) parent.setToolTip(references.tooltip(references.textReference(child.ID()))) listItems(child, parent, level + 1) else: rectChild = addRectText(delta, TEXT_WIDTH, rect, "", level, tooltip=child.title()) rectChild.setToolTip(references.tooltip(references.textReference(child.ID()))) # Find tracked references in that scene (or parent folders) for ref in trackedItems: result = [] # Tests if POV scenePOV = False # Will hold true of character is POV of the current text, not containing folder if references.type(ref) == references.CharacterLetter: ID = references.ID(ref) c = child while c: if c.POV() == ID: result.append(c.ID()) if c == child: scenePOV = True c = c.parent() # Search in notes/references c = child while c: result += references.findReferencesTo(ref, c, recursive=False) c = c.parent() if result: ref2 = result[0] # Create a RefCircle with the reference c = RefCircle(TEXT_WIDTH / 2, - CIRCLE_WIDTH / 2, CIRCLE_WIDTH, ID=ref2, important=scenePOV) # Store it, with the position of that item, to display it on the line later on refCircles.append((ref, c, rect.mapToItem(outline, rectChild.pos()))) delta += w listItems(root, outline) OUTLINE_WIDTH = itemWidth(root) # Add Tracked items i = 0 itemsRect = s.addRect(0, 0, 0, 0) itemsRect.setPos(0, MAX_LEVEL * LEVEL_HEIGHT + SPACING) # Set of colors for plots (as long as they don't have their own colors) colors = [ "#D97777", "#AE5F8C", "#D9A377", "#FFC2C2", "#FFDEC2", "#D2A0BC", "#7B0F0F", "#7B400F", "#620C3D", "#AA3939", "#AA6C39", "#882D61", "#4C0000", "#4C2200", "#3D0022", ] for ref in trackedItems: if references.type(ref) == references.CharacterLetter: color = self._mdlCharacter.getCharacterByID(references.ID(ref)).color() else: color = QColor(colors[i % len(colors)]) # Rect r = QGraphicsRectItem(0, 0, TITLE_WIDTH, LINE_HEIGHT, itemsRect) r.setPen(QPen(Qt.NoPen)) r.setBrush(QBrush(color)) r.setPos(0, i * LINE_HEIGHT + i * SPACING) r.setToolTip(references.tooltip(ref)) i += 1 # Text name = references.title(ref) txt = QGraphicsSimpleTextItem(name, r) txt.setPos(r.boundingRect().center() - txt.boundingRect().center()) # Line line = PlotLine(0, 0, OUTLINE_WIDTH + SPACING, 0) line.setPos(TITLE_WIDTH, r.mapToScene(r.rect().center()).y()) s.addItem(line) line.setPen(QPen(color, 5)) line.setToolTip(references.tooltip(ref)) # We add the circles / references to text, on the line for ref2, circle, pos in refCircles: if ref2 == ref: circle.setParentItem(line) circle.setPos(pos.x(), 0) # self.view.fitInView(0, 0, TOTAL_WIDTH, i * LINE_HEIGHT, Qt.KeepAspectRatioByExpanding) # KeepAspectRatio self.view.setSceneRect(0, 0, 0, 0)
QGraphicsItem, QGraphicsRectItem, QGraphicsTextItem, QGraphicsEllipseItem import sys from random import random app = QApplication(sys.argv) # definition de la scène scene = QGraphicsScene() rectGris = QGraphicsRectItem(0., 0., 200., 150.) rectGris.setBrush(QBrush(Qt.lightGray)) scene.addItem(rectGris) # ou (equivalent): # rectGris = scene.addRect(0.,0.,200.,150.,brush=QBrush(Qt.lightGray)) texte = QGraphicsTextItem("Tous en scène !") dy = rectGris.rect().height() - texte.sceneBoundingRect().height() texte.setPos(rectGris.x(), rectGris.y() + dy) texte.setDefaultTextColor(Qt.blue) scene.addItem(texte) d = 48. # diametre smiley ox = 4. # largeur oeil oy = 6. # hauteur oeil smiley = QGraphicsEllipseItem(-d / 2, -d / 2, d, d) smiley.setBrush(QBrush(Qt.yellow)) yeux = [QGraphicsEllipseItem(-ox/2.,-oy/2.,ox,oy,parent=smiley) \ for _ in range(2)] yeux[0].setPos(-d / 6, -d / 8) yeux[1].setPos(+d / 6, -d / 8) brush = QBrush(Qt.black) for oeil in yeux:
class BankNodeItem (NodeItem): maincolor = FlPalette.bank altcolor = FlPalette.bankvar label = "%s Bank" def __init__ (self, nodeobj, parent=None, view=None, state=1): super().__init__(nodeobj, parent, view, state) self.rect = QRectF() self.setZValue(-1) self.updatecomment() self.updatebanktype() self.updatebankmode() def nudgechildren(self): super().nudgechildren() for sub in self.sublist(): sub.setrank(self) def sublist (self): ID = self.nodeobj.ID itemtable = self.view.itemtable if self.state == 1 and ID in itemtable and not self.iscollapsed(): children = [] for child in self.nodeobj.subnodes: if child in itemtable[ID]: item = itemtable[ID][child] else: continue if item.state > -1: children.append(item) return children else: return [] def treeposition (self, ranks=None): self.updatelayout(external=True) return super().treeposition(ranks) def graphicsetup (self): super().graphicsetup() darkbrush = QBrush(FlPalette.bg) nopen = QPen(0) viewport = self.view.viewport() self.btypeicon = QGraphicsPixmapItemCond(self.icon, self, viewport) self.btypeicon.setPos(self.iconx-self.style.itemmargin-self.iwidth, self.style.itemmargin) self.iconx = self.btypeicon.x() self.fggroup.addToGroup(self.btypeicon) self.centerbox = QGraphicsRectItem(self) self.centerbox.setCacheMode(QGraphicsItem.DeviceCoordinateCache) self.centerbox.setRect(QRectF()) self.centerbox.setBrush(darkbrush) self.centerbox.setPen(nopen) self.centerbox.setPos(0, self.nodelabel.y()+self.nodelabel.boundingRect().height()+self.style.itemmargin*2) self.fggroup.addToGroup(self.centerbox) def updatebanktype (self): types = {"talk": "(T)", "response": "(R)", "": ""} self.nodelabel.setText("%s Bank %s" % (self.realid(), types[self.nodeobj.banktype])) def updatebankmode (self): icons = {"First": "bank-first", "All": "bank-all", "Append": "bank-append", "": "blank"} pixmap = self.pixmap("images/%s.png" % icons[self.nodeobj.bankmode]) self.btypeicon.setPixmap(pixmap) if self.nodeobj.bankmode: self.btypeicon.setToolTip("Bank mode: %s" % self.nodeobj.bankmode) else: self.btypeicon.setToolTip("") def updatecenterbox (self): verticalpos = self.centerbox.y() maxwidth = self.style.nodetextwidth subnodes = self.sublist() for subnode in subnodes: if subnode.nodeobj.typename == "bank": subnode.updatelayout(external=True) noderect = subnode.boundingRect() nodeheight = noderect.height() nodewidth = noderect.width() subnode.show() subnode.yoffset = self.mapToScene(0,verticalpos + nodeheight/2+self.style.activemargin).y()-self.y_bottom() verticalpos += nodeheight+self.style.activemargin*2 maxwidth = max(maxwidth, nodewidth) centerrect = self.centerbox.rect() centerrect.setWidth(maxwidth+self.style.selectmargin*2) centerrect.setHeight(verticalpos-self.centerbox.y()) self.centerbox.setRect(centerrect) centerrect = self.centerbox.mapRectToParent(centerrect) self.comment.setY(centerrect.bottom()+self.style.itemmargin) def updatelayout (self, external=False): subnodes = self.sublist() if self.iscollapsed(): rect = self.nodelabel.mapRectToParent(self.nodelabel.boundingRect()) else: self.updatecenterbox() rect = self.fggroup.childrenBoundingRect() mainrect = rect.marginsAdded(self.style.banknodemargins) self.mainbox.setRect(mainrect) self.shadowbox.setRect(mainrect) self.selectbox.setRect(mainrect.marginsAdded(self.style.selectmargins)) activerect = mainrect.marginsAdded(self.style.activemargins) self.activebox.setRect(activerect) oldypos = self.centerbox.mapToScene(self.centerbox.pos()).y() self.graphgroup.setPos(-activerect.width()//2-activerect.x(), -activerect.height()//2-activerect.y()) newypos = self.centerbox.mapToScene(self.centerbox.pos()).y() for subnode in subnodes: subnode.yoffset += newypos - oldypos subnode.setY(self.y()) self.prepareGeometryChange() self.rect = self.graphgroup.mapRectToParent(mainrect) if not external: self.view.updatelayout() def setY (self, y): super().setY(y) for subnode in self.sublist(): subnode.setY(y) def contextMenuEvent (self, event): menu = QMenu() if self.isselected(): window = FlGlob.mainwindow menu.addAction(window.actions["collapse"]) if self.isghost(): menu.addAction(window.actions["selectreal"]) menu.addAction(window.actions["copynode"]) menu.addMenu(window.subnodemenu) menu.addMenu(window.addmenu) menu.addAction(window.actions["moveup"]) menu.addAction(window.actions["movedown"]) menu.addAction(window.actions["parentswap"]) menu.addAction(window.actions["unlinknode"]) menu.addAction(window.actions["unlinkstree"]) menu.addAction(window.actions["settemplate"]) menu.addMenu(window.transformmenu) if not menu.isEmpty(): menu.exec_(event.screenPos())
class Checkpoint(VisualizerGraphicItem): def __init__(self, ID=0, x=0, y=0): super(self.__class__, self).__init__(ID, x, y) self._kind_name = 'checkpoint' self._ids = {} self._graphics_item = QGraphicsRectItem(self) self._text = QGraphicsTextItem(self._graphics_item) self._shine = False def set_rect(self, rect): scale = config.get('display', 'id_font_scale') bold = config.get('display', 'id_font_bold') font = QFont('', rect.width() * 0.08 * scale) self._text.setFont(font) self._text.setPos(rect.x(), rect.y() + 0.6 * rect.height()) self._text.setDefaultTextColor( QColor(config.get('display', 'id_font_color'))) if self._display_mode == 0: ss = '' if bold: ss = '<b>' for key in self._ids: count = 0 for ii in self._ids[key]: if count == 0: ss = ss + '(' + key + ': ' + ii[0] else: ss = ss + ', ' + ii[0] count += 1 ss = ss + ')\n' if bold: ss += '</b>' self._text.setHtml(ss) self._graphics_item.setRect(rect.x(), rect.y(), rect.width(), rect.height()) elif self._display_mode == 1: self._text.setPlainText('') self._graphics_item.setRect(rect.x(), rect.y(), rect.width(), rect.height()) def do_action(self, time_step): self._shine = False return def undo_action(self, time_step): self._shine = False return def visit(self): self._shine = True def determine_color(self, number, count, pattern=None): color = None if len(self._ids) == 1: for key in self._ids: color_id = self._ids[key][0][1] + 1 if color_id < len(self._colors): color = self._colors[color_id] if color is None: color = self._colors[0] if self._shine: color = calculate_color(color, QColor(255, 255, 255), 0.4) self._graphics_item.setBrush(QBrush(color)) def parse_init_value(self, name, value): result = super(self.__class__, self).parse_init_value(name, value) if result != 1: return result if name == 'checkpoint' and len(value.arguments) == 3: if str(value.arguments[0]) not in self._ids: self._ids[str(value.arguments[0])] = [] self._ids[str(value.arguments[0])].append( (str(value.arguments[1]), value.arguments[2].number)) return 0 return 1 def get_rect(self): return self._graphics_item.rect()
class PaintArea(QGraphicsView): def __init__(self, width=10, parent=None): QGraphicsView.__init__(self, parent) self._frame = None self._instructions = None self.setScene(QGraphicsScene(self)) self._items = self.scene().createItemGroup([]) self.setMouseTracking(True) self.pen = QPen(Qt.black, width, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) self.painting = False self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) self.viewport().setCursor(self.getCursor()) self.updateScene() def updateScene(self): if self._frame: self.scene().setBackgroundBrush(Qt.gray) oldCanvas = self.canvas() self.setSceneRect(QRectF(self.contentsRect())) self.centerScene() self.scaleItems(oldCanvas, self.canvas()) def centerScene(self): self.centerFrame() self.centerInstructions() def scaleItems(self, oldCanvas, newCanvas): pass def canvas(self): if self._frame: return self._frame.rect() return QRectF(self.contentsRect()) def fitInstructions(self): textSize = self._instructions.document().size() factor = min(self.canvas().size().width() / textSize.width(), self.canvas().size().height() / textSize.height()) f = self._instructions.font() f.setPointSizeF(f.pointSizeF() * factor) self._instructions.setFont(f) def centerInstructions(self): if self._instructions: self.fitInstructions() size = self.size() textSize = self._instructions.document().size() self._instructions.setPos((size.width() - textSize.width()) / 2.0, (size.height() - textSize.height()) / 2.0) def setInstructions(self, text): if self._instructions: self._instructions.setPlainText(text) else: self._instructions = self.scene().addText(text, QFont('Arial', 10, QFont.Bold)) self._instructions.setZValue(-1) self._instructions.setDefaultTextColor(QColor(220, 220, 220)) self._text = text self.centerInstructions() def setFrame(self, width, height): if self._frame: self._frame.setRect(0, 0, width, height) else: self.addFrame(QRectF(0, 0, width, height)) self.centerScene() def addFrame(self, rect): self._frame = QGraphicsRectItem(rect) self._frame.setPen(QPen(Qt.NoPen)) self._frame.setBrush(Qt.white) self._frame.setZValue(-2) self.scene().addItem(self._frame) def centerFrame(self): if self._frame: rect = self._frame.rect() size = self.contentsRect() factor = min((size.width() + 1) / rect.width(), (size.height() + 1) / rect.height()) w, h = rect.width() * factor, rect.height() * factor self._frame.setRect(size.x() + (size.width() - w) / 2.0, size.y() + (size.height() - h) / 2.0, w, h) def resizeEvent(self, event): self.updateScene() def setBrushSize(self, size): self.pen.setWidth(size) self.viewport().setCursor(self.getCursor()) def render(self, painter): if self._instructions: self.scene().removeItem(self._instructions) self.scene().render(painter, source=self.scene().itemsBoundingRect()) if self._instructions: self.scene().addItem(self._instructions) def getLines(self): items = [item for item in self.scene().items() if item.group() == self._items] return self.canvas(), items def clear(self): for item in self.scene().items(): if item.group() == self._items: self._items.removeFromGroup(item) def getCursor(self): antialiasing_margin = 1 size = self.pen.width() pixmap = QPixmap(size + antialiasing_margin * 2, size + antialiasing_margin * 2) pixmap.fill(Qt.transparent) painter = QPainter(pixmap) painter.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) painter.drawEllipse(QRectF(QPointF(antialiasing_margin, antialiasing_margin), QSizeF(size, size))) painter.end() return QCursor(pixmap) def addLine(self, start, end): if start == end: delta = QPointF(.0001, 0) end = start - delta line = self.scene().addLine(QLineF(start, end), self.pen) self._items.addToGroup(line) def drawPoint(self, pos): delta = QPointF(.0001, 0) line = self.scene().addLine(QLineF(pos, pos - delta), self.pen) self._items.addToGroup(line) def mousePressEvent(self, event): self.start = QPointF(self.mapToScene(event.pos())) self.painting = True self.addLine(self.start, self.start) def mouseReleaseEvent(self, event): self.painting = False def mouseMoveEvent(self, event): pos = QPointF(self.mapToScene(event.pos())) if self.painting: self.addLine(self.start, pos) self.start = pos
class RecordsetWindow(QWidget): dataDisplayRequest = pyqtSignal(str, int) dataUpdateRequest = pyqtSignal(str, Base) def __init__(self, manager, recordset: list, parent=None): super().__init__(parent=parent) self.UI = Ui_frmRecordsets() self.UI.setupUi(self) # Internal lists self.sensors = {} # List of sensors objects self.sensors_items = {} # List of QAction corresponding to each sensors self.sensors_graphs = {} # List of graph corresponding to each sensor graph self.sensors_location = [] # List of sensors location in this recordset self.sensors_blocks = {} # Sensor blocks - each block of data for each sensor # Variables self.time_pixmap = False # Flag used to check if we need to repaint the timeline or not self.zoom_level = 1 # Current timeview zoom level # Manually created UI objects self.time_bar = QGraphicsLineItem() # Currently selected timestamp bar self.selection_rec = QGraphicsRectItem() # Section rectangle self.sensors_menu = QMenu(self.UI.btnNewGraph) self.UI.btnNewGraph.setMenu(self.sensors_menu) # Data access informations self.dbMan = manager self.recordsets = recordset # Init temporal browser self.timeScene = QGraphicsScene() self.UI.graphTimeline.setScene(self.timeScene) self.UI.graphTimeline.fitInView(self.timeScene.sceneRect()) self.UI.graphTimeline.time_clicked.connect(self.timeview_clicked) self.UI.graphTimeline.time_selected.connect(self.timeview_selected) self.UI.scrollTimeline.valueChanged.connect(self.timeview_scroll) # Init temporal sensor list self.timeSensorsScene = QGraphicsScene() self.UI.graphSensorsTimeline.setScene(self.timeSensorsScene) self.UI.graphSensorsTimeline.fitInView(self.timeSensorsScene.sceneRect(), Qt.KeepAspectRatio) # Update general informations about recordsets self.update_recordset_infos() # Load sensors for that recordset self.load_sensors() # Connect signals to slots self.UI.btnClearSelection.clicked.connect(self.on_timeview_clear_selection_requested) self.UI.btnTimeZoomSelection.clicked.connect(self.on_timeview_zoom_selection_requested) self.UI.btnZoomReset.clicked.connect(self.on_timeview_zoom_reset_requested) self.UI.btnDisplayTimeline.clicked.connect(self.on_timeview_show_hide_requested) self.UI.btnTileHorizontal.clicked.connect(self.tile_graphs_horizontally) self.UI.btnTileVertical.clicked.connect(self.tile_graphs_vertically) self.UI.btnTileAuto.clicked.connect(self.tile_graphs_auto) self.sensors_menu.triggered.connect(self.sensor_graph_selected) # Initial UI state self.UI.btnZoomReset.setEnabled(False) self.UI.btnTimeZoomSelection.setEnabled(False) self.UI.btnClearSelection.setEnabled(False) self.update_tile_buttons_state() def paintEvent(self, paint_event): if not self.time_pixmap: self.refresh_timeview() self.time_pixmap = True def resizeEvent(self, resize_event): if self.time_pixmap: self.refresh_timeview() def refresh_timeview(self): QGuiApplication.setOverrideCursor(Qt.BusyCursor) # Computes required timescene size min_width = self.UI.graphTimeline.width() - 5 if len(self.recordsets) > 0: num_days = (self.get_recordset_end_day_date() - self.get_recordset_start_day_date()).days # Minimum size for days if num_days * 75 > min_width: min_width = num_days * 75 - 5 # Resize timeScene correctly self.timeScene.clear() self.timeScene.setSceneRect(self.timeScene.itemsBoundingRect()) self.timeScene.addLine(0, 80, min_width, 80, QPen(Qt.transparent)) # Set background color back_brush = QBrush(Qt.lightGray) self.timeScene.setBackgroundBrush(back_brush) self.timeSensorsScene.setBackgroundBrush(back_brush) # Update display self.draw_dates() self.draw_sensors_names() self.draw_recordsets() self.draw_sensors() self.draw_grid() self.draw_timebar() # Adjust splitter sizes self.adjust_timeview_size() # self.UI.frmSensors.hide() QGuiApplication.restoreOverrideCursor() def adjust_timeview_size(self): self.UI.frameScrollSpacer.setFixedWidth(self.UI.graphTimeline.pos().x()) if self.timeScene.itemsBoundingRect().width() * self.zoom_level > self.UI.graphTimeline.width(): self.UI.scrollTimeline.setVisible(True) # self.UI.scrollTimeline.setMinimum(self.UI.graphTimeline.width()/2) self.UI.scrollTimeline.setMinimum(0) self.UI.scrollTimeline.setMaximum(self.timeScene.itemsBoundingRect().width() * self.zoom_level) self.UI.scrollTimeline.setPageStep(self.UI.graphTimeline.width()/2) self.UI.scrollTimeline.setSingleStep(self.UI.graphTimeline.width()/5) else: self.UI.scrollTimeline.setVisible(False) def load_sensors(self): # self.UI.lstSensors.clear() self.sensors = {} self.sensors_items = {} self.sensors_location = [] self.sensors_menu.clear() if len(self.recordsets) > 0: for recordset in self.recordsets: for sensor in self.dbMan.get_sensors(recordset): if sensor.location not in self.sensors_location: self.sensors_location.append(sensor.location) self.sensors_menu.addSection(sensor.location) if sensor.id_sensor not in self.sensors: self.sensors[sensor.id_sensor] = sensor sensor_item = QAction(sensor.name) sensor_item.setCheckable(True) sensor_item.setProperty("sensor_id", sensor.id_sensor) self.sensors_items[sensor.id_sensor] = sensor_item self.sensors_menu.addAction(sensor_item) # Create sensors blocks for display self.load_sensors_blocks() else: self.UI.btnNewGraph.setEnabled(False) def update_recordset_infos(self): if len(self.recordsets) == 0: self.UI.lblTotalValue.setText("Aucune donnée.") self.UI.lblDurationValue.setText("Aucune donnée.") return start_time = self.recordsets[0].start_timestamp end_time = self.recordsets[len(self.recordsets) - 1].end_timestamp # Coverage self.UI.lblTotalValue.setText(start_time.strftime('%d-%m-%Y %H:%M:%S') + " @ " + end_time.strftime( '%d-%m-%Y %H:%M:%S')) # Duration # TODO: format better self.UI.lblDurationValue.setText(str(end_time - start_time)) self.UI.lblCursorTime.setText(start_time.strftime('%d-%m-%Y %H:%M:%S')) def get_recordset_start_day_date(self): if len(self.recordsets) == 0: return None start_time = self.recordsets[0].start_timestamp start_time = (datetime(start_time.year, start_time.month, start_time.day, 0, 0, 0)) return start_time def get_recordset_end_day_date(self): if len(self.recordsets) == 0: return None end_time = self.recordsets[len(self.recordsets) - 1].end_timestamp end_time = (datetime(end_time.year, end_time.month, end_time.day, 0, 0, 0) + timedelta(days=1)) return end_time def get_relative_timeview_pos(self, current_time): # start_time = self.recordsets[0].start_timestamp.timestamp() start_time = self.get_recordset_start_day_date().timestamp() # end_time = self.recordsets[len(self.recordsets) - 1].end_timestamp.timestamp() end_time = self.get_recordset_end_day_date().timestamp() time_span = (end_time - start_time) # Total number of seconds in recordsets # if type(current_time) is datetime: if isinstance(current_time, datetime): current_time = current_time.timestamp() if time_span > 0: # return ((current_time - start_time) / time_span) * self.UI.graphTimeline.width() return ((current_time - start_time) / time_span) * self.timeScene.width() else: return 0 def get_time_from_timeview_pos(self, pos): if len(self.recordsets) == 0: return None; start_time = self.get_recordset_start_day_date().timestamp() end_time = self.get_recordset_end_day_date().timestamp() current_time = (pos / self.timeScene.width()) * (end_time - start_time) + start_time current_time = datetime.fromtimestamp(current_time) return current_time def draw_dates(self): if len(self.recordsets) == 0: return # Computations start_time = self.recordsets[0].start_timestamp end_time = self.recordsets[len(self.recordsets) - 1].end_timestamp # time_span = (end_time - start_time).total_seconds() # Total number of seconds in recordsets current_time = (datetime(start_time.year, start_time.month, start_time.day, 0, 0, 0) + timedelta(days=1)) # Drawing tools black_pen = QPen(Qt.black) blue_brush = QBrush(Qt.darkBlue) # Date background self.timeScene.addRect(0, 0, self.timeScene.width(), 20, black_pen, blue_brush) self.timeSensorsScene.addRect(0, 0, self.timeSensorsScene.width(), 20, black_pen, blue_brush) # First date date_text = self.timeScene.addText(start_time.strftime("%d-%m-%Y")) date_text.setPos(0, 0) # -5 date_text.setDefaultTextColor(Qt.white) date_text.setFlag(QGraphicsItem.ItemIgnoresTransformations, True) # Date separators while current_time <= end_time: pos = self.get_relative_timeview_pos(current_time) date_text = self.timeScene.addText(current_time.strftime("%d-%m-%Y")) date_text.setPos(pos, 0) # -5 date_text.setDefaultTextColor(Qt.white) date_text.setFlag(QGraphicsItem.ItemIgnoresTransformations, True) current_time += timedelta(days=1) # self.UI.graphTimeline.fitInView(self.timeScene.sceneRect(), Qt.KeepAspectRatio) def draw_grid(self): if len(self.recordsets) == 0: return # Computations start_time = self.recordsets[0].start_timestamp end_time = self.recordsets[len(self.recordsets) - 1].end_timestamp # time_span = (end_time - start_time).total_seconds() # Total number of seconds in recordsets current_time = (datetime(start_time.year, start_time.month, start_time.day, 0, 0, 0) + timedelta(days=1)) vgrid_pen = QPen(Qt.gray) hgrid_pen = QPen(Qt.black) vgrid_pen.setCosmetic(True) # Horizontal lines pos = 20 # last_location = "" sensor_location_brush = QBrush(Qt.black) sensor_location_pen = QPen(Qt.transparent) for location in self.sensors_location: # Must create a new location line self.timeScene.addRect(0, pos, self.timeScene.width() - 1, 15, sensor_location_pen, sensor_location_brush) pos += 15 sensors = self.get_sensors_for_location(location) for _ in sensors: self.timeScene.addLine(0, pos, self.timeScene.width() - 1, pos, hgrid_pen) self.timeSensorsScene.addLine(0, pos, self.timeSensorsScene.width(), pos, hgrid_pen) pos += 20 # Final line self.timeScene.addLine(0, pos, self.timeScene.width() - 1, pos, hgrid_pen) self.timeSensorsScene.addLine(0, pos, self.timeSensorsScene.width() - 1, pos, hgrid_pen) # Date separators self.timeScene.addLine(0, 0, 0, self.timeScene.height(), vgrid_pen) # Other dates while current_time <= end_time: pos = self.get_relative_timeview_pos(current_time) self.timeScene.addLine(pos, 0, pos, self.timeScene.height(), vgrid_pen) current_time += timedelta(days=1) def draw_recordsets(self): recordset_brush = QBrush(QColor(212, 247, 192)) # Green recordset_pen = QPen(Qt.transparent) # Recording length for record in self.recordsets: start_pos = self.get_relative_timeview_pos(record.start_timestamp) end_pos = self.get_relative_timeview_pos(record.end_timestamp) span = end_pos - start_pos # print (str(span)) self.timeScene.addRect(start_pos, 21, span, self.timeSensorsScene.height()-21, recordset_pen, recordset_brush) # self.UI.graphTimeline.update() return def draw_sensors_names(self): if len(self.sensors) == 0: return sensor_location_brush = QBrush(Qt.black) sensor_location_pen = QPen(Qt.transparent) # Sensor names pos = 20 for location in self.sensors_location: # Must create a new location space for later pos += 15 sensors = self.get_sensors_for_location(location) for sensor_id in sensors: sensor = self.sensors[sensor_id] # Sensor names label = self.timeSensorsScene.addText(sensor.name) label.setPos(0, pos) label.setDefaultTextColor(Qt.black) # label.setFont(QFont("Times", 10, QFont.Bold)) pos += 20 # Adjust size appropriately self.timeSensorsScene.setSceneRect(self.timeSensorsScene.itemsBoundingRect()) self.UI.graphSensorsTimeline.setMaximumWidth(self.timeSensorsScene.itemsBoundingRect().width()) # Sensor location background pos = 20 for location in self.sensors_location: # Must create a new location line self.timeSensorsScene.addRect(0, pos, self.timeSensorsScene.width(), 15, sensor_location_pen, sensor_location_brush) label = self.timeSensorsScene.addText(location) label.setPos(0, pos) label.setDefaultTextColor(Qt.white) label.setFont(QFont("Times", 7)) pos += 15 sensors = self.get_sensors_for_location(location) for sensor_id in sensors: pos += 20 def draw_sensors(self): if len(self.sensors) == 0: return sensor_brush = QBrush(Qt.darkGreen) sensor_pen = QPen(Qt.transparent) sensors_rects = self.create_sensors_rects() for _, rect in enumerate(sensors_rects): self.timeScene.addRect(rect, sensor_pen, sensor_brush) # Adjust size appropriately self.timeSensorsScene.setSceneRect(self.timeSensorsScene.itemsBoundingRect()) self.UI.graphSensorsTimeline.setMaximumWidth(self.timeSensorsScene.itemsBoundingRect().width()) # self.UI.graphSensorsTimeline.setMaximumHeight(self.timeSensorsScene.itemsBoundingRect().height()) def load_sensors_blocks(self): # Create request tasks tasks = [] for location in self.sensors_location: sensors = self.get_sensors_for_location(location) for sensor_id in sensors: for record in self.recordsets: tasks.append(DBSensorTimesTask(title="Chargement des données temporelles", db_manager=self.dbMan, sensor_id=sensor_id, recordset=record)) QGuiApplication.setOverrideCursor(Qt.BusyCursor) process = BackgroundProcess(tasks) dialog = ProgressDialog(process, "Chargement") process.start() dialog.exec() QGuiApplication.restoreOverrideCursor() # Combine tasks results self.sensors_blocks = {} for task in tasks: for result in task.results: if result['sensor_id'] not in self.sensors_blocks: self.sensors_blocks[result['sensor_id']] = [] start_time = result['start_time'] end_time = result['end_time'] data = {"start_time": start_time, "end_time": end_time} self.sensors_blocks[result['sensor_id']].append(data) def create_sensors_rects(self): rects = [] pos = 20 for location in self.sensors_location: # Must create a new location space for later pos += 15 sensors = self.get_sensors_for_location(location) for sensor_id in sensors: # sensor = self.sensors[sensor_id] # for record in self.recordsets: # datas = self.dbMan.get_all_sensor_data(sensor=sensor, recordset=record, channel=sensor.channels[0]) # for data in datas: # start_pos = self.get_relative_timeview_pos(data.timestamps.start_timestamp) # end_pos = self.get_relative_timeview_pos(data.timestamps.end_timestamp) # span = max(end_pos - start_pos, 1) # rects.append(QRectF(start_pos, pos + 3, span, 14)) if sensor_id in self.sensors_blocks: for block in self.sensors_blocks[sensor_id]: start_pos = self.get_relative_timeview_pos(block['start_time']) end_pos = self.get_relative_timeview_pos(block['end_time']) span = max(end_pos - start_pos, 1) rects.append(QRectF(start_pos, pos + 3, span, 14)) pos += 20 return rects def draw_timebar(self): line_pen = QPen(Qt.cyan) line_pen.setWidth(2) self.time_bar = self.timeScene.addLine(1, 21, 1, self.timeScene.height()-1, line_pen) # self.time_bar = self.timeScene.addLine(0, 1, 0, self.timeScene.height() - 1, line_pen) self.time_bar.setFlag(QGraphicsItem.ItemIgnoresTransformations, True) def get_sensors_for_location(self, location): sensors_id = [] for sensor in self.sensors.values(): if sensor.location == location: sensors_id.append(sensor.id_sensor) return sensors_id @staticmethod def get_sensor_graph_type(sensor): if sensor.id_sensor_type == SensorType.ACCELEROMETER \ or sensor.id_sensor_type == SensorType.GYROMETER \ or sensor.id_sensor_type == SensorType.BATTERY \ or sensor.id_sensor_type == SensorType.LUX \ or sensor.id_sensor_type == SensorType.CURRENT \ or sensor.id_sensor_type == SensorType.BAROMETER \ or sensor.id_sensor_type == SensorType.MAGNETOMETER \ or sensor.id_sensor_type == SensorType.TEMPERATURE \ or sensor.id_sensor_type == SensorType.HEARTRATE \ or sensor.id_sensor_type == SensorType.ORIENTATION \ or sensor.id_sensor_type == SensorType.FSR: return GraphType.LINECHART if sensor.id_sensor_type == SensorType.GPS: return GraphType.MAP return GraphType.UNKNOWN @pyqtSlot(Sensor, datetime, datetime) def query_sensor_data(self, sensor: Sensor, start_time: datetime, end_time: datetime): timeseries = self.get_sensor_data(sensor, start_time, end_time)[0] if self.sensors_graphs[sensor.id_sensor]: graph_type = self.get_sensor_graph_type(sensor) graph_window = self.sensors_graphs[sensor.id_sensor] if graph_type == GraphType.LINECHART: series_id = 0 for series in timeseries: # Filter times that don't fit in the range y_range = series['y'] x_range = series['x'] y_range = y_range[x_range >= start_time.timestamp()] x_range = x_range[x_range >= start_time.timestamp()] y_range = y_range[x_range <= end_time.timestamp()] x_range = x_range[x_range <= end_time.timestamp()] if len(x_range)>0 and len(y_range)>0: graph_window.graph.update_data(x_range, y_range, series_id) series_id += 1 return @pyqtSlot(QAction) # @timing def sensor_graph_selected(self, sensor_item): sensor_id = sensor_item.property("sensor_id") sensor = self.sensors[sensor_id] sensor_label = sensor.name + " (" + sensor.location + ")" if sensor_item.isChecked(): # Choose the correct display for each sensor graph_window = None timeseries, channel_data = self.get_sensor_data(sensor) # Fetch all sensor data # Color map for curves colors = [Qt.blue, Qt.green, Qt.yellow, Qt.red] graph_type = self.get_sensor_graph_type(sensor) graph_window = GraphWindow(graph_type, sensor, self.UI.mdiArea) graph_window.setStyleSheet(self.styleSheet() + graph_window.styleSheet()) if graph_type == GraphType.LINECHART: # Add series for series in timeseries: graph_window.graph.add_data(series['x'], series['y'], color=colors.pop(), legend_text=series['label']) graph_window.graph.set_title(sensor_label) if graph_type == GraphType.MAP: for data in channel_data: gps = GPSGeodetic() gps.from_bytes(data.data) if gps.latitude != 0 and gps.longitude != 0: graph_window.graph.addPosition(data.timestamps.start_timestamp, gps.latitude / 1e7, gps.longitude / 1e7) graph_window.setCursorPositionFromTime(data.timestamps.start_timestamp) if graph_window is not None: self.UI.mdiArea.addSubWindow(graph_window).setWindowTitle(sensor_label) self.sensors_graphs[sensor.id_sensor] = graph_window # self.UI.displayContents.layout().insertWidget(0,graph) graph_window.show() QApplication.instance().processEvents() graph_window.aboutToClose.connect(self.graph_was_closed) graph_window.requestData.connect(self.query_sensor_data) graph_window.graph.cursorMoved.connect(self.graph_cursor_changed) graph_window.graph.selectedAreaChanged.connect(self.graph_selected_area_changed) graph_window.graph.clearedSelectionArea.connect(self.on_timeview_clear_selection_requested) graph_window.zoomAreaRequested.connect(self.graph_zoom_area) graph_window.zoomResetRequested.connect(self.graph_zoom_reset) self.UI.mdiArea.tileSubWindows() else: # Remove from display try: if self.sensors_graphs[sensor.id_sensor] is not None: self.UI.mdiArea.removeSubWindow(self.sensors_graphs[sensor.id_sensor].parent()) self.sensors_graphs[sensor.id_sensor].hide() del self.sensors_graphs[sensor.id_sensor] self.UI.mdiArea.tileSubWindows() except KeyError: pass self.update_tile_buttons_state() # @timing def get_sensor_data(self, sensor, start_time=None, end_time=None): QGuiApplication.setOverrideCursor(Qt.BusyCursor) task = DBSensorAllDataTask("Chargement des données...", self.dbMan, sensor, start_time, end_time, self.recordsets) process = BackgroundProcess([task]) dialog = ProgressDialog(process, "Traitement") process.start() dialog.exec() QGuiApplication.restoreOverrideCursor() return task.results['timeseries'], task.results['channel_data'] def update_tile_buttons_state(self): if self.sensors_graphs.keys().__len__() > 1: self.UI.btnTileAuto.setEnabled(True) self.UI.btnTileHorizontal.setEnabled(True) self.UI.btnTileVertical.setEnabled(True) else: self.UI.btnTileAuto.setEnabled(False) self.UI.btnTileHorizontal.setEnabled(False) self.UI.btnTileVertical.setEnabled(False) @pyqtSlot(QObject) def graph_was_closed(self, graph): for sensor_id, sensor_graph in self.sensors_graphs.items(): if sensor_graph == graph: # self.sensors_graphs[sensor_id] = None del self.sensors_graphs[sensor_id] self.sensors_items[sensor_id].setChecked(False) break self.UI.mdiArea.tileSubWindows() self.update_tile_buttons_state() @pyqtSlot(float) def graph_cursor_changed(self, timestamp): current_time = timestamp / 1000 for graph in self.sensors_graphs.values(): if graph is not None: graph.setCursorPositionFromTime(current_time, False) pos = self.get_relative_timeview_pos(current_time) self.time_bar.setPos(pos,0) # Ensure time bar is visible if scrollable self.ensure_time_bar_visible(pos) self.UI.lblCursorTime.setText(datetime.fromtimestamp(current_time).strftime('%d-%m-%Y %H:%M:%S')) def ensure_time_bar_visible(self, pos): if self.UI.scrollTimeline.isVisible(): max_visible_x = self.UI.graphTimeline.mapToScene(self.UI.graphTimeline.rect()).boundingRect().x() \ + self.UI.graphTimeline.mapToScene(self.UI.graphTimeline.rect()).boundingRect().width() min_visible_x = self.UI.graphTimeline.mapToScene(self.UI.graphTimeline.rect()).boundingRect().x() if pos < min_visible_x or pos > max_visible_x: self.UI.scrollTimeline.setValue(pos) @pyqtSlot(datetime, datetime) def graph_zoom_area(self, start_time, end_time): for graph in self.sensors_graphs.values(): if graph is not None: graph.zoomAreaRequestTime(start_time, end_time) @pyqtSlot() def graph_zoom_reset(self): for graph in self.sensors_graphs.values(): if graph is not None: graph.zoomResetRequest(False) @pyqtSlot(float, float) def graph_selected_area_changed(self, start_timestamp, end_timestamp): # Update timeview selection area start_pos = self.get_relative_timeview_pos(start_timestamp / 1000) end_pos = self.get_relative_timeview_pos(end_timestamp / 1000) self.timeview_selected(start_pos, end_pos) # Ensure time bar is visible if scrollable self.ensure_time_bar_visible(start_pos) # Update selection for each graph for graph in self.sensors_graphs.values(): if graph is not None: graph.setSelectionAreaFromTime(start_timestamp, end_timestamp) @pyqtSlot(int) def timeview_scroll(self, pos): self.UI.graphTimeline.centerOn(pos / self.zoom_level, 0) @pyqtSlot(float) def timeview_clicked(self, x): self.time_bar.setPos(x, 0) if len(self.recordsets)==0: return # Find time corresponding to that position timestamp = self.get_time_from_timeview_pos(x) self.UI.lblCursorTime.setText(timestamp.strftime('%d-%m-%Y %H:%M:%S')) for graph in self.sensors_graphs.values(): if graph is not None: # try: graph.setCursorPositionFromTime(timestamp, False) # except AttributeError: # continue @pyqtSlot(float, float) def timeview_selected(self, start_x, end_x): selection_brush = QBrush(QColor(153, 204, 255, 128)) selection_pen = QPen(Qt.transparent) self.timeScene.removeItem(self.selection_rec) self.selection_rec = self.timeScene.addRect(start_x, 20, end_x-start_x, self.timeScene.height()-20, selection_pen, selection_brush) self.UI.btnClearSelection.setEnabled(True) self.UI.btnTimeZoomSelection.setEnabled(True) # Update selection for each graph for graph in self.sensors_graphs.values(): if graph is not None: graph.setSelectionAreaFromTime(self.get_time_from_timeview_pos(start_x), self.get_time_from_timeview_pos(end_x)) @pyqtSlot() def on_timeview_clear_selection_requested(self): self.timeScene.removeItem(self.selection_rec) self.UI.btnClearSelection.setEnabled(False) self.UI.btnTimeZoomSelection.setEnabled(False) # Clear all selected areas in graphs for graph in self.sensors_graphs.values(): graph.clearSelectionArea() @pyqtSlot() def on_timeview_zoom_selection_requested(self): self.UI.graphTimeline.scale(1 / self.zoom_level, 1) # zoom_value = (self.timeScene.width() / (self.selection_rec.rect().width())) zoom_value = (self.UI.graphTimeline.width() / (self.selection_rec.rect().width())) self.zoom_level = zoom_value self.UI.graphTimeline.scale(zoom_value, 1) self.UI.btnZoomReset.setEnabled(True) self.adjust_timeview_size() # self.UI.graphTimeline.ensureVisible(self.selection_rec.rect(), 0, 0) # self.UI.graphTimeline.centerOn(self.selection_rec.rect().x() + self.selection_rec.rect().width() / 2, 0) self.UI.scrollTimeline.setValue((self.selection_rec.rect().x() + self.selection_rec.rect().width() / 2) * self.zoom_level) self.on_timeview_clear_selection_requested() @pyqtSlot() def on_timeview_zoom_reset_requested(self): self.UI.graphTimeline.scale(1 / self.zoom_level, 1) self.zoom_level = 1 self.UI.btnZoomReset.setEnabled(False) self.adjust_timeview_size() self.UI.scrollTimeline.setValue(0) @pyqtSlot() def on_timeview_show_hide_requested(self): visible = not self.UI.frameTimeline.isVisible() self.UI.frameTimeline.setVisible(visible) self.UI.frameTimelineControls.setVisible(visible) # self.UI.lblCursorTime.setVisible(visible) @pyqtSlot() def on_process_recordset(self): # Display Process Window window = ProcessSelectWindow(self.dbMan, self.recordsets) window.setStyleSheet(self.styleSheet()) if window.exec() == QDialog.Accepted: self.dataUpdateRequest.emit("result", window.processed_data) self.dataDisplayRequest.emit("result", window.processed_data.id_processed_data) @pyqtSlot() def tile_graphs_horizontally(self): self.tile_graphs(True) @pyqtSlot() def tile_graphs_vertically(self): self.tile_graphs(False) def tile_graphs(self, horizontal: bool): if self.UI.mdiArea.subWindowList() is None: return position = QPoint(0, 0) for window in self.UI.mdiArea.subWindowList(): if horizontal: rect = QRect(0, 0, self.UI.mdiArea.width() / len(self.UI.mdiArea.subWindowList()), self.UI.mdiArea.height()) else: rect = QRect(0, 0, self.UI.mdiArea.width(), self.UI.mdiArea.height() / len(self.UI.mdiArea.subWindowList())) window.setGeometry(rect) window.move(position) if horizontal: position.setX(position.x() + window.width()) else: position.setY(position.y() + window.height()) @pyqtSlot() def tile_graphs_auto(self): self.UI.mdiArea.tileSubWindows()
class SelectionScene(QGraphicsScene): selection_drawing_finished = pyqtSignal(QRectF) def __init__(self, scene_rect: QRectF, parent: QObject = None): super(SelectionScene, self).__init__(scene_rect, parent) rectangle_color = QColor(Qt.red) self.default_border_pen = QPen(Qt.red, 3, Qt.SolidLine, Qt.SquareCap, Qt.MiterJoin) self.default_fill_brush = QBrush(rectangle_color, Qt.BDiagPattern) self.new_selection_view: QGraphicsRectItem = None self.new_selection_origin: QPointF = None self.mode = EditorMode.DRAW_MODE def mousePressEvent(self, event: QGraphicsSceneMouseEvent): if self.mode == EditorMode.DRAW_MODE: self._handle_mouse_press_draw_mode(event) elif self.mode == EditorMode.MOVE_MODE: raise NotImplementedError("Move mode is not implemented!") def _handle_mouse_press_draw_mode(self, event: QGraphicsSceneMouseEvent): if event.button() == Qt.LeftButton and self.new_selection_view is None: self.new_selection_origin = event.scenePos() self._restrict_to_scene_space(self.new_selection_origin) scene_logger.info( f"Beginning to draw a new selection: " f"X={self.new_selection_origin.x()}, Y={self.new_selection_origin.y()}" ) self.new_selection_view = QGraphicsRectItem( self.new_selection_origin.x(), self.new_selection_origin.y(), 0, 0) self.new_selection_view.setPen(self.default_border_pen) self.new_selection_view.setBrush(self.default_fill_brush) self.addItem(self.new_selection_view) event.accept() def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): if self.mode == EditorMode.DRAW_MODE: self._handle_mouse_move_draw_mode(event) elif self.mode == EditorMode.MOVE_MODE: raise NotImplementedError("Move mode is not implemented!") def _handle_mouse_move_draw_mode(self, event: QGraphicsSceneMouseEvent): point2 = event.scenePos() self._restrict_to_scene_space(point2) if self.new_selection_origin is None: scene_logger.warning("Move event: Selection origin is None!") event.accept() return rectangle = QRectF( QPointF(min(self.new_selection_origin.x(), point2.x()), min(self.new_selection_origin.y(), point2.y())), QPointF(max(self.new_selection_origin.x(), point2.x()), max(self.new_selection_origin.y(), point2.y()))) self.new_selection_view.setRect(rectangle) event.accept() def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): if self.mode == EditorMode.DRAW_MODE: self._handle_mouse_release_draw_mode(event) elif self.mode == EditorMode.MOVE_MODE: raise NotImplementedError("Move mode is not implemented!") def _handle_mouse_release_draw_mode(self, event: QGraphicsSceneMouseEvent): self.new_selection_view: QGraphicsRectItem if event.button( ) == Qt.LeftButton and self.new_selection_view is not None: absolute_rectangle = self.new_selection_view.mapRectFromScene( self.new_selection_view.rect()) if self.is_rectangle_valid_selection(absolute_rectangle): self.selection_drawing_finished.emit(absolute_rectangle) else: scene_logger.info( f"Discarding invalid selection: " f"x={absolute_rectangle.x()}, y={absolute_rectangle.y()}, " f"width={absolute_rectangle.width()}, height={absolute_rectangle.height()}" ) self.removeItem(self.new_selection_view) self.new_selection_origin = None self.new_selection_view = None event.accept() def load_selections(self, current: QModelIndex): selection_count: int = current.model().rowCount( current) # The number of child nodes, which are selections current_first_column = current.sibling( current.row(), 0) # Selections are below the first column selections: typing.List[Selection] = [ current_first_column.child(index, 0).data(Qt.UserRole) for index in range(selection_count) ] editor_logger.debug(f"Loading selection list: {selections}") for selection in selections: self._draw_rectangle(current, selection) def _restrict_to_scene_space(self, point: QPointF): """Restrict rectangle drawing to the screen space. This prevents drawing out of the source image bounds.""" point.setX(min(max(point.x(), 0), self.sceneRect().width())) point.setY(min(max(point.y(), 0), self.sceneRect().height())) def _draw_rectangle(self, current: QModelIndex, rectangle: Selection): self.addRect(self._to_local_coordinates(current, rectangle), self.default_border_pen, self.default_fill_brush) def _to_local_coordinates(self, current: QModelIndex, rectangle: Selection) -> QRectF: """ Scales a model Selection to local coordinates. Large images are scaled down, so the rectangles need to be scaled, too. This function performs the scaling and conversion to floating point based rectangles, as expected by QGraphicsView. """ scaling_factor: float = self.width() / current.sibling( current.row(), 0).data(Qt.UserRole).width if scaling_factor >= 1: result = rectangle.as_qrectf else: result = QRectF(rectangle.top_left.x * scaling_factor, rectangle.top_left.y * scaling_factor, rectangle.width * scaling_factor, rectangle.height * scaling_factor) scene_logger.debug( f"Scaled {rectangle} to {result.topLeft(), result.bottomRight()}" ) return result def is_rectangle_valid_selection(self, selection: QRectF) -> bool: """ Returns True, if the given rectangle is a valid selection. A selection is determined to be valid if its width and height is at least 0.5% of the source image or 20 pixels large, whichever comes first """ return (selection.width() > self.sceneRect().width() * 0.01 or selection.width() > 20) \ and (selection.height() > self.sceneRect().height() * 0.01 or selection.height() > 20)
class IDView(QGraphicsView): """Simple extension of QGraphicsView - containing an image and click-to-zoom/unzoom """ def __init__(self, parent, fnames): QGraphicsView.__init__(self) self.parent = parent self.initUI(fnames) def initUI(self, fnames): # Make QGraphicsScene self.scene = QGraphicsScene() # TODO = handle different image sizes. self.images = {} self.imageGItem = QGraphicsItemGroup() self.scene.addItem(self.imageGItem) self.updateImage(fnames) self.setBackgroundBrush(QBrush(Qt.darkCyan)) self.parent.tool = "zoom" self.boxFlag = False self.originPos = QPointF(0, 0) self.currentPos = self.originPos self.boxItem = QGraphicsRectItem() self.boxItem.setPen(QPen(Qt.darkCyan, 1)) self.boxItem.setBrush(QBrush(QColor(0, 255, 0, 64))) def updateImage(self, fnames): """Update the image with that from filename""" for n in self.images: self.imageGItem.removeFromGroup(self.images[n]) self.images[n].setVisible(False) if fnames is not None: x = 0 n = 0 for fn in fnames: self.images[n] = QGraphicsPixmapItem(QPixmap(fn)) self.images[n].setTransformationMode(Qt.SmoothTransformation) self.images[n].setPos(x, 0) self.images[n].setVisible(True) self.scene.addItem(self.images[n]) x += self.images[n].boundingRect().width() + 10 self.imageGItem.addToGroup(self.images[n]) n += 1 # Set sensible sizes and put into the view, and fit view to the image. br = self.imageGItem.boundingRect() self.scene.setSceneRect( 0, 0, max(1000, br.width()), max(1000, br.height()), ) self.setScene(self.scene) self.fitInView(self.imageGItem, Qt.KeepAspectRatio) def deleteRect(self): if self.boxItem.scene() is None: return self.scene.removeItem(self.boxItem) self.parent.rectangle = None def mousePressEvent(self, event): if self.parent.tool == "rect": self.originPos = self.mapToScene(event.pos()) self.currentPos = self.originPos self.boxItem.setRect(QRectF(self.originPos, self.currentPos)) if self.boxItem.scene() is None: self.scene.addItem(self.boxItem) self.boxFlag = True else: super(IDView, self).mousePressEvent(event) def mouseMoveEvent(self, event): if self.parent.tool == "rect" and self.boxFlag: self.currentPos = self.mapToScene(event.pos()) if self.boxItem is None: return else: self.boxItem.setRect(QRectF(self.originPos, self.currentPos)) else: super(IDView, self).mousePressEvent(event) def mouseReleaseEvent(self, event): if self.boxFlag: self.boxFlag = False self.parent.rectangle = self.boxItem.rect() self.parent.whichFile = self.parent.vTW.currentIndex() return """Left/right click to zoom in and out""" if (event.button() == Qt.RightButton) or ( QGuiApplication.queryKeyboardModifiers() == Qt.ShiftModifier): self.scale(0.8, 0.8) else: self.scale(1.25, 1.25) self.centerOn(event.pos()) def resetView(self): """Reset the view to its reasonable initial state.""" self.fitInView(self.imageGItem, Qt.KeepAspectRatio)
class GameArea(QWidget): #constants of widget RATIO_WITH_SHIPLIST = 11 / 14 RATIO_WITHOUT_SHIPLIST = 1 EPS = 0.3 #signals shipPlaced = pyqtSignal(Ship) def __init__(self, parent=None): super(GameArea, self).__init__(parent) self.__ui = Ui_GameArea() self.__ui.setupUi(self) self.__ratio = self.RATIO_WITH_SHIPLIST self.__scaleFactor = 1 self.controller = Controller() self.controller._accept = self.__accept self.controller._decline = self.__decline self.__gameModel = None self.__shipList = { "boat": ShipListItem(length=1, name="boat", count=4), "destroyer": ShipListItem(length=2, name="destroyer", count=3), "cruiser": ShipListItem(length=3, name="cruiser", count=2), "battleship": ShipListItem(length=4, name="battleship", count=1), } # resources self.__cellImages = {"intact": None, "hit": None, "miss": None} self.__sprites = {"explosion": None, "splash": None} self.__originalTileSize = 0 self.tileSize = 0 self.__shipListImage = QImage() self.__counterImage = QImage() # drawing items self.__placedShips = [] self.__field = [QGraphicsPixmapItem() for _ in range(100)] self.__letters = [QGraphicsTextItem() for _ in range(10)] self.__numbers = [QGraphicsTextItem() for _ in range(10)] for i in range(10): self.__letters[i].setDefaultTextColor(QColor.fromRgb(0, 0, 0)) self.__numbers[i].setDefaultTextColor(QColor.fromRgb(0, 0, 0)) self.__spriteAnimations = [] self.__shipListItem = QGraphicsPixmapItem() self.__ghostShip = QGraphicsPixmapItem( ) # data: 0 - rotation; 1 - ShipListItem self.__placer = QGraphicsRectItem() self.__dragShip = False self.__targetPixmap = None self.__targets = [] self.__scanEffect = None # prepare Qt objects self.__scene = QGraphicsScene() self.__loadResources() self.__initGraphicsView() self.__adjustedToSize = 0 def serviceModel(self, game_model): self.removeModel() self.__gameModel = game_model self.__gameModel.shipKilled.connect(self.__shootAround) def removeModel(self): if self.__gameModel: self.shipKilled.disconnect() self.__gameModel = None def __loadResources(self): if DEBUG_RESOURCE: resourcesPath = os.path.join(os.path.dirname(__file__), DEBUG_RESOURCE) else: resourcesPath = Environment.Resources.path() first = True for imageName in self.__cellImages: image = QImage( os.path.join(resourcesPath, "img", "cells", f"{imageName}.png")) if first: first = False self.__originalTileSize = min(image.width(), image.height()) else: self.__originalTileSize = min(self.__originalTileSize, image.width(), image.height()) self.__cellImages[imageName] = image for spriteName in self.__sprites: image = QImage( os.path.join(resourcesPath, "img", "cells", f"{spriteName}.png")) self.__sprites[spriteName] = image for shipName, ship in self.__shipList.items(): ship.image = QImage( os.path.join(resourcesPath, "img", "ships", f"{shipName}.png")) self.__shipListImage = QImage( os.path.join(resourcesPath, "img", "backgrounds", "shiplist.png")) self.__counterImage = QImage( os.path.join(resourcesPath, "img", "miscellaneous", "shipcounter.png")) self.__targetPixmap = QPixmap( os.path.join(resourcesPath, "img", "cells", "target.png")) def __initGraphicsView(self): self.__ui.graphicsView.setScene(self.__scene) self.__ui.graphicsView.viewport().installEventFilter(self) self.__ui.graphicsView.setRenderHints( QPainter.RenderHint.HighQualityAntialiasing | QPainter.RenderHint.TextAntialiasing | QPainter.RenderHint.SmoothPixmapTransform) self.__ui.graphicsView.horizontalScrollBar().blockSignals(True) self.__ui.graphicsView.verticalScrollBar().blockSignals(True) for cell in self.__field: cell.setData(0, "intact") pixmap = QPixmap.fromImage(self.__cellImages["intact"]) cell.setPixmap(pixmap) cell.setTransformationMode( Qt.TransformationMode.SmoothTransformation) self.__scene.addItem(cell) ca_letters = ["А", "Б", "В", "Г", "Д", "Е", "Ж", "З", "И", "К"] # ca_letters = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"] ca_numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] font = QFont("Roboto") font.setPixelSize(int(self.__originalTileSize * 0.8)) for i in range(10): self.__letters[i].setPlainText(ca_letters[i]) self.__letters[i].setFont(font) self.__numbers[i].setPlainText(ca_numbers[i]) self.__numbers[i].setFont(font) self.__scene.addItem(self.__letters[i]) self.__scene.addItem(self.__numbers[i]) self.__shipListItem.setPixmap(QPixmap.fromImage(self.__shipListImage)) self.__shipListItem.setTransformationMode( Qt.TransformationMode.SmoothTransformation) self.__scene.addItem(self.__shipListItem) font.setPixelSize(int(self.__originalTileSize * 0.3)) for _, ship in self.__shipList.items(): t = QTransform().rotate(90) ship.shipItem.setPixmap( QPixmap.fromImage(ship.image).transformed(t)) ship.shipItem.setTransformationMode( Qt.TransformationMode.SmoothTransformation) ship.counterItem.setPixmap(QPixmap.fromImage(self.__counterImage)) ship.counterItem.setTransformationMode( Qt.TransformationMode.SmoothTransformation) ship.counterText.setPlainText(str(ship.count)) ship.counterText.setFont(font) self.__scene.addItem(ship.shipItem) self.__scene.addItem(ship.counterItem) self.__scene.addItem(ship.counterText) self.__ghostShip.setTransformationMode( Qt.TransformationMode.SmoothTransformation) self.__ghostShip.setOpacity(0.7) pen = QPen() pen.setWidth(2) pen.setStyle(Qt.PenStyle.DashLine) pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin) self.__placer.setPen(pen) def __setCell(self, x, y, cell_type): if cell_type not in self.__cellImages.keys(): raise ValueError( f"Type is {cell_type}. Allowed \"intact\", \"miss\", \"hit\"") cellItem = self.__field[y * 10 + x] cellItem.setData(0, cell_type) pixmap = QPixmap.fromImage(self.__cellImages[cell_type]) cellItem.setPixmap(pixmap) def __runAnimation(self, x, y, animation, looped=False): sprite = SpriteItem() sprite.setData(0, QPoint(x, y)) spritePixmap = QPixmap.fromImage(self.__sprites[animation]) sprite.setSpriteMap(spritePixmap, 60, 60, 5) sprite.setScale(self.__scaleFactor) sprite.setPos((x + 1) * self.tileSize, (y + 1) * self.tileSize) sprite.setZValue(10) self.__spriteAnimations.append(sprite) self.__scene.addItem(sprite) def removeAnimation(): self.__scene.removeItem(sprite) self.__spriteAnimations.remove(sprite) sprite.stopAnimation() sprite.startAnimation(100, looped, removeAnimation) def __shootAround(self, ship: Ship): shipCells = [] for i in range(ship.length): shipCells.append(ship.pos + (QPoint(0, i) if ship.vertical else QPoint(i, 0))) cells = [] for cell in shipCells: arounds = [ cell + QPoint(i, j) for i in range(-1, 2) for j in range(-1, 2) ] for candidate in arounds: if 0 <= candidate.x() < 10 and 0 <= candidate.y() < 10: if candidate not in cells: cells.append(candidate) for cell in cells: if cell not in shipCells: self.__setCell(cell.x(), cell.y(), "miss") self.__runAnimation(cell.x(), cell.y(), "splash") log.debug( f"name: {ship.name} | length: {ship.length} | pos: {ship.pos} | vertical: {ship.vertical}" ) def hasHeightForWidth(self): return True def heightForWidth(self, width): return width / self.__ratio def hideShipList(self): self.__ratio = self.RATIO_WITHOUT_SHIPLIST self.__scene.removeItem(self.__shipListItem) for _, ship in self.__shipList.items(): self.__scene.removeItem(ship.shipItem) self.__adjustedToSize = None resize = QResizeEvent(self.size(), self.size()) QApplication.postEvent(self, resize) def getPlacedShips(self): return self.__placedShips def placedShipsCount(self): return len(self.__placedShips) def hideShips(self): for ship in self.__placedShips: self.__scene.removeItem(ship) def removePlacedShips(self): if self.__shipListItem.scene() is None: return for ship in self.__placedShips: shipListItem = ship.data(1) shipListItem.count += 1 shipListItem.counterText.setPlainText(str(shipListItem.count)) self.__scene.removeItem(ship) self.__placedShips.clear() def shuffleShips(self): if self.__shipListItem.scene() is None: return self.removePlacedShips() def findPossibleCells(length, rotation): vertical = rotation.isVertical() if vertical == rotation.isHorizontal(): raise Exception( "Unknown state! Rotation is not horizontal and not vertical." ) # wtf width = 1 if vertical else length height = length if vertical else 1 cells = [] for x in range(10): for y in range(10): if QRect(0, 0, 10, 10) != QRect(0, 0, 10, 10).united( QRect(x, y, width, height)): break if self.__validatePosition(x, y, width, height): cells.append(QPoint(x, y)) return cells shipList = list(self.__shipList.values()) shipList.sort(key=lambda ship: ship.length, reverse=True) for shipItem in shipList: for i in range(shipItem.count): rot = random.choice(list(Rotation)) cells = findPossibleCells(shipItem.length, rot) if not cells: rot = rot.next() cells = findPossibleCells(shipItem.length, rot) if not cells: return cell = random.choice(cells) self.__placeShip(shipItem, cell.x(), cell.y(), rot) def resizeEvent(self, event): size = event.size() if size == self.__adjustedToSize: return self.__adjustedToSize = size nowWidth = size.width() nowHeight = size.height() width = min(nowWidth, nowHeight * self.__ratio) height = min(nowHeight, nowWidth / self.__ratio) h_margin = round((nowWidth - width) / 2) - 2 v_margin = round((nowHeight - height) / 2) - 2 self.setContentsMargins( QMargins(h_margin, v_margin, h_margin, v_margin)) self.__resizeScene() def scanEffect(self, x, y): """ :return: True on success, False otherwise """ if self.__scanEffect: return False def scanFinisedCallback(): self.__scene.removeItem(self.__scanEffect) del self.__scanEffect self.__scanEffect = None target = FadingPixmapItem(self.__targetPixmap) target.setPos((x + 1) * self.tileSize, (y + 1) * self.tileSize) target.setScale(self.__scaleFactor) target.setData(0, (x, y)) self.__targets.append(target) self.__scene.addItem(target) self.__scanEffect = FloatingGradientItem( QRectF(self.tileSize, self.tileSize, self.__originalTileSize * 10, self.__originalTileSize * 10), scanFinisedCallback) self.__scanEffect.setScale(self.__scaleFactor) self.__scanEffect.setBackwards(True) self.__scene.addItem(self.__scanEffect) return True def __resizeScene(self): width = self.__ui.graphicsView.width() height = self.__ui.graphicsView.height() self.__scene.setSceneRect(0, 0, width, height) self.tileSize = min(width / 11, height / (11 / self.__ratio)) self.__scaleFactor = self.tileSize / self.__originalTileSize for i, cell in enumerate(self.__field): x = i % 10 y = i // 10 cell.setScale(self.__scaleFactor) cell.setPos((x + 1) * self.tileSize, (y + 1) * self.tileSize) for sprite in self.__spriteAnimations: pos = sprite.data(0) sprite.setPos((pos.x() + 1) * self.tileSize, (pos.y() + 1) * self.tileSize) sprite.setScale(self.__scaleFactor) for i in range(10): letter = self.__letters[i] letter.setScale(self.__scaleFactor) offsetX = (self.tileSize - letter.boundingRect().width() * self.__scaleFactor) / 2 offsetY = (self.tileSize - letter.boundingRect().height() * self.__scaleFactor) / 2 letter.setPos((i + 1) * self.tileSize + offsetX, offsetY) number = self.__numbers[i] number.setScale(self.__scaleFactor) offsetX = (self.tileSize - number.boundingRect().width() * self.__scaleFactor) / 2 offsetY = (self.tileSize - number.boundingRect().height() * self.__scaleFactor) / 2 number.setPos(offsetX, (i + 1) * self.tileSize + offsetY) xPos = 0 for _, ship in self.__shipList.items(): xPos += (ship.length - 1) xOffset = xPos * self.tileSize ship.shipItem.setScale(self.__scaleFactor) ship.shipItem.setPos(self.tileSize + xOffset, self.tileSize * (12 + self.EPS)) ship.counterItem.setScale(self.__scaleFactor) ship.counterItem.setPos(self.tileSize + xOffset, self.tileSize * (12.65 + self.EPS)) counterSize = ship.counterItem.boundingRect() textSize = ship.counterText.boundingRect() textXOffset = (counterSize.width() - textSize.width()) * self.__scaleFactor / 2 textYOffset = (counterSize.height() - textSize.height()) * self.__scaleFactor / 2 textX = ship.counterItem.pos().x() + textXOffset textY = ship.counterItem.pos().y() + textYOffset ship.counterText.setScale(self.__scaleFactor) ship.counterText.setPos(textX, textY) for ship in self.__placedShips: mapPos = ship.data(2) ship.setPos((mapPos.x() + 1) * self.tileSize, (mapPos.y() + 1) * self.tileSize) ship.setScale(self.__scaleFactor) for target in self.__targets: x, y = target.data(0) target.setPos((x + 1) * self.tileSize, (y + 1) * self.tileSize) target.setScale(self.__scaleFactor) if self.__scanEffect: self.__scanEffect.setPos(self.tileSize, self.tileSize) self.__scanEffect.setScale(self.__scaleFactor) shipListX = self.tileSize shipListY = self.tileSize * (11 + self.EPS) self.__shipListItem.setScale(self.__scaleFactor) self.__shipListItem.setPos(shipListX, shipListY) self.__ghostShip.setScale(self.__scaleFactor) def eventFilter(self, obj, event): if obj is self.__ui.graphicsView.viewport(): if event.type() == QEvent.MouseButtonPress: self.__viewportMousePressEvent(event) elif event.type() == QEvent.MouseButtonRelease: self.__viewportMouseReleaseEvent(event) elif event.type() == QEvent.MouseMove: self.__viewportMouseMoveEvent(event) return super().eventFilter(obj, event) def sceneToMap(self, x, y): x -= self.tileSize y -= self.tileSize x //= self.tileSize y //= self.tileSize return int(x), int(y) def __initGhostShip(self, ship, pos): self.__ghostShip.setPixmap(QPixmap.fromImage(ship.image)) self.__ghostShip.setPos(pos) self.__ghostShip.setRotation(0) width = self.__ghostShip.boundingRect().width() height = self.__ghostShip.boundingRect().height() self.__ghostShip.setOffset(-width / 2, -height / 2) self.__ghostShip.setData(0, Rotation.UP) self.__ghostShip.setData(1, ship) self.__placer.setRect(0, 0, self.tileSize, self.tileSize * ship.length) self.__placer.setZValue(50) self.__scene.addItem(self.__ghostShip) self.__ghostShip.setZValue(100) def __rotateGhostShip(self, rotation=None): rotation = rotation if rotation else self.__ghostShip.data(0).next() self.__ghostShip.setData(0, rotation) self.__ghostShip.setRotation(rotation.value) placerRect = self.__placer.rect() maxSide = max(placerRect.width(), placerRect.height()) minSide = min(placerRect.width(), placerRect.height()) if rotation.isHorizontal(): self.__placer.setRect(0, 0, maxSide, minSide) elif rotation.isVertical(): self.__placer.setRect(0, 0, minSide, maxSide) else: raise Exception( "Unknown state! Rotation is not horizontal and not vertical." ) # wtf self.__validatePlacer() def __ghostShipLongSurface(self): pos = self.__ghostShip.pos() x = pos.x() y = pos.y() rot: Rotation = self.__ghostShip.data(0) length = self.__ghostShip.data(1).length if rot.isHorizontal(): x -= self.tileSize * length / 2 return x, y if rot.isVertical(): y -= self.tileSize * length / 2 return x, y def __validatePosition(self, x, y, width=1, height=1): positionRect = QRectF((x + 1) * self.tileSize, (y + 1) * self.tileSize, self.tileSize * width, self.tileSize * height) isPlacerValid = True for ship in self.__placedShips: shipRect = ship.mapRectToScene(ship.boundingRect()).adjusted( -self.tileSize / 2, -self.tileSize / 2, self.tileSize / 2, self.tileSize / 2) isPlacerValid = not positionRect.intersects(shipRect) if not isPlacerValid: break return isPlacerValid def __validatePlacer(self): sceneX, sceneY = self.__ghostShipLongSurface() x, y = self.sceneToMap(sceneX, sceneY) self.__placer.setPos((x + 1) * self.tileSize, (y + 1) * self.tileSize) permittedArea = QRectF(self.__ui.graphicsView.viewport().geometry()) permittedArea.setTopLeft(QPointF(self.tileSize, self.tileSize)) permittedArea.setBottomRight( QPointF(self.tileSize * 12, self.tileSize * 12)) placerSize = self.__placer.boundingRect() placerRect = QRectF(sceneX, sceneY, placerSize.width(), placerSize.height()) isPlacerValid = False # first validation - ship can be placed inside game field if permittedArea.contains(self.__ghostShip.pos( )) and permittedArea == permittedArea.united(placerRect): if self.__placer.scene() == None: self.__scene.addItem(self.__placer) x, y = self.sceneToMap(sceneX, sceneY) self.__placer.setPos((x + 1) * self.tileSize, (y + 1) * self.tileSize) isPlacerValid = True else: if self.__placer.scene() == self.__scene: self.__scene.removeItem(self.__placer) # second validation - placer does not intersect with other ships if isPlacerValid: isPlacerValid = self.__validatePosition( x, y, placerSize.width() / self.tileSize, placerSize.height() / self.tileSize) # set color of placer pen = self.__placer.pen() if isPlacerValid: pen.setColor(Qt.GlobalColor.darkGreen) else: pen.setColor(Qt.GlobalColor.red) self.__placer.setPen(pen) self.__placer.setData(0, isPlacerValid) def __placeShip(self, shipListItem, x, y, rotation): sceneX, sceneY = (x + 1) * self.tileSize, (y + 1) * self.tileSize shipListItem.count -= 1 shipListItem.counterText.setPlainText(str(shipListItem.count)) pixmap = QPixmap(shipListItem.image).transformed(QTransform().rotate( (rotation.value))) placedShip = QGraphicsPixmapItem(pixmap) placedShip.setData(0, rotation) placedShip.setData(1, shipListItem) placedShip.setData(2, QPoint(x, y)) # position in map coordinates placedShip.setPos(sceneX, sceneY) placedShip.setTransformationMode( Qt.TransformationMode.SmoothTransformation) placedShip.setScale(self.__scaleFactor) self.__placedShips.append(placedShip) self.__scene.addItem(placedShip) def __placeGhostShip(self): isPlacingPermitted = self.__placer.data(0) if isPlacingPermitted: sceneX = self.__placer.pos().x() + self.tileSize / 2 sceneY = self.__placer.pos().y() + self.tileSize / 2 mapX, mapY = self.sceneToMap(sceneX, sceneY) rotation = self.__ghostShip.data(0) if rotation.isVertical(): vertical = True elif rotation.isHorizontal(): vertical = False else: raise Exception( "Unknown state! Rotation is not horizontal and not vertical." ) # wtf shipListItem = self.__ghostShip.data(1) self.__placeShip(shipListItem, mapX, mapY, rotation) log.debug( f"ship \"{shipListItem.name}\"; " f"position ({mapX}, {mapY}); " f"oriented {'vertically' if vertical else 'horizontally'}") self.shipPlaced.emit( Ship(name=shipListItem.name, length=shipListItem.length, pos=QPoint(mapX, mapY), vertical=vertical)) def __viewportMousePressEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: if self.__shipListItem.scene(): # check press on shiplist shipUnderMouse = None for _, ship in self.__shipList.items(): if ship.shipItem.isUnderMouse(): shipUnderMouse = ship break # check press on field rotation = Rotation.RIGHT if shipUnderMouse == None: for ship in self.__placedShips: if ship.isUnderMouse(): rotation = ship.data(0) shipListItem = ship.data(1) if shipListItem.count == 0: shipListItem.shipItem.setGraphicsEffect(None) shipListItem.count += 1 shipListItem.counterText.setPlainText( str(shipListItem.count)) shipUnderMouse = shipListItem self.__placedShips.remove(ship) self.__scene.removeItem(ship) break # if ship grabbed if shipUnderMouse and shipUnderMouse.count > 0: self.__initGhostShip(shipUnderMouse, event.pos()) self.__rotateGhostShip(rotation) self.__dragShip = True x, y = self.sceneToMap(event.pos().x(), event.pos().y()) if x >= 0 and x < 10 and y >= 0 and y < 10: if self.controller.isBot: return self.controller.emitHit(x, y) if event.button() == Qt.MouseButton.RightButton: if self.__dragShip: self.__rotateGhostShip() def __viewportMouseReleaseEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: if self.__dragShip: self.__scene.removeItem(self.__ghostShip) self.__dragShip = False if self.__placer.scene() != None: self.__scene.removeItem(self.__placer) # self.__placeShip() self.__placeGhostShip() self.__placer.setData(0, False) def __viewportMouseMoveEvent(self, event): if self.__dragShip: self.__ghostShip.setPos(event.pos()) permittedArea = QRectF( self.__ui.graphicsView.viewport().geometry()) permittedArea.setTopLeft(QPointF(self.tileSize, self.tileSize)) # stop dragging ship if mouse out of field # if not permittedArea.contains(event.pos()): # self.__scene.removeItem(self.__ghostShip) # self.__dragShip = False self.__validatePlacer() def __accept(self, x, y, hit_type: CellState): log.debug( f" -- ACCEPTED -- hit on point ({x}, {y}) hit type: {hit_type}") cell = self.__field[y * 10 + x] if cell.data(0) != "intact": return if hit_type in [CellState.HIT, CellState.KILLED]: self.__setCell(x, y, 'hit') self.__runAnimation(x, y, "explosion", looped=True) for target in self.__targets: if (x, y) == target.data(0): self.__scene.removeItem(target) self.__targets.remove(target) break elif hit_type in [CellState.MISS]: self.__setCell(x, y, 'miss') self.__runAnimation(x, y, "splash", looped=False) if hit_type == CellState.KILLED: for ship in self.__placedShips: rotation = ship.data(0) length = ship.data(1).length position = ship.data(2) width = 1 if rotation.isVertical() else length height = length if rotation.isVertical() else 1 rect = QRect(position.x(), position.y(), width, height) if rect.contains(x, y): self.__scene.addItem(ship) def __decline(self, x, y): log.debug(f"declined hit on point ({x}, {y})")