def testQGraphicsProxyWidget(self):
        scene = QGraphicsScene()

        proxy = QGraphicsProxyWidget(None, Qt.Window)
        widget = QLabel('Widget')
        proxy.setWidget(widget)
        proxy.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
        scene.addItem(proxy)
        scene.setSceneRect(scene.itemsBoundingRect())

        view = QGraphicsView(scene)
        view.setRenderHints(QPainter.Antialiasing|QPainter.SmoothPixmapTransform)
        view.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)
        view.show()

        timer = QTimer.singleShot(100, self.app.quit)
        self.app.exec_()
Esempio n. 2
0
 def relationship_pixmap(self, str_object_class_name_list):
     """A pixmap for the given object_class name list,
     created by rendering several object pixmaps next to each other."""
     if not str_object_class_name_list:
         engine = CharIconEngine("\uf1b3", 0)
         return engine.pixmap(self.ICON_SIZE)
     object_class_name_list = tuple(str_object_class_name_list.split(","))
     if object_class_name_list in self.rel_cls_pixmap_cache:
         return self.rel_cls_pixmap_cache[object_class_name_list]
     scene = QGraphicsScene()
     x = 0
     for j, object_class_name in enumerate(object_class_name_list):
         pixmap = self.object_pixmap(object_class_name)
         pixmap_item = scene.addPixmap(pixmap)
         if j % 2 == 0:
             y = 0
         else:
             y = -0.875 * 0.75 * pixmap_item.boundingRect().height()
             pixmap_item.setZValue(-1)
         pixmap_item.setPos(x, y)
         x += 0.875 * 0.5 * pixmap_item.boundingRect().width()
     pixmap = QPixmap(scene.itemsBoundingRect().toRect().size())
     pixmap.fill(Qt.transparent)
     painter = QPainter(pixmap)
     painter.setRenderHint(QPainter.Antialiasing, True)
     scene.render(painter)
     painter.end()
     self.rel_cls_pixmap_cache[object_class_name_list] = pixmap
     return pixmap
class UiVScreenOverview:

    def __init__(self, view, vscreen):
        """
        :type view: PySide2.QtWidgets.QGraphicsView.QGraphicsView
        :param vscreen: hwmonitor.vscreen.vscreen.VScreen
        """
        self.view = view
        self.vscreen = vscreen

        self.view_margin = 10
        self.background_color = QColor(100, 100, 100)

        view.setInteractive(False)
        view.setStyleSheet('border: 0px;')
        view.setRenderHint(QPainter.Antialiasing)
        view.setCacheMode(QGraphicsView.CacheBackground)
        view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        self.scene = QGraphicsScene(view)
        self.scene.setBackgroundBrush(self.background_color)

        for monitor in self.vscreen.monitors:
            item = MonitorRepresentation(index_from_device_name(monitor.device_name), monitor)
            item.update_position()
            self.scene.addItem(item)

        view.setScene(self.scene)

    def update_position(self):
        for item in self.scene.items():
            item.update_position()

    def resize_scene(self):
        """Resize the scene to fit inside the available space of the viewport with an given margin.

        Update the scene rect, since it probably changes when changing the layout.
        """
        rect = self.scene.itemsBoundingRect()
        self.view.setSceneRect(rect)

        view_rect = self.view.viewport().rect().adjusted(self.view_margin, self.view_margin,
                                                         -self.view_margin, -self.view_margin)
        scene_rect = self.view.matrix().mapRect(rect)

        x_factor = view_rect.width() / scene_rect.width()
        y_factor = view_rect.height() / scene_rect.height()
        factor = min(x_factor, y_factor)

        self.view.scale(factor, factor)
        self.view.centerOn(rect.center())
Esempio n. 4
0
    def testQGraphicsProxyWidget(self):
        scene = QGraphicsScene()

        proxy = QGraphicsProxyWidget(None, Qt.Window)
        widget = QLabel('Widget')
        proxy.setWidget(widget)
        proxy.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
        scene.addItem(proxy)
        scene.setSceneRect(scene.itemsBoundingRect())

        view = QGraphicsView(scene)
        view.setRenderHints(QPainter.Antialiasing|QPainter.SmoothPixmapTransform)
        view.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)
        view.show()

        timer = QTimer.singleShot(100, self.app.quit)
        self.app.exec_()
 def _create_obj_group_renderer(self, object_class_name):
     display_icon = self.display_icons.get(object_class_name, -1)
     icon_code, color_code = interpret_icon_id(display_icon)
     font = QFont('Font Awesome 5 Free Solid')
     scene = QGraphicsScene()
     x = 0
     for _ in range(2):
         y = 0
         for _ in range(2):
             text_item = scene.addText(chr(icon_code), font)
             text_item.setDefaultTextColor(color_code)
             text_item.setPos(x, y)
             y += 0.875 * text_item.boundingRect().height()
         x += 0.875 * text_item.boundingRect().width()
     scene.addRect(scene.itemsBoundingRect())
     self.obj_group_renderers[
         object_class_name] = _SceneSvgRenderer.from_scene(scene)
Esempio n. 6
0
class DisplayWidget(QStackedWidget):
    imageClicked = Signal(int, QEvent)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.max_columns = None  # Set when presenter starts # Max cols for multimages
        self.cached_images = {}  # Cache read of image from disk

        self.start()

    def emitter(self, index, event):
        self.imageClicked.emit(index, event)

    def start(self):
        # Layout for single images
        self.single_image = QWidget()
        self.single_image_layout = QGridLayout()
        self.single_image_view = QGraphicsView()
        self.single_image_scene = QGraphicsScene()
        self.single_image_view.setScene(self.single_image_scene)
        self.single_image_layout.addWidget(self.single_image_view)
        self.single_image.setLayout(self.single_image_layout)

        # Layout for multiple images
        self.multiple_image = QWidget()
        self.multiple_image_view_layout = QVBoxLayout()
        self.multiple_image_layout = QGraphicsGridLayout()
        self.multiple_image_view = QGraphicsView()
        self.multiple_image_scene = QGraphicsScene()
        self.multiple_image_view.setScene(self.multiple_image_scene)
        self.panel = QGraphicsWidget()
        self.multiple_image_scene.addItem(self.panel)
        self.multiple_image_view_layout.addWidget(self.multiple_image_view)
        self.panel.setLayout(self.multiple_image_layout)
        self.multiple_image.setLayout(self.multiple_image_view_layout)

        self.addWidget(self.single_image)
        self.addWidget(self.multiple_image)

    def setMaxColumns(self, max_cols):
        self.max_columns = max_cols

    def images_load(self, images):
        """ Take list of images and display in main window """
        num = len(images)
        if num == 0:
            return

        width = self.width()
        maxcol = self.max_columns
        if num < maxcol:
            maxcol = num
        colwidth = width / maxcol

        # Set proper widget for display of multiple or single images
        if num > 1:
            self.setCurrentWidget(self.multiple_image)
            # Clear the layout
            while self.multiple_image_layout.count():
                self.multiple_image_layout.removeAt(0)
            # Clear the scene
            for child in self.panel.childItems():
                child.setParent(None)
        else:
            self.setCurrentWidget(self.single_image)
            self.single_image_scene.clear()
            self.single_image_scene.setSceneRect(
                self.single_image_scene.itemsBoundingRect())

        # Display images or image
        row = 0
        col = -1
        for index, image in images.items():
            col += 1
            if col >= maxcol:
                col = 0
                row += 1

            # Used any cached reads
            if index not in self.cached_images.keys():
                image_reader = QImageReader()
                image_reader.setDecideFormatFromContent(True)
                image_reader.setFileName(image.getFilename())
                self.cached_images[index] = image_reader.read()
            cached_image = self.cached_images[index]

            pixmap = QPixmap(cached_image)
            if num > 1:
                pixmap = pixmap.scaledToWidth(colwidth - 20)
                rec = MultiImageWidget(pixmap, basename(image.getFilename()),
                                       index)
                rec.imageClicked.connect(self.emitter)
                self.multiple_image_layout.addItem(rec, row, col)
            else:
                self.single_image_scene.addPixmap(pixmap)

        adjusted = self.multiple_image_scene.itemsBoundingRect()
        adjusted.adjust(0, 0, 0, 8 * row)
        self.multiple_image_scene.setSceneRect(adjusted)
class HumanVisualizationWidget(QGraphicsView):
    def __init__(self, parent=None):
        super(HumanVisualizationWidget, self).__init__(parent)
        self._scene = QGraphicsScene(self)
        self.setScene(self._scene)
        # circle = QGraphicsEllipseItem( 10, 10, 10 ,10)
        # self._scene.addItem(circle)
        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
        self._boxes = []
        self._humans = {}

    def load_inner_model(self, file):
        import xml.etree.cElementTree as ET
        tree = ET.ElementTree(file=file)
        root = tree.getroot()
        transforms = tree.findall(".//transform[plane]")
        walls = {}
        for trans in transforms:
            if 'id' in trans.attrib and 'pared' in trans.attrib['id']:
                print("Pared:", trans.attrib['id'])
                current_wall = [0] * 7
                # "wall5": [x, y, width, height, posx, posy, 0]
                if 'tx' in trans.attrib:
                    # print trans.attrib['tx']
                    current_wall[4] = int(float(trans.attrib['tx']))
                if 'ty' in trans.attrib:
                    # print trans.attrib['ty']
                    current_wall[5] = int(float(trans.attrib['tz']))
                # current_wall =
                planes = trans.findall('plane')
                for plane in planes:
                    if 'id' in plane.attrib and 'muro' in plane.attrib['id']:
                        # if 'nx' in plane.attrib:
                        # 	print plane.attrib['nx']
                        # if 'nz' in plane.attrib:
                        # 	print plane.attrib['nz']
                        if 'size' in plane.attrib:
                            # print int(float(plane.attrib['size'].split(',')[0])
                            current_wall[2] = int(
                                float(plane.attrib['size'].split(',')[0])) / 2.
                            # print int(float(plane.attrib['size'].split(',')[1])
                            current_wall[3] = int(
                                float(plane.attrib['size'].split(',')[1])) / 2.
                            if current_wall[2] < current_wall[3]:
                                current_wall[2] = 200
                            else:
                                current_wall[3] = 200
                walls[trans.attrib['id']] = current_wall
        for id in sorted(walls.keys()):
            object = walls[id]
            # rect = QRectF(-float(object[2]) / 2, -float(object[3]) / 2, float(object[2]), float(object[3]))
            rect = QRectF(0, 0, float(object[2]), float(object[3]))

            border = QPen(QColor("black"))
            fill = QBrush(QColor("black"))
            box = self._scene.addRect(rect, border, fill)

            self._scene.addEllipse(
                QRectF(float(object[4]), float(object[5]), 10, 10),
                QPen(QColor("green")), QBrush(QColor("green")))
            box.setPos(float(object[4]), float(object[5]))
            box.setRotation(float(object[6]))
            self._boxes.append(box)
            self._scene.update()
            QApplication.processEvents()
            sleep(1)

    def load_custom_json_world(self, file):

        if not os.path.isfile(file):
            print("Error reading world file, check config params:", file)
            return False

        with open(file, "r") as read_file:
            json_data = json.load(read_file)

        types_colors = {
            "tables": "SandyBrown",
            "roundTables": "Khaki",
            "walls": "Brown",
            "points": "Blue"
        }
        self.clear()
        for type, color in types_colors.items():
            if type in json_data:
                tables = json_data[type]
                for object in tables.values():
                    rect = QRectF(-float(object[2]) / 2, -float(object[3]) / 2,
                                  float(object[2]), float(object[3]))
                    border = QPen(QColor(color))
                    fill = QBrush(QColor(color))
                    if type == "roundTables":
                        box = self._scene.addEllipse(rect, border, fill)
                    else:
                        box = self._scene.addRect(rect, border, fill)

                    box.setPos(float(object[4]), float(object[5]))
                    box.setRotation(float(object[6]))
                    self._boxes.append(box)

    def load_json_world(self, file):

        if not os.path.isfile(file):
            print("Error reading world file, check config params:", file)
            return False

        with open(file, "r") as read_file:
            json_data = json.load(read_file)

            polygon_points = []
        paths_count = 0
        for item in json_data:
            if 'json_geometry' in item:
                geometry = item['json_geometry']
                if geometry['type'] == 'Polygon':
                    for coord in geometry['coordinates'][0]:

                        if isinstance(coord, list) and (
                            (isinstance(coord, list) and len(coord) == 2) or
                            (len(coord) == 3 and coord[3] == 0)):
                            current_point = QPointF(coord[0], coord[1])
                            polygon_points.append(current_point)
                        else:
                            print("Unknown coord", geometry["coordinates"][0])
                    polygon = QPolygonF(polygon_points)
                    path = QPainterPath()
                    path.addPolygon(polygon)
                    contour = QGraphicsPathItem(path)
                    # r = lambda: random.randint(0, 255)
                    # next_color = '#%02X%02X%02X' % (r(), r(), r())
                    contour.setPen(QPen(QColor("red"), 0.1))

                    contour.setBrush(QBrush(Qt.transparent))
                    # if paths_count == 4:
                    print(item['json_featuretype'])
                    self._scene.addItem(contour)
                    paths_count += 1
        self.update()

    def clear(self):
        for human in self._humans.values():
            self._scene.removeItem(human)
        self._humans = {}
        # self._scene.setSceneRect(QRectF(0,0,400,400))

    def resizeEvent(self, event):
        # skip initial entry
        self.own_resize()
        super(HumanVisualizationWidget, self).resizeEvent(event)

    def own_resize(self):
        self.fitInView(self._scene.itemsBoundingRect(), Qt.KeepAspectRatio)
        self._scene.setSceneRect(self._scene.itemsBoundingRect())

    def add_human_by_pos(self, id, pos):
        x, y = pos
        human = QGraphicsEllipseItem(0, 0, 200, 200)
        self._scene.addItem(human)
        human.setBrush(QBrush(Qt.black, style=Qt.SolidPattern))
        human_text = QGraphicsTextItem(str(pos))
        font = QFont("Helvetica [Cronyx]", 40, QFont.Bold)
        human_text.setFont(font)
        human_text.setParentItem(human)
        human.setPos(pos[0], pos[1])
        self._humans[id] = human
        human.setZValue(30)

    def move_human(self, id, pos):
        x, y = pos
        human = self._humans[id]
        human.setPos(x, y)

    def set_human_color(self, id, color):
        if id in self._humans:
            self._humans[id].setBrush(color)
Esempio n. 8
0
class QTraceViewer(QWidget):
    TAG_SPACING = 50
    LEGEND_X = -50
    LEGEND_Y = 0
    LEGEND_WIDTH = 10

    TRACE_FUNC_X = 0
    TRACE_FUNC_Y = 0
    TRACE_FUNC_WIDTH = 50
    TRACE_FUNC_MINHEIGHT = 1000

    MARK_X = LEGEND_X
    MARK_WIDTH = TRACE_FUNC_X - LEGEND_X + TRACE_FUNC_WIDTH
    MARK_HEIGHT = 1

    def __init__(self, workspace, disasm_view, parent=None):
        super().__init__(parent=parent)
        self.workspace = workspace
        self.disasm_view = disasm_view

        self.view = None
        self.scene = None
        self.mark = None
        self.curr_position = 0
        self._use_precise_position = False

        self._init_widgets()

        self.trace.am_subscribe(self._on_set_trace)
        self.selected_ins.am_subscribe(self._on_select_ins)
        self.view.installEventFilter(self)

    #
    # Forwarding properties
    #

    @property
    def trace(self):
        return self.workspace.instance.trace

    @property
    def selected_ins(self):
        return self.disasm_view.infodock.selected_insns

    def _init_widgets(self):
        self.view = QGraphicsView()
        self.scene = QGraphicsScene()
        self.view.setScene(self.scene)

        self._reset()

        layout = QHBoxLayout()
        layout.addWidget(self.view)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setAlignment(self.view, Qt.AlignLeft)

        self.setLayout(layout)

    def _reset(self):
        self.scene.clear()  #clear items
        self.mark = None

        self.legend = None
        self.legend_height = 0

        self.trace_func = QGraphicsItemGroup()
        self.scene.addItem(self.trace_func)
        self.hide()

    def _on_set_trace(self, **kwargs):
        self._reset()

        if self.trace.am_obj is not None:
            l.debug('minheight: %d, count: %d', self.TRACE_FUNC_MINHEIGHT,
                    self.trace.count)
            if self.trace.count <= 0:
                l.warning(
                    "No valid addresses found in trace to show. Check base address offsets?"
                )
                self.trace.am_obj = None
                self.trace.am_event()
                return
            if self.TRACE_FUNC_MINHEIGHT < self.trace.count * 15:
                self.trace_func_unit_height = 15
                show_func_tag = True
            else:
                self.trace_func_unit_height = self.TRACE_FUNC_MINHEIGHT / self.trace.count
                show_func_tag = True

            self.legend_height = int(self.trace.count *
                                     self.trace_func_unit_height)

            self._show_trace_func(show_func_tag)
            self._show_legend()
            self._set_mark_color()

            self.scene.setSceneRect(self.scene.itemsBoundingRect())  #resize
            self.setFixedWidth(self.scene.itemsBoundingRect().width())
            self.view.setFixedWidth(self.scene.itemsBoundingRect().width())

            self.show()

    def _on_select_ins(self, **kwargs):
        if self.trace == None:
            return

        if self.mark is not None:
            for i in self.mark.childItems():
                self.mark.removeFromGroup(i)
                self.scene.removeItem(i)
            self.scene.removeItem(self.mark)

        self.mark = QGraphicsItemGroup()
        self.scene.addItem(self.mark)

        if self.selected_ins:
            addr = next(iter(self.selected_ins))
            positions = self.trace.get_positions(addr)
            if positions:  #if addr is in list of positions
                if not self._use_precise_position:  #handle case where insn was selected from disas view
                    self.curr_position = positions[0] - self.trace.count
                for p in positions:
                    color = self._get_mark_color(p, self.trace.count)
                    y = self._get_mark_y(p, self.trace.count)

                    if p == self.trace.count + self.curr_position:  #add thicker line for 'current' mark
                        self.mark.addToGroup(
                            self.scene.addRect(self.MARK_X, y, self.MARK_WIDTH,
                                               self.MARK_HEIGHT * 4,
                                               QPen(QColor('black')),
                                               QBrush(color)))
                    else:
                        self.mark.addToGroup(
                            self.scene.addRect(self.MARK_X, y, self.MARK_WIDTH,
                                               self.MARK_HEIGHT, QPen(color),
                                               QBrush(color)))
                #y = self._get_mark_y(positions[0], self.trace.count)
                #self.view.verticalScrollBar().setValue(y - 0.5 * self.view.size().height())

                self.scene.update()  #force redraw of the scene
                self.scroll_to_position(self.curr_position)

    def scroll_to_position(self, position):
        relative_pos = self.trace.count + position
        y_offset = self._get_mark_y(relative_pos, self.trace.count)

        scrollValue = 0
        if y_offset > 0.5 * self.view.size().height():
            scrollValue = y_offset - 0.5 * self.view.size().height()
        scrollValue = min(scrollValue, self.view.verticalScrollBar().maximum())
        self.view.verticalScrollBar().setValue(scrollValue)
        self._use_precise_position = False

    def jump_next_insn(self):
        if self.curr_position + self.trace.count < self.trace.count - 1:  #for some reason indexing is done backwards
            self.curr_position += 1
            self._use_precise_position = True
            func_name = self.trace.trace_func[self.curr_position].func_name
            func = self._get_func_from_func_name(func_name)
            bbl_addr = self.trace.trace_func[self.curr_position].bbl_addr
            self._jump_bbl(func, bbl_addr)

    def jump_prev_insn(self):
        if self.curr_position + self.trace.count > 0:
            self.curr_position -= 1
            self._use_precise_position = True
            func_name = self.trace.trace_func[self.curr_position].func_name
            func = self._get_func_from_func_name(func_name)
            bbl_addr = self.trace.trace_func[self.curr_position].bbl_addr
            self._jump_bbl(func, bbl_addr)

    def eventFilter(self, object, event):  #specifically to catch arrow keys
        # more elegant solution to link w/ self.view's scroll bar keypressevent?
        if event.type() == QEvent.Type.KeyPress:
            if not (event.modifiers() & Qt.ShiftModifier):  #shift + arrowkeys
                return False
            key = event.key()
            if key == Qt.Key_Up or key == Qt.Key_Left:
                self.jump_prev_insn()
            elif key == Qt.Key_Down or key == Qt.Key_Right:
                self.jump_next_insn()
            return True

        return False  # pass through all other events

    def mousePressEvent(self, event):
        button = event.button()
        pos = self._to_logical_pos(event.pos())
        if button == Qt.LeftButton and self._at_legend(pos):
            func = self._get_func_from_y(pos.y())
            bbl_addr = self._get_bbl_from_y(pos.y())
            self._use_precise_position = True
            self.curr_position = self._get_position(pos.y())
            self._jump_bbl(func, bbl_addr)

    def _jump_bbl(self, func, bbl_addr):
        all_insn_addrs = self.workspace.instance.project.factory.block(
            bbl_addr).instruction_addrs
        # TODO: replace this with am_events perhaps?
        self.workspace.on_function_selected(func)
        self.selected_ins.clear()
        self.selected_ins.update(all_insn_addrs)
        self.selected_ins.am_event()
        # TODO: this ought to happen automatically as a result of the am_event
        self.disasm_view.current_graph.show_instruction(bbl_addr)

    def _get_mark_color(self, i, total):
        relative_gradient_pos = i * 1000 // total
        return self.legend_img.pixelColor(self.LEGEND_WIDTH // 2,
                                          relative_gradient_pos)

    def _get_mark_y(self, i, total):
        return self.TRACE_FUNC_Y + self.trace_func_unit_height * i

    def _show_trace_func(self, show_func_tag):
        x = self.TRACE_FUNC_X
        y = self.TRACE_FUNC_Y
        prev_name = None
        for position in self.trace.trace_func:
            bbl_addr = position.bbl_addr
            func_name = position.func_name
            l.debug('Draw function %x, %s', bbl_addr, func_name)
            color = self.trace.get_func_color(func_name)
            self.trace_func.addToGroup(
                self.scene.addRect(x, y, self.TRACE_FUNC_WIDTH,
                                   self.trace_func_unit_height, QPen(color),
                                   QBrush(color)))
            if show_func_tag is True and func_name != prev_name:
                tag = self.scene.addText(func_name,
                                         QFont("Source Code Pro", 7))
                tag.setPos(x + self.TRACE_FUNC_WIDTH + self.TAG_SPACING,
                           y - tag.boundingRect().height() // 2)
                self.trace_func.addToGroup(tag)
                anchor = self.scene.addLine(
                    self.TRACE_FUNC_X + self.TRACE_FUNC_WIDTH, y,
                    x + self.TRACE_FUNC_WIDTH + self.TAG_SPACING, y)
                self.trace_func.addToGroup(anchor)
                prev_name = func_name
            y += self.trace_func_unit_height

    def _make_legend_gradient(self, x1, y1, x2, y2):
        gradient = QLinearGradient(x1, y1, x2, y2)
        gradient.setColorAt(0.0, Qt.red)
        gradient.setColorAt(0.4, Qt.yellow)
        gradient.setColorAt(0.6, Qt.green)
        gradient.setColorAt(0.8, Qt.blue)
        gradient.setColorAt(1.0, Qt.darkBlue)
        return gradient

    def _show_legend(self):
        pen = QPen(Qt.transparent)

        gradient = self._make_legend_gradient(
            self.LEGEND_X, self.LEGEND_Y, self.LEGEND_X,
            self.LEGEND_Y + self.legend_height)
        brush = QBrush(gradient)
        self.legend = self.scene.addRect(self.LEGEND_X, self.LEGEND_Y,
                                         self.LEGEND_WIDTH, self.legend_height,
                                         pen, brush)

        reference_gradient = self._make_legend_gradient(
            0, 0, self.LEGEND_WIDTH, 1000)
        base_img = QImage(self.LEGEND_WIDTH, 1000, QImage.Format.Format_ARGB32)
        p = QPainter(base_img)
        p.fillRect(base_img.rect(), reference_gradient)
        self.legend_img = base_img  #reference shade

    def _set_mark_color(self):
        for p in range(self.trace.count):
            color = self._get_mark_color(p, self.trace.count)
            self.trace.set_mark_color(p, color)

    def _at_legend(self, pos):
        x = pos.x()
        y = pos.y()
        if self.TRACE_FUNC_X + self.LEGEND_X < x < self.view.width() and \
           self.TRACE_FUNC_Y < y < self.TRACE_FUNC_Y + self.legend_height:
            return True
        else:
            return False

    def _to_logical_pos(self, pos):
        x_offset = self.view.horizontalScrollBar().value()
        y_offset = self.view.verticalScrollBar().value()
        return QPoint(pos.x() + x_offset, pos.y() + y_offset)

    def _get_position(self, y):
        y_relative = y - self.legend_height

        return int(y_relative // self.trace_func_unit_height)

    def _get_bbl_from_y(self, y):
        position = self._get_position(y)
        return self.trace.get_bbl_from_position(position)

    def _get_func_from_func_name(self, func_name):
        return self.workspace.instance.kb.functions.function(name=func_name)

    def _get_func_from_y(self, y):
        position = self._get_position(y)
        func_name = self.trace.get_func_name_from_position(position)
        return self._get_func_from_func_name(func_name)
class AbstractGraphicViewer(QGraphicsView, TcWidget):
    def __init__(self, parent=None):
        super(AbstractGraphicViewer, self).__init__(parent)
        self.m_scaleX = 0
        self.m_scaleY = 0
        self._pan = False
        self._panStartX = 0
        self._panStartY = 0
        self.scene = QGraphicsScene()
        self.scene.setItemIndexMethod(QGraphicsScene.NoIndex)
        self.scene.setSceneRect(-2000, -2000, 4000, 4000)
        self.setScene(self.scene)
        self.setCacheMode(QGraphicsView.CacheBackground)
        self.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)
        self.setRenderHint(QPainter.Antialiasing)
        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        self.setMinimumSize(400, 400)
        self.adjustSize()
        self.setMouseTracking(True)
        self.viewport().setMouseTracking(True)

    def wheelEvent(self, event):
        if event.angleDelta().y() > 0:
            factor = 1.1
        else:
            factor = 0.9
        view_pos = event.pos()
        scene_pos = self.mapToScene(view_pos)
        self.centerOn(scene_pos)
        self.scale(factor, factor)
        delta = self.mapToScene(view_pos) - self.mapToScene(self.viewport().rect().center())
        self.centerOn(scene_pos - delta)

    def resizeEvent(self, event):
        super(AbstractGraphicViewer, self).resizeEvent(event)

    def mouseMoveEvent(self, event):
        if self._pan:
            self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - (event.x() - self._panStartX))
            self.verticalScrollBar().setValue(self.verticalScrollBar().value() - (event.y() - self._panStartY))
            self._panStartX = event.x()
            self._panStartY = event.y()
            event.accept()
        super(AbstractGraphicViewer, self).mouseMoveEvent(event)

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self._pan = True
            self._panStartX = event.x()
            self._panStartY = event.y()
            self.setCursor(QCursor.ClosedHandCursor)
            event.accept()
            return
        super(AbstractGraphicViewer, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            self._pan = False
            self.setCursor(QCursor.ArrowCursor)
            event.accept()
        super(AbstractGraphicViewer, self).mouseReleaseEvent(event)

    def showEvent(self, event):
        super(AbstractGraphicViewer, self).showEvent(event)
        adjusted = self.scene.itemsBoundingRect().adjusted(-100, -100, 100, 100)
        self.scene.setSceneRect(adjusted)
        # FitInView is called two times because of this bug: https://bugreports.qt.io/browse/QTBUG-1047
        update_state = self.updatesEnabled()
        self.setUpdatesEnabled(False)
        self.fitInView(adjusted, Qt.KeepAspectRatio)
        QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
        self.fitInView(adjusted, Qt.KeepAspectRatio)
        self.setUpdatesEnabled(update_state)
Esempio n. 10
0
class QTraceViewer(QWidget):
    """
    Load a basic block trace through json and visualize it in the disassembly
    Ref: https://github.com/angr/angr-management/pull/122
    """

    TAG_SPACING = 50
    LEGEND_X = -50
    LEGEND_Y = 0
    LEGEND_WIDTH = 10

    TRACE_FUNC_X = 0
    TRACE_FUNC_Y = 0
    TRACE_FUNC_WIDTH = 50
    TRACE_FUNC_MINHEIGHT = 1000

    TAB_HEADER_SIZE = 40
    MAX_WINDOW_SIZE = 500

    MARK_X = LEGEND_X
    MARK_WIDTH = TRACE_FUNC_X - LEGEND_X + TRACE_FUNC_WIDTH
    MARK_HEIGHT = 1

    def __init__(self, workspace, disasm_view, parent=None):
        super().__init__(parent=parent)
        self.workspace = workspace
        self.disasm_view = disasm_view

        self.mark = None
        self.legend = None
        self.legend_height = 0
        self.legend_img = None
        self.trace_func_unit_height = 0

        self.trace_func = None
        self.trace_id = None

        self.view = None
        self.traceView = None
        self.traceTab = None
        self.traceScene = None
        self.multiView = None
        self.listView = None
        self.mark = None
        self.curr_position = 0
        self._use_precise_position = False
        self._selected_traces = []

        self._init_widgets()

        self.trace.am_subscribe(self._on_set_trace)
        self.selected_ins.am_subscribe(self._on_select_ins)
        self.traceTab.installEventFilter(self)

    #
    # Forwarding properties
    #

    @property
    def trace(self):
        return self.workspace.instance.trace

    @property
    def multi_trace(self):
        return self.workspace.instance.multi_trace

    @property
    def selected_ins(self):
        return self.disasm_view.infodock.selected_insns

    def _init_widgets(self):
        self.view = QTabWidget()  # QGraphicsView()

        self.traceTab = QWidget()
        tracelayout = QVBoxLayout()

        self.traceView = QGraphicsView()
        self.traceScene = QGraphicsScene()
        self.traceView.setScene(self.traceScene)

        self.listView = QTableWidget(0, 2)  # row, col
        self.listView.setHorizontalHeaderItem(0, QTableWidgetItem("Trace ID"))
        self.listView.setHorizontalHeaderItem(1, QTableWidgetItem("Input ID"))
        self.listView.setSelectionMode(QAbstractItemView.SingleSelection)
        self.listView.setSelectionBehavior(QAbstractItemView.SelectRows)
        # self.listView.horizontalHeader().setStretchLastSection(True)
        # self.listView.horizontalHeader().setSectionResizeModel(0, QHeaderView.Stretch)
        self.listView.cellClicked.connect(self._switch_current_trace)

        self.traceSeedButton = QPushButton("View Input Seed")
        self.traceSeedButton.clicked.connect(self._view_input_seed)

        tracelayout.addWidget(self.traceView)
        tracelayout.addWidget(self.listView)
        tracelayout.addWidget(self.traceSeedButton)
        self.traceTab.setLayout(tracelayout)

        self.multiView = QWidget()
        multiLayout = QVBoxLayout()
        self.multiTraceList = QTableWidget(0, 2)  # row, col
        self.multiTraceList.setSelectionMode(QAbstractItemView.MultiSelection)
        self.multiTraceList.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.multiTraceList.setHorizontalScrollMode(
            self.multiTraceList.ScrollPerPixel)
        self.multiTraceList.setHorizontalHeaderItem(
            0, QTableWidgetItem("Trace ID"))
        self.multiTraceList.setHorizontalHeaderItem(
            1, QTableWidgetItem("Input ID"))
        self.selectMultiTrace = QPushButton("Refresh Heatmap")
        self.selectMultiTrace.clicked.connect(self._refresh_heatmap)
        multiLayout.addWidget(self.multiTraceList)
        multiLayout.addWidget(self.selectMultiTrace)
        self.multiView.setLayout(multiLayout)

        self.view.addTab(self.traceTab, "SingleTrace")
        self.view.addTab(self.multiView, "MultiTrace HeatMap")
        self.SINGLE_TRACE = 0
        self.MULTI_TRACE = 1

        self.view.currentChanged.connect(self._on_tab_change)

        self._reset()

        layout = QVBoxLayout()
        layout.addWidget(self.view)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setAlignment(self.view, Qt.AlignLeft)

        self.setLayout(layout)

    def _reset(self):
        self.traceScene.clear()  #clear items
        self.listView.clearContents()
        self.multiTraceList.clearContents()
        self.mark = None

        self.legend = None
        self.legend_height = 0

        self.trace_func = QGraphicsItemGroup()
        self.trace_id = QGraphicsItemGroup()
        self.traceScene.addItem(self.trace_func)
        self.hide()

    def _view_input_seed(self):
        current_trace_stats = self.trace.am_obj
        input_id = current_trace_stats.input_id

        inputSeed = self.multi_trace.am_obj.get_input_seed_for_id(input_id)
        msgText = "%s" % inputSeed
        msgDetails = "Input for [%s]" % current_trace_stats.id
        msgbox = QMessageBox()
        msgbox.setWindowTitle("Seed Input")
        msgbox.setDetailedText(msgDetails)
        msgbox.setText(msgText)
        msgbox.setStandardButtons(QMessageBox.Ok)
        msgbox.exec()

    def _switch_current_trace(self, row):
        if self.listView.rowCount() <= 0:
            return

        current_trace = self.trace.am_obj.id
        new_trace = self.multiTraceList.item(row, 0).text()
        if current_trace == new_trace:
            return

        trace_stats = self.multi_trace.am_obj.get_trace_with_id(new_trace)
        if trace_stats:
            self.trace.am_obj = trace_stats
            self._on_set_trace()

    def _on_set_trace(self):
        self._reset()
        if self.trace.am_none or self.trace.count is None:
            return

        l.debug('minheight: %d, count: %d', self.TRACE_FUNC_MINHEIGHT,
                self.trace.count)
        if self.trace.count <= 0:
            l.warning(
                "No valid addresses found in trace to show. Check base address offsets?"
            )
            self.trace.am_obj = None
            self.trace.am_event()
            return
        if self.TRACE_FUNC_MINHEIGHT < self.trace.count * 15:
            self.trace_func_unit_height = 15
            show_func_tag = True
        else:
            self.trace_func_unit_height = self.TRACE_FUNC_MINHEIGHT / self.trace.count
            show_func_tag = True

        self.legend_height = int(self.trace.count *
                                 self.trace_func_unit_height)

        self._show_trace_func(show_func_tag)
        self._show_legend()
        self._show_trace_ids()
        self._set_mark_color()
        self._refresh_multi_list()

        boundingSize = self.traceScene.itemsBoundingRect().width()
        windowSize = boundingSize
        if boundingSize > self.MAX_WINDOW_SIZE:
            windowSize = self.MAX_WINDOW_SIZE
        self.traceScene.setSceneRect(
            self.traceScene.itemsBoundingRect())  #resize
        self.setFixedWidth(windowSize)

        # self.listScene.setSceneRect(self.listScene.itemsBoundingRect()) #resize
        self.multiView.setFixedWidth(windowSize)
        cellWidth = windowSize // 2
        self.listView.setColumnWidth(0, cellWidth)
        self.listView.setColumnWidth(1, cellWidth)
        self.listView.setFixedHeight(self.multiView.height() // 4)
        self.multiTraceList.setColumnWidth(0, cellWidth)
        self.multiTraceList.setColumnWidth(1, cellWidth)
        self.view.setFixedWidth(windowSize)

        self.show()

    def _populate_trace_table(self, view, trace_ids):
        numIDs = len(trace_ids)
        view.clearContents()
        view.setRowCount(numIDs)
        row = 0  #start after label row
        for traceID in trace_ids:
            inputID = self.multi_trace.am_obj.get_input_id_for_trace_id(
                traceID)
            if inputID is None:
                self.workspace.log("No inputID found for trace %s" % traceID)
            view.setItem(row, 0, QTableWidgetItem(traceID))
            view.setItem(row, 1, QTableWidgetItem(inputID))
            row += 1

    def _refresh_heatmap(self):
        multiTrace = self.multi_trace.am_obj
        multiTrace.clear_heatmap()
        multiTrace.is_active_tab = True

        selected_items = self.multiTraceList.selectedItems()
        self._selected_traces.clear()
        for row in range(self.multiTraceList.rowCount()):
            item = self.multiTraceList.item(row, 0)
            if item in selected_items:
                self._selected_traces.append(item.text())
        multiTrace.reload_heatmap(self._selected_traces)
        self.multi_trace.am_event()

    def _refresh_multi_list(self):
        multiTrace = self.multi_trace.am_obj
        trace_ids = multiTrace.get_all_trace_ids()

        self.multiTraceList.clearContents()
        self._populate_trace_table(self.multiTraceList, trace_ids)
        if self._selected_traces and self.multiTraceList.rowCount() > 0:
            self.multiTraceList.item(0, 0).setSelected(True)
            self.multiTraceList.item(0, 1).setSelected(True)
        else:
            for row in range(self.multiTraceList.rowCount()):
                item = self.multiTraceList.item(row, 0)
                inputItem = self.multiTraceList.item(row, 1)
                if item.text() in self._selected_traces:
                    item.setSelected(True)
                    inputItem.setSelected(True)
        self.multi_trace.am_event()

    def _on_tab_change(self):
        # self._reset()
        multiTrace = self.multi_trace.am_obj
        if self.view.currentIndex() == self.MULTI_TRACE:
            multiTrace.is_active_tab = True
            self._refresh_multi_list()
        elif self.view.currentIndex() == self.SINGLE_TRACE:
            multiTrace = self.multi_trace.am_obj
            multiTrace.is_active_tab = False
            self._show_trace_ids()

    def _on_select_ins(self, **kwargs):  # pylint: disable=unused-argument
        if self.trace.am_none:
            return

        if self.mark is not None:
            for i in self.mark.childItems():
                self.mark.removeFromGroup(i)
                self.traceScene.removeItem(i)
            self.traceScene.removeItem(self.mark)

        self.mark = QGraphicsItemGroup()
        self.traceScene.addItem(self.mark)

        if self.selected_ins:
            addr = next(iter(self.selected_ins))
            positions = self.trace.get_positions(addr)
            if positions:  #if addr is in list of positions
                if not self._use_precise_position:  #handle case where insn was selected from disas view
                    self.curr_position = positions[0] - self.trace.count
                for p in positions:
                    color = self._get_mark_color(p, self.trace.count)
                    y = self._get_mark_y(p)

                    if p == self.trace.count + self.curr_position:  #add thicker line for 'current' mark
                        self.mark.addToGroup(
                            self.traceScene.addRect(self.MARK_X, y,
                                                    self.MARK_WIDTH,
                                                    self.MARK_HEIGHT * 4,
                                                    QPen(QColor('black')),
                                                    QBrush(color)))
                    else:
                        self.mark.addToGroup(
                            self.traceScene.addRect(self.MARK_X, y,
                                                    self.MARK_WIDTH,
                                                    self.MARK_HEIGHT,
                                                    QPen(color),
                                                    QBrush(color)))

                self.traceScene.update()  #force redraw of the traceScene
                self.scroll_to_position(self.curr_position)

    def scroll_to_position(self, position):
        relative_pos = self.trace.count + position
        y_offset = self._get_mark_y(relative_pos)

        scrollValue = 0
        if y_offset > 0.5 * self.traceView.size().height():
            scrollValue = y_offset - 0.5 * self.traceView.size().height()
        scrollValue = min(scrollValue,
                          self.traceView.verticalScrollBar().maximum())
        self.traceView.verticalScrollBar().setValue(scrollValue)
        self._use_precise_position = False

    def jump_next_insn(self):
        if self.curr_position + self.trace.count < self.trace.count - 1:  #for some reason indexing is done backwards
            self.curr_position += 1
            self._use_precise_position = True
            bbl_addr = self.trace.get_bbl_from_position(self.curr_position)
            func = self.trace.get_func_from_position(self.curr_position)
            self._jump_bbl(func, bbl_addr)

    def jump_prev_insn(self):
        if self.curr_position + self.trace.count > 0:
            self.curr_position -= 1
            self._use_precise_position = True
            bbl_addr = self.trace.get_bbl_from_position(self.curr_position)
            func = self.trace.get_func_from_position(self.curr_position)
            self._jump_bbl(func, bbl_addr)

    def eventFilter(self, obj, event):  #specifically to catch arrow keys #pylint: disable=unused-argument
        # more elegant solution to link w/ self.view's scroll bar keypressevent?
        if event.type() == QEvent.Type.KeyPress:
            if not event.modifiers() & Qt.ShiftModifier:  #shift + arrowkeys
                return False
            key = event.key()
            if key in [Qt.Key_Up, Qt.Key_Left]:
                self.jump_prev_insn()
            elif key in [Qt.Key_Down, Qt.Key_Right]:
                self.jump_next_insn()
            return True

        return False  # pass through all other events

    def mousePressEvent(self, event):
        button = event.button()
        pos = self._to_logical_pos(event.pos())
        if button == Qt.LeftButton and self.view.currentIndex(
        ) == self.SINGLE_TRACE and self._at_legend(pos):
            func = self._get_func_from_y(pos.y())
            bbl_addr = self._get_bbl_from_y(pos.y())
            self._use_precise_position = True
            self.curr_position = self._get_position(pos.y())
            self._jump_bbl(func, bbl_addr)

    def _jump_bbl(self, func, bbl_addr):
        all_insn_addrs = self.workspace.instance.project.factory.block(
            bbl_addr).instruction_addrs
        # TODO: replace this with am_events perhaps?
        if func is None:
            return
        self.workspace.on_function_selected(func)
        self.selected_ins.clear()
        self.selected_ins.update(all_insn_addrs)
        self.selected_ins.am_event()
        # TODO: this ought to happen automatically as a result of the am_event
        self.disasm_view.current_graph.show_instruction(bbl_addr)

    def _get_mark_color(self, i, total):
        relative_gradient_pos = i * 1000 // total
        return self.legend_img.pixelColor(self.LEGEND_WIDTH // 2,
                                          relative_gradient_pos)

    def _get_mark_y(self, i):
        return self.TRACE_FUNC_Y + self.trace_func_unit_height * i

    def _show_trace_ids(self):
        trace_ids = self.multi_trace.get_all_trace_ids()
        # traceID = self.listScene.addText(id_txt, QFont("Source Code Pro", 7))
        # traceID.setPos(5,5)
        self.listView.clearContents()
        self._populate_trace_table(self.listView, trace_ids)
        if len(self.listView.selectedItems()) <= 0 and not self.trace.am_none:
            for row in range(self.listView.rowCount()):
                item = self.listView.item(row, 0)
                inputItem = self.listView.item(row, 1)
                if self.trace.id in item.text():
                    item.setSelected(True)
                    inputItem.setSelected(True)
                    break

    def _show_trace_func(self, show_func_tag):
        x = self.TRACE_FUNC_X
        y = self.TRACE_FUNC_Y
        prev_name = None
        for position in self.trace.trace_func:
            bbl_addr = position.bbl_addr
            func_name = position.func_name
            l.debug('Draw function %x, %s', bbl_addr, func_name)
            color = self.trace.get_func_color(func_name)
            self.trace_func.addToGroup(
                self.traceScene.addRect(x, y, self.TRACE_FUNC_WIDTH,
                                        self.trace_func_unit_height,
                                        QPen(color), QBrush(color)))
            if show_func_tag is True and func_name != prev_name:
                tag = self.traceScene.addText(func_name,
                                              QFont("Source Code Pro", 7))
                tag.setPos(x + self.TRACE_FUNC_WIDTH + self.TAG_SPACING,
                           y - tag.boundingRect().height() // 2)
                self.trace_func.addToGroup(tag)
                anchor = self.traceScene.addLine(
                    self.TRACE_FUNC_X + self.TRACE_FUNC_WIDTH, y,
                    x + self.TRACE_FUNC_WIDTH + self.TAG_SPACING, y)
                self.trace_func.addToGroup(anchor)
                prev_name = func_name
            y += self.trace_func_unit_height

    @staticmethod
    def _make_legend_gradient(x1, y1, x2, y2):
        gradient = QLinearGradient(x1, y1, x2, y2)
        gradient.setColorAt(0.0, Qt.red)
        gradient.setColorAt(0.4, Qt.yellow)
        gradient.setColorAt(0.6, Qt.green)
        gradient.setColorAt(0.8, Qt.blue)
        gradient.setColorAt(1.0, Qt.darkBlue)
        return gradient

    def _show_legend(self):
        pen = QPen(Qt.transparent)

        gradient = self._make_legend_gradient(
            self.LEGEND_X, self.LEGEND_Y, self.LEGEND_X,
            self.LEGEND_Y + self.legend_height)
        brush = QBrush(gradient)
        self.legend = self.traceScene.addRect(self.LEGEND_X, self.LEGEND_Y,
                                              self.LEGEND_WIDTH,
                                              self.legend_height, pen, brush)

        reference_gradient = self._make_legend_gradient(
            0, 0, self.LEGEND_WIDTH, 1000)
        base_img = QImage(self.LEGEND_WIDTH, 1000, QImage.Format.Format_ARGB32)
        p = QPainter(base_img)
        p.fillRect(base_img.rect(), reference_gradient)
        self.legend_img = base_img  #reference shade

    def _set_mark_color(self):
        for p in range(self.trace.count):
            color = self._get_mark_color(p, self.trace.count)
            self.trace.set_mark_color(p, color)

    def _at_legend(self, pos):
        x = pos.x()
        y = pos.y()
        return self.TRACE_FUNC_X + self.LEGEND_X < x < self.traceView.width() and \
           self.TRACE_FUNC_Y < y < self.TRACE_FUNC_Y + self.legend_height

    def _to_logical_pos(self, pos):
        x_offset = self.traceView.horizontalScrollBar().value()
        y_offset = self.traceView.verticalScrollBar().value()
        return QPoint(pos.x() + x_offset, pos.y() + y_offset)

    def _get_position(self, y):
        y_relative = y - self.legend_height - self.TAB_HEADER_SIZE

        return int(y_relative // self.trace_func_unit_height)

    def _get_bbl_from_y(self, y):
        position = self._get_position(y)
        return self.trace.get_bbl_from_position(position)

    def _get_func_from_y(self, y):
        position = self._get_position(y)
        func = self.trace.get_func_from_position(position)
        return func
Esempio n. 11
0
class QFeatureMapView(QGraphicsView):
    """
    Main view for feature map scene.
    """
    def __init__(self, disasm_view, parent=None):
        super().__init__(parent)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setMouseTracking(True)
        self._scene = QGraphicsScene(parent=self)
        self.setScene(self._scene)
        self.fm: FeatureMapItem = FeatureMapItem(disasm_view)
        self._scale: float = 1.0
        self._scene.addItem(self.fm)

        self.setBackgroundBrush(Conf.palette_base)
        self.setResizeAnchor(QGraphicsView.NoAnchor)
        self.setTransformationAnchor(QGraphicsView.NoAnchor)
        self.setAlignment(Qt.AlignTop | Qt.AlignLeft)
        self.update_size()

        self._base_width: int = 0

    def mouseMoveEvent(self, event):
        """
        Handle mouse move events.

        Mouse move events whilst not holding a mouse button will not be propagated down to QGraphicsItems, so we catch
        the movement events here in the view and forward them to the feature map item.
        """
        scene_pt = self.mapToScene(event.pos().x(), event.pos().y())
        item_pt = self.fm.mapFromScene(scene_pt)
        self.fm.on_mouse_move_event_from_view(item_pt)
        super().mouseMoveEvent(event)

    def wheelEvent(self, event):
        """
        Handle wheel events to scale and translate the feature map.
        """
        if event.modifiers() & Qt.ControlModifier == Qt.ControlModifier:
            self.adjust_viewport_scale(
                1.25 if event.delta() > 0 else 1 / 1.25,
                QPoint(event.pos().x(),
                       event.pos().y()))
        else:
            self.translate(100 * (-1 if event.delta() < 0 else 1), 0)
            super().wheelEvent(event)

    def resizeEvent(self, event):  # pylint: disable=unused-argument
        """
        Handle view resize events, updating the feature map size accordingly.
        """
        self.update_size()

    def adjust_viewport_scale(self,
                              scale: Optional[float] = None,
                              point: Optional[QPoint] = None):
        """
        Adjust viewport scale factor.
        """
        if point is None:
            point = QPoint(0, 0)
        point_rel = self.mapToScene(point).x() / self.fm.width

        if scale is None:
            self._scale = 1.0
        else:
            self._scale *= scale
            if self._scale < 1.0:
                self._scale = 1.0

        self.update_size()
        self.translate(
            self.mapToScene(point).x() - point_rel * self.fm.width, 0)

    def keyPressEvent(self, event):
        """
        Handle key events.
        """
        if event.modifiers() & Qt.ControlModifier == Qt.ControlModifier:
            if event.key() == Qt.Key_0:
                self.adjust_viewport_scale()
                event.accept()
                return
            elif event.key() == Qt.Key_Equal:
                self.adjust_viewport_scale(1.25)
                event.accept()
                return
            elif event.key() == Qt.Key_Minus:
                self.adjust_viewport_scale(1 / 1.25)
                event.accept()
                return
        super().keyPressEvent(event)

    def changeEvent(self, event: QEvent):
        """
        Redraw on color scheme update.
        """
        if event.type() == QEvent.StyleChange:
            self.setBackgroundBrush(Conf.palette_base)
            self.fm.refresh()

    def update_size(self):
        """
        Resize feature map.
        """
        vg = self.viewport().geometry()
        if self._scale <= 1.0:
            # Only resize to feature map to viewport width if scale is at base level to not disturb preferred size
            self._base_width = vg.width()
        self.fm.set_width(self._base_width * self._scale)
        self.fm.set_height(vg.height())
        self.fm.refresh()
        self.setSceneRect(self._scene.itemsBoundingRect())