class QMemoryDataBlock(QCachedGraphicsItem): ADDRESS_LABEL_OFFSET = 20 LINEAR_INSTRUCTION_OFFSET = 120 BYTE_AREA_SPACING = 15 def __init__(self, workspace, infodock, addr, memory_data, bytes_per_line=16, parent=None): super().__init__(parent=parent) self.workspace = workspace self.infodock = infodock self.addr = addr self.memory_data: MemoryData = memory_data self.bytes_per_line: int = bytes_per_line # TODO: Move it to Conf self._addr_text = None self._width = None self._height = None self._bytes = [ ] # widgets self._addr_item: QGraphicsSimpleTextItem = None self._label_item: Optional[QGraphicsSimpleTextItem] = None self._line_items: List[Tuple[int, QGraphicsSimpleTextItem, List[QGraphicsSimpleTextItem], List[QGraphicsSimpleTextItem]]] = None self._init_widgets() # # Public methods # @property def width(self): return self.boundingRect().width() @property def height(self): return self.boundingRect().height() def paint(self, painter, option, widget): should_highlight = self.infodock.is_label_selected(self.addr) highlight_color = Conf.disasm_view_label_highlight_color if should_highlight: painter.setBrush(highlight_color) painter.setPen(highlight_color) painter.drawRect(0, 0, self.width, self.height) # # Event handlers # def mousePressEvent(self, event): if event.button() == Qt.LeftButton: # unselect all other labels self.infodock.unselect_all_labels() # select this label self.infodock.select_label(self.addr) # # Private methods # def _init_widgets(self): self._addr_text = "%08x" % self.addr self._bytes = [ ] if self.memory_data.content: for byt in self.memory_data.content: self._bytes.append(byt) if len(self._bytes) < self.memory_data.size: # load more from mapped memory start_address = self.memory_data.addr + len(self._bytes) size = self.memory_data.size - len(self._bytes) try: mem_bytes = self.workspace.instance.project.loader.memory.load(start_address, size) except KeyError: mem_bytes = b"" self._bytes += [ b for b in mem_bytes ] + [ '??' ] * (size - len(mem_bytes)) # address self._addr_item = QGraphicsSimpleTextItem(self._addr_text, self) self._addr_item.setFont(Conf.disasm_font) self._addr_item.setBrush(Conf.disasm_view_node_address_color) # label self._init_label_item() # bytes self._init_bytes() self._layout_items_and_update_size() def _init_label_item(self): lbl_text = get_label_text(self.addr, self.workspace.instance.kb) if lbl_text: self._label_item = QGraphicsSimpleTextItem(lbl_text, self) self._label_item.setFont(Conf.code_font) self._label_item.setBrush(Conf.disasm_view_label_color) else: if self._label_item is not None: self._label_item.setParentItem(None) self._label_item = None def _init_bytes(self): if self._line_items: # remove existing items for line in self._line_items: for item in line: item.setParentItem(None) self._line_items = None addr = self.addr i = 0 self._line_items = [] while i < len(self._bytes): byte_offset = addr % self.bytes_per_line if byte_offset == 0: end_pos = i + self.bytes_per_line else: end_pos = self.bytes_per_line - byte_offset all_bytes = self._bytes[i : end_pos] # print("... print %#x, %d bytes, byte_offset %d" % (addr, len(all_bytes), byte_offset)) addr_item, bytes_list, character_list = self._init_line(addr, byte_offset, all_bytes) self._line_items.append((byte_offset, addr_item, bytes_list, character_list)) addr += end_pos - i i = end_pos def _init_line(self, addr, byte_offset, all_bytes): # colors printable_byte_color = Conf.disasm_view_printable_byte_color printable_char_color = Conf.disasm_view_printable_character_color unprintable_byte_color = Conf.disasm_view_unprintable_byte_color unprintable_char_color = Conf.disasm_view_unprintable_character_color unknown_byte_color = Conf.disasm_view_unknown_byte_color unknown_char_color = Conf.disasm_view_unknown_character_color # address addr_text = "%08x" % addr addr_item = QGraphicsSimpleTextItem(addr_text, self) addr_item.setBrush(Conf.disasm_view_node_address_color) addr_item.setFont(Conf.disasm_font) # draw each byte bytes_list = [ ] for idx, byt in enumerate(all_bytes): if type(byt) is int: if is_printable(byt): color = printable_byte_color else: color = unprintable_byte_color o = QGraphicsSimpleTextItem("%02x" % byt, self) o.setFont(Conf.disasm_font) o.setBrush(color) else: # str, usually because it is an unknown byte, in which case the str is "??" o = QGraphicsSimpleTextItem(byt, self) o.setBrush(unknown_byte_color) o.setFont(Conf.disasm_font) bytes_list.append(o) line_chars = byte_offset + idx + 1 # the number of existing characters on this line, including spaces if line_chars % 8 == 0 and line_chars != self.bytes_per_line: # print a deliminator o = QGraphicsSimpleTextItem("-", self) o.setBrush(Qt.black) o.setFont(Conf.disasm_font) bytes_list.append(o) # printable characters character_list = [ ] for byt in all_bytes: if type(byt) is int: if is_printable(byt): color = printable_char_color ch = chr(byt) else: color = unprintable_char_color ch = "." else: color = unknown_char_color ch = "?" o = QGraphicsSimpleTextItem(ch, self) o.setBrush(color) o.setFont(Conf.disasm_font) character_list.append(o) return addr_item, bytes_list, character_list def _layout_items_and_update_size(self): x, y = 0, 0 # # first line # # address self._addr_item.setPos(x, y) x += self._addr_item.boundingRect().width() # label if self._label_item: x += self.ADDRESS_LABEL_OFFSET self._label_item.setPos(x, y) # # the following lines: content # max_x = x x = 0 y += self._addr_item.boundingRect().height() for byte_offset, addr_item, bytes_line, characters_line in self._line_items: addr_item.setPos(x, y) x += addr_item.boundingRect().width() + self.LINEAR_INSTRUCTION_OFFSET # skip byte offset byte_width = bytes_line[0].boundingRect().width() byte_spacing = byte_width // 2 x += byte_offset * (byte_width + byte_spacing) all_bytes = 0 pos = 0 while pos < len(bytes_line): byte_ = bytes_line[pos] byte_.setPos(x, y) x += byte_width line_chars = byte_offset + all_bytes + 1 # the number of existing characters on this line, including spaces if line_chars % 8 == 0 and line_chars != self.bytes_per_line: # now we get a delimiter pos += 1 delimiter = bytes_line[pos] delimiter.setPos(x, y) x += byte_spacing pos += 1 all_bytes += 1 if (byte_offset + all_bytes) % self.bytes_per_line != 0: more_chars = self.bytes_per_line - (byte_offset + all_bytes % self.bytes_per_line) x += more_chars * (byte_width + byte_spacing) x += self.BYTE_AREA_SPACING # printable characters character_width = characters_line[0].boundingRect().width() x += byte_offset * character_width for o in characters_line: o.setPos(x, y) x += character_width max_x = max(x, max_x) # next line! x = 0 y += bytes_line[0].boundingRect().height() self._width = max_x self._height = y self.recalculate_size() def _boundingRect(self): return QRectF(0, 0, self._width, self._height)
class ProjectItemIcon(QGraphicsRectItem): def __init__(self, toolbox, x, y, w, h, project_item, icon_file, icon_color, background_color): """Base class for project item icons drawn in Design View. Args: toolbox (ToolBoxUI): QMainWindow instance x (float): Icon x coordinate y (float): Icon y coordinate w (float): Icon width h (float): Icon height project_item (ProjectItem): Item icon_file (str): Path to icon resource icon_color (QColor): Icon's color background_color (QColor): Background color """ super().__init__() self._toolbox = toolbox self._project_item = project_item self._moved_on_scene = False self.renderer = QSvgRenderer() self.svg_item = QGraphicsSvgItem() self.colorizer = QGraphicsColorizeEffect() self.setRect(QRectF(x, y, w, h)) # Set ellipse coordinates and size self.text_font_size = 10 # point size # Make item name graphics item. name = project_item.name if project_item else "" self.name_item = QGraphicsSimpleTextItem(name) shadow_effect = QGraphicsDropShadowEffect() shadow_effect.setOffset(1) shadow_effect.setEnabled(False) self.setGraphicsEffect(shadow_effect) self.set_name_attributes() # Set font, size, position, etc. # Make connector buttons self.connectors = dict( bottom=ConnectorButton(self, toolbox, position="bottom"), left=ConnectorButton(self, toolbox, position="left"), right=ConnectorButton(self, toolbox, position="right"), ) # Make exclamation and rank icons self.exclamation_icon = ExclamationIcon(self) self.rank_icon = RankIcon(self) # Group the drawn items together by setting the background rectangle as the parent of other QGraphicsItems # NOTE: setting the parent item moves the items as one! self.name_item.setParentItem(self) for conn in self.connectors.values(): conn.setParentItem(self) self.svg_item.setParentItem(self) self.exclamation_icon.setParentItem(self) self.rank_icon.setParentItem(self) brush = QBrush(background_color) self._setup(brush, icon_file, icon_color) # Add items to scene scene = self._toolbox.ui.graphicsView.scene() scene.addItem(self) def _setup(self, brush, svg, svg_color): """Setup item's attributes. Args: brush (QBrush): Used in filling the background rectangle svg (str): Path to SVG icon file svg_color (QColor): Color of SVG icon """ self.setPen(QPen(Qt.black, 1, Qt.SolidLine)) self.setBrush(brush) self.colorizer.setColor(svg_color) # Load SVG loading_ok = self.renderer.load(svg) if not loading_ok: self._toolbox.msg_error.emit( "Loading SVG icon from resource:{0} failed".format(svg)) return size = self.renderer.defaultSize() self.svg_item.setSharedRenderer(self.renderer) self.svg_item.setElementId( "") # guess empty string loads the whole file dim_max = max(size.width(), size.height()) rect_w = self.rect().width() # Parent rect width margin = 32 self.svg_item.setScale((rect_w - margin) / dim_max) x_offset = (rect_w - self.svg_item.sceneBoundingRect().width()) / 2 y_offset = (rect_w - self.svg_item.sceneBoundingRect().height()) / 2 self.svg_item.setPos(self.rect().x() + x_offset, self.rect().y() + y_offset) self.svg_item.setGraphicsEffect(self.colorizer) self.setFlag(QGraphicsItem.ItemIsMovable, enabled=True) self.setFlag(QGraphicsItem.ItemIsSelectable, enabled=True) self.setFlag(QGraphicsItem.ItemIsFocusable, enabled=True) self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, enabled=True) self.setAcceptHoverEvents(True) self.setCursor(Qt.PointingHandCursor) # Set exclamation and rank icons position self.exclamation_icon.setPos( self.rect().topRight() - self.exclamation_icon.sceneBoundingRect().topRight()) self.rank_icon.setPos(self.rect().topLeft()) def name(self): """Returns name of the item that is represented by this icon.""" return self._project_item.name def update_name_item(self, new_name): """Set a new text to name item. Used when a project item is renamed.""" self.name_item.setText(new_name) self.set_name_attributes() def set_name_attributes(self): """Set name QGraphicsSimpleTextItem attributes (font, size, position, etc.)""" # Set font size and style font = self.name_item.font() font.setPointSize(self.text_font_size) font.setBold(True) self.name_item.setFont(font) # Set name item position (centered on top of the master icon) name_width = self.name_item.boundingRect().width() name_height = self.name_item.boundingRect().height() self.name_item.setPos( self.rect().x() + self.rect().width() / 2 - name_width / 2, self.rect().y() - name_height - 4) def conn_button(self, position="left"): """Returns items connector button (QWidget).""" return self.connectors.get(position, self.connectors["left"]) def outgoing_links(self): return [ l for conn in self.connectors.values() for l in conn.outgoing_links() ] def incoming_links(self): return [ l for conn in self.connectors.values() for l in conn.incoming_links() ] def hoverEnterEvent(self, event): """Sets a drop shadow effect to icon when mouse enters its boundaries. Args: event (QGraphicsSceneMouseEvent): Event """ self.prepareGeometryChange() self.graphicsEffect().setEnabled(True) event.accept() def hoverLeaveEvent(self, event): """Disables the drop shadow when mouse leaves icon boundaries. Args: event (QGraphicsSceneMouseEvent): Event """ self.prepareGeometryChange() self.graphicsEffect().setEnabled(False) event.accept() def mouseMoveEvent(self, event): """Moves icon(s) while the mouse button is pressed. Update links that are connected to selected icons. Args: event (QGraphicsSceneMouseEvent): Event """ super().mouseMoveEvent(event) selected_icons = set(x for x in self.scene().selectedItems() if isinstance(x, ProjectItemIcon)) links = set(link for icon in selected_icons for conn in icon.connectors.values() for link in conn.links) for link in links: link.update_geometry() def mouseReleaseEvent(self, event): if self._moved_on_scene: self._moved_on_scene = False scene = self.scene() scene.shrink_if_needed() scene.item_move_finished.emit(self) super().mouseReleaseEvent(event) def contextMenuEvent(self, event): """Show item context menu. Args: event (QGraphicsSceneMouseEvent): Mouse event """ self.scene().clearSelection() self.setSelected(True) self._toolbox.show_item_image_context_menu(event.screenPos(), self.name()) def keyPressEvent(self, event): """Handles deleting and rotating the selected item when dedicated keys are pressed. Args: event (QKeyEvent): Key event """ if event.key() == Qt.Key_Delete and self.isSelected(): ind = self._toolbox.project_item_model.find_item(self.name()) delete_int = int(self._toolbox.qsettings().value( "appSettings/deleteData", defaultValue="0")) delete_bool = delete_int != 0 self._toolbox.remove_item(ind, delete_item=delete_bool) event.accept() elif event.key() == Qt.Key_R and self.isSelected(): # TODO: # 1. Change name item text direction when rotating # 2. Save rotation into project file rect = self.mapToScene(self.boundingRect()).boundingRect() center = rect.center() t = QTransform() t.translate(center.x(), center.y()) t.rotate(90) t.translate(-center.x(), -center.y()) self.setPos(t.map(self.pos())) self.setRotation(self.rotation() + 90) links = set(lnk for conn in self.connectors.values() for lnk in conn.links) for link in links: link.update_geometry() event.accept() else: super().keyPressEvent(event) def itemChange(self, change, value): """ Reacts to item removal and position changes. In particular, destroys the drop shadow effect when the items is removed from a scene and keeps track of item's movements on the scene. Args: change (GraphicsItemChange): a flag signalling the type of the change value: a value related to the change Returns: Whatever super() does with the value parameter """ if change == QGraphicsItem.ItemScenePositionHasChanged: self._moved_on_scene = True elif change == QGraphicsItem.GraphicsItemChange.ItemSceneChange and value is None: self.prepareGeometryChange() self.setGraphicsEffect(None) return super().itemChange(change, value) def show_item_info(self): """Update GUI to show the details of the selected item.""" ind = self._toolbox.project_item_model.find_item(self.name()) self._toolbox.ui.treeView_project.setCurrentIndex(ind)