Exemple #1
0
    def select_area(self, area: Optional[Area]):
        self.area = area
        if area is None:
            return

        if "total_boundings" in area.extra:
            min_x, max_x, min_y, max_y = [
                area.extra["total_boundings"][k]
                for k in ["x1", "x2", "y1", "y2"]
            ]
        else:
            min_x, min_y = math.inf, math.inf
            max_x, max_y = -math.inf, -math.inf
            for node in area.nodes:
                if node.location is None:
                    continue
                min_x = min(min_x, node.location.x)
                min_y = min(min_y, node.location.y)
                max_x = max(max_x, node.location.x)
                max_y = max(max_y, node.location.y)

        image_path = self.game.data_path.joinpath(
            "assets", "maps",
            f"{area.map_name}.png") if self.game is not None else None
        if image_path is not None and image_path.exists():
            self._background_image = QtGui.QImage(os.fspath(image_path))
            min_x = area.extra.get("map_min_x", 0)
            min_y = area.extra.get("map_min_y", 0)
            max_x = self._background_image.width() - area.extra.get(
                "map_max_x", 0)
            max_y = self._background_image.height() - area.extra.get(
                "map_max_y", 0)
            self.image_bounds = BoundsInt(min_x, min_y, max_x, max_y)
            self.world_bounds = BoundsFloat(min_x, min_y, max_x, max_y)
        else:
            self.update_world_bounds()

        self.area_bounds = BoundsFloat(
            min_x=min_x,
            min_y=min_y,
            max_x=max_x,
            max_y=max_y,
        )
        self.area_size = QSizeF(
            max(max_x - min_x, 1),
            max(max_y - min_y, 1),
        )
        self.update()
    def ScalePicture(self):
        rect = QRectF(self.graphicsItem.pos(), QSizeF(
            self.pixMap.size()))
        unity = self.graphicsView.transform().mapRect(QRectF(0, 0, 1, 1))
        width = unity.width()
        height = unity.height()
        if width <= 0 or height <= 0:
            return
        self.graphicsView.scale(1 / width, 1 / height)
        viewRect = self.graphicsView.viewport().rect()
        sceneRect = self.graphicsView.transform().mapRect(rect)
        if sceneRect.width() <= 0 or sceneRect.height() <= 0:
            return
        x_ratio = viewRect.width() / sceneRect.width()
        y_ratio = viewRect.height() / sceneRect.height()
        x_ratio = y_ratio = min(x_ratio, y_ratio)

        self.graphicsView.scale(x_ratio, y_ratio)
        # if self.readImg.isStripModel:
        #     height2 = self.pixMap.size().height() / 2
        #     height3 = self.graphicsView.size().height()/2
        #     height3 = height3/x_ratio
        #     p = self.graphicsItem.pos()
        #     self.graphicsItem.setPos(p.x(), p.y()+height2-height3)
        self.graphicsView.centerOn(rect.center())

        for _ in range(abs(self.scaleCnt)):
            if self.scaleCnt > 0:
                self.graphicsView.scale(1.1, 1.1)
            else:
                self.graphicsView.scale(1/1.1, 1/1.1)
Exemple #3
0
 def CreateSelectionRect(self) -> QRectF:
     cursorScene = self.mapToScene(self._currentCursorPosition.toPoint())
     if self._state is State.SELECTING:
         selectionRect = QRectF(0, 0, abs(self._boxSelectionRectAnchor.x() - cursorScene.x()),
                                abs(self._boxSelectionRectAnchor.y() - cursorScene.y()))
         selectionRect.moveCenter((cursorScene + self._boxSelectionRectAnchor) / 2.0)
     else:
         selectionRect = QRectF(cursorScene, QSizeF())
     return selectionRect
Exemple #4
0
    def _update_scale_variables(self):
        self.border_x = self.rect().width() * 0.05
        self.border_y = self.rect().height() * 0.05
        canvas_width = max(self.rect().width() - self.border_x * 2, 1)
        canvas_height = max(self.rect().height() - self.border_y * 2, 1)

        self.scale = min(canvas_width / self.area_size.width(),
                         canvas_height / self.area_size.height())

        self.canvas_size = QSizeF(canvas_width, canvas_width)
Exemple #5
0
    def __init__(self):
        super().__init__()

        scene = QGraphicsScene()
        self.setScene(scene)

        self.setRenderHint(QPainter.Antialiasing)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setMouseTracking(True)

        self._offset = QPointF(0, 0)
        self._zoom = 0

        self.backgroundColor = QColor(40, 40, 40)

        self.gridSpacing = QSizeF(60, 60)
        self.gridThickness = 1
        self.gridColor = QColor(100, 100, 100)
        self.gridZoomThreshold = -1.5
        self.showGrid = True

        self.selectionBoxStrokeColor = QColor(52, 222, 235)
        self.selectionBoxFillColor = QColor(52, 222, 235, 50)
        self.selectionBoxThickness = 2
        self._editing = True

        self._hoveredItems: List[ChipItem] = []
        self._selectedItems: List[ChipItem] = []
        self._sceneItems: Set[ChipItem] = set()

        self._boxSelectionRectAnchor = QPointF()
        self._currentCursorPosition = QPointF()

        self.selectionBox = self.scene().addRect(QRectF(),
                                                 QPen(self.selectionBoxStrokeColor, self.selectionBoxThickness),
                                                 QBrush(self.selectionBoxFillColor))
        self.selectionBox.setVisible(False)

        self._state = State.IDLE

        self.UpdateView()
Exemple #6
0
class ChipSceneViewer(QGraphicsView):
    selectionChanged = Signal(list)

    def __init__(self):
        super().__init__()

        scene = QGraphicsScene()
        self.setScene(scene)

        self.setRenderHint(QPainter.Antialiasing)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setMouseTracking(True)

        self._offset = QPointF(0, 0)
        self._zoom = 0

        self.backgroundColor = QColor(40, 40, 40)

        self.gridSpacing = QSizeF(60, 60)
        self.gridThickness = 1
        self.gridColor = QColor(100, 100, 100)
        self.gridZoomThreshold = -1.5
        self.showGrid = True

        self.selectionBoxStrokeColor = QColor(52, 222, 235)
        self.selectionBoxFillColor = QColor(52, 222, 235, 50)
        self.selectionBoxThickness = 2
        self._editing = True

        self._hoveredItems: List[ChipItem] = []
        self._selectedItems: List[ChipItem] = []
        self._sceneItems: Set[ChipItem] = set()

        self._boxSelectionRectAnchor = QPointF()
        self._currentCursorPosition = QPointF()

        self.selectionBox = self.scene().addRect(QRectF(),
                                                 QPen(self.selectionBoxStrokeColor, self.selectionBoxThickness),
                                                 QBrush(self.selectionBoxFillColor))
        self.selectionBox.setVisible(False)

        self._state = State.IDLE

        self.UpdateView()

    def SetEditing(self, editing: bool):
        self._editing = editing

        self.showGrid = editing

        for item in self._sceneItems:
            item.SetEditDisplay(editing)

        if not editing:
            self.DeselectAll()

    def GetSelectedItems(self):
        return self._selectedItems

    def AddItem(self, item: ChipItem):
        if item not in self._sceneItems:
            self._sceneItems.add(item)
            item.Move(QPointF())
            item.onRemoved.connect(self.RemoveItem)
            self.scene().addItem(item.GraphicsObject())
        item.SetEditDisplay(self._editing)
        return item

    def RemoveItem(self, item: ChipItem):
        self.DeselectItem(item)
        if item in self._hoveredItems:
            self._hoveredItems.remove(item)

        if item in self._sceneItems:
            self._sceneItems.remove(item)
            self.scene().removeItem(item.GraphicsObject())

    def GetItems(self):
        return self._sceneItems

    def RemoveAll(self):
        self._hoveredItems.clear()
        self._selectedItems.clear()
        for item in self._sceneItems.copy():
            self.RemoveItem(item)

    def SelectItem(self, item: ChipItem):
        if item not in self._selectedItems:
            item.SetSelected(True)
            self._selectedItems.append(item)

    def ToggleSelectItem(self, item: ChipItem):
        if item in self._selectedItems:
            self.DeselectItem(item)
        else:
            self.SelectItem(item)

    def DeselectItem(self, item: ChipItem):
        if item in self._selectedItems:
            item.SetSelected(False)
            self._selectedItems.remove(item)

    def DeselectAll(self):
        for item in self._selectedItems.copy():
            self.DeselectItem(item)

    def Recenter(self):
        itemsRect = QRectF()
        for item in self._sceneItems:
            itemsRect = itemsRect.united(item.GraphicsObject().boundingRect().translated(item.GraphicsObject().pos()))
        self._offset = itemsRect.center()

        self.UpdateView()

    def CenterItem(self, item: ChipItem):
        QApplication.processEvents()
        sceneCenter = self.mapToScene(self.rect().center())
        currentCenter = item.GraphicsObject().sceneBoundingRect().center()
        delta = sceneCenter - currentCenter
        item.Move(delta)

    def UpdateView(self):
        matrix = QTransform()
        matrix.scale(2 ** self._zoom, 2 ** self._zoom)
        self.setTransform(matrix)
        self.setSceneRect(QRectF(self._offset.x(), self._offset.y(), 10, 10))

        self.UpdateSelectionBox()
        self.UpdateHoveredItems()

    @staticmethod
    def GetSelectionMode():
        if QApplication.keyboardModifiers() == Qt.ShiftModifier:
            return SelectionMode.MODIFY
        return SelectionMode.NORMAL

    def CreateSelectionRect(self) -> QRectF:
        cursorScene = self.mapToScene(self._currentCursorPosition.toPoint())
        if self._state is State.SELECTING:
            selectionRect = QRectF(0, 0, abs(self._boxSelectionRectAnchor.x() - cursorScene.x()),
                                   abs(self._boxSelectionRectAnchor.y() - cursorScene.y()))
            selectionRect.moveCenter((cursorScene + self._boxSelectionRectAnchor) / 2.0)
        else:
            selectionRect = QRectF(cursorScene, QSizeF())
        return selectionRect

    def UpdateHoveredItems(self):
        if self._state is State.PANNING:
            return

        if not self._editing or self._state is State.MOVING:
            hoveredChipItems = []
        else:
            # What are we hovering over in the scene
            hoveredGraphicsItems = [item for item in self.scene().items(self.selectionBox.sceneBoundingRect())]

            hoveredChipItems = [item for item in self._sceneItems if item.GraphicsObject() in hoveredGraphicsItems]
            # Make sure we maintain the found order
            hoveredChipItems.sort(key=lambda x: hoveredGraphicsItems.index(x.GraphicsObject()))

            if self._state is not State.SELECTING:
                hoveredChipItems = hoveredChipItems[:1]

        for item in hoveredChipItems:
            if item not in self._hoveredItems and item.CanSelect():
                item.SetHovered(True)
        for item in self._hoveredItems:
            if item not in hoveredChipItems:
                item.SetHovered(False)
        self._hoveredItems = hoveredChipItems

    def UpdateSelectionBox(self):
        if self._state == State.SELECTING:
            self.selectionBox.setVisible(True)
            self.selectionBox.prepareGeometryChange()
        else:
            self.selectionBox.setVisible(False)
        self.selectionBox.setRect(self.CreateSelectionRect())

    def drawBackground(self, painter: QPainter, rect: QRectF):
        currentColor = self.backgroundBrush().color()
        if currentColor != self.backgroundColor:
            self.setBackgroundBrush(QBrush(self.backgroundColor))

        super().drawBackground(painter, rect)

        if self._zoom <= self.gridZoomThreshold or not self.showGrid:
            return

        painter.setPen(QPen(self.gridColor, self.gridThickness))

        lines = []
        if self.gridSpacing.width() > 0:
            xStart = rect.left() - rect.left() % self.gridSpacing.width()
            while xStart <= rect.right():
                line = QLineF(xStart, rect.bottom(), xStart, rect.top())
                lines.append(line)
                xStart = xStart + self.gridSpacing.width()

        if self.gridSpacing.height() > 0:
            yStart = rect.top() - rect.top() % self.gridSpacing.height()
            while yStart <= rect.bottom():
                line = QLineF(rect.left(), yStart, rect.right(), yStart)
                lines.append(line)
                yStart = yStart + self.gridSpacing.height()

        painter.drawLines(lines)

    def wheelEvent(self, event: QWheelEvent):
        numSteps = float(event.angleDelta().y()) / 1000

        oldWorldPos = self.mapToScene(self._currentCursorPosition.toPoint())
        self._zoom = min(self._zoom + numSteps, 0)
        self.UpdateView()
        newWorldPos = self.mapToScene(self._currentCursorPosition.toPoint())

        delta = newWorldPos - oldWorldPos
        self._offset -= delta

        self.UpdateView()

    def mousePressEvent(self, event):
        if self._state != State.IDLE:
            return

        if event.button() == Qt.RightButton:
            self._state = State.PANNING
        elif event.button() == Qt.LeftButton and self._editing:
            if len(self._hoveredItems) == 0:
                self._state = State.SELECTING
                self._boxSelectionRectAnchor = self.mapToScene(self._currentCursorPosition.toPoint())
            else:
                hoveredItem = self._hoveredItems[0]
                if self.GetSelectionMode() is SelectionMode.MODIFY:
                    self.ToggleSelectItem(hoveredItem)
                    self.selectionChanged.emit(self._selectedItems)
                else:
                    if hoveredItem not in self._selectedItems:
                        self.DeselectAll()
                        self.SelectItem(hoveredItem)
                        self.selectionChanged.emit(self._selectedItems)
                    if hoveredItem.CanMove(self.mapToScene(self._currentCursorPosition.toPoint())):
                        self._state = State.MOVING

        self.UpdateView()

        super().mousePressEvent(event)

    def mouseMoveEvent(self, event: QMouseEvent):
        # Update position movement
        newCursorPosition = event.localPos()
        if self._currentCursorPosition is None:
            deltaScene = QPointF()
        else:
            deltaScene = (self.mapToScene(newCursorPosition.toPoint()) -
                          self.mapToScene(self._currentCursorPosition.toPoint()))
        self._currentCursorPosition = newCursorPosition

        if self._state is State.PANNING:
            self._offset -= deltaScene
            self.UpdateView()
        elif self._state is State.MOVING:
            for selectedItem in self._selectedItems:
                selectedItem.Move(deltaScene)

        self.UpdateView()

        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event: QMouseEvent):
        super().mouseReleaseEvent(event)
        if self._state == State.PANNING:
            if event.button() == Qt.RightButton:
                self._state = State.IDLE
        elif self._state == State.SELECTING:
            if event.button() == Qt.LeftButton:
                self._state = State.IDLE
                if self.GetSelectionMode() is SelectionMode.MODIFY:
                    for item in self._hoveredItems:
                        self.ToggleSelectItem(item)
                else:
                    self.DeselectAll()
                    for item in self._hoveredItems:
                        self.SelectItem(item)
                self.selectionChanged.emit(self._selectedItems)
        elif self._state == State.MOVING:
            if event.button() == Qt.LeftButton:
                self._state = State.IDLE

        self.UpdateView()

        super().mouseReleaseEvent(event)

    def keyReleaseEvent(self, event: QKeyEvent):
        if event.key() == Qt.Key.Key_Delete:
            self.DeleteSelected()

        if event.key() == Qt.Key.Key_D and event.modifiers() == Qt.Modifier.CTRL:
            self.DuplicateSelected()

        super().keyReleaseEvent(event)

    def DeleteSelected(self):
        for item in self.GetSelectedItems().copy():
            if item.CanDelete():
                item.RequestDelete()

    def DuplicateSelected(self):
        newItems = []
        for item in self.GetSelectedItems():
            if item.CanDuplicate():
                newItem = item.Duplicate()
                newItem.Move(QPointF(50, 50))
                self.AddItem(newItem)
                newItems.append(newItem)
        if newItems:
            self.DeselectAll()
            [self.SelectItem(item) for item in newItems]
            self.selectionChanged.emit(self.GetSelectedItems())
Exemple #7
0
class DataEditorCanvas(QtWidgets.QWidget):
    game: Optional[RandovaniaGame] = None
    world: Optional[World] = None
    area: Optional[Area] = None
    highlighted_node: Optional[Node] = None
    _background_image: Optional[QtGui.QImage] = None
    world_bounds: BoundsFloat
    area_bounds: BoundsFloat
    area_size: QSizeF
    image_bounds: BoundsInt
    edit_mode: bool = True

    scale: float
    border_x: float = 75
    border_y: float = 75
    canvas_size: QSizeF

    _next_node_location: NodeLocation = NodeLocation(0, 0, 0)
    CreateNodeRequest = Signal(NodeLocation)
    MoveNodeRequest = Signal(Node, NodeLocation)
    SelectNodeRequest = Signal(Node)
    SelectAreaRequest = Signal(Area)
    SelectConnectionsRequest = Signal(Node)
    ReplaceConnectionsRequest = Signal(Node, Requirement)
    CreateDockRequest = Signal(NodeLocation, Area)
    MoveNodeToAreaRequest = Signal(Node, Area)

    state: Optional[State] = None
    visible_nodes: Optional[set[Node]] = None

    def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
        super().__init__(parent)

        self._show_all_connections_action = QtGui.QAction(
            "Show all node connections", self)
        self._show_all_connections_action.setCheckable(True)
        self._show_all_connections_action.setChecked(False)
        self._show_all_connections_action.triggered.connect(self.update)

        self._create_node_action = QtGui.QAction("Create node here", self)
        self._create_node_action.triggered.connect(self._on_create_node)

        self._move_node_action = QtGui.QAction("Move selected node here", self)
        self._move_node_action.triggered.connect(self._on_move_node)

    def _on_create_node(self):
        self.CreateNodeRequest.emit(self._next_node_location)

    def _on_move_node(self):
        self.MoveNodeRequest.emit(self.highlighted_node,
                                  self._next_node_location)

    def set_edit_mode(self, value: bool):
        self.edit_mode = value

    def select_game(self, game: RandovaniaGame):
        self.game = game

    def select_world(self, world: World):
        self.world = world
        image_path = self.game.data_path.joinpath(
            "assets", "maps",
            f"{world.name}.png") if self.game is not None else None
        if image_path is not None and image_path.exists():
            self._background_image = QtGui.QImage(os.fspath(image_path))
            self.image_bounds = BoundsInt(
                min_x=world.extra.get("map_min_x", 0),
                min_y=world.extra.get("map_min_y", 0),
                max_x=self._background_image.width() -
                world.extra.get("map_max_x", 0),
                max_y=self._background_image.height() -
                world.extra.get("map_max_y", 0),
            )
        else:
            self._background_image = None

        self.update_world_bounds()
        self.update()

    def update_world_bounds(self):
        if self.world is None:
            return

        min_x, min_y = math.inf, math.inf
        max_x, max_y = -math.inf, -math.inf

        for area in self.world.areas:
            total_boundings = area.extra.get("total_boundings")
            if total_boundings is None:
                continue
            min_x = min(min_x, total_boundings["x1"], total_boundings["x2"])
            max_x = max(max_x, total_boundings["x1"], total_boundings["x2"])
            min_y = min(min_y, total_boundings["y1"], total_boundings["y2"])
            max_y = max(max_y, total_boundings["y1"], total_boundings["y2"])

        self.world_bounds = BoundsFloat(
            min_x=min_x,
            min_y=min_y,
            max_x=max_x,
            max_y=max_y,
        )

    def get_image_point(self, x: float, y: float):
        bounds = self.image_bounds
        return QPointF(bounds.min_x + (bounds.max_x - bounds.min_x) * x,
                       bounds.min_y + (bounds.max_y - bounds.min_y) * y)

    def select_area(self, area: Optional[Area]):
        self.area = area
        if area is None:
            return

        if "total_boundings" in area.extra:
            min_x, max_x, min_y, max_y = [
                area.extra["total_boundings"][k]
                for k in ["x1", "x2", "y1", "y2"]
            ]
        else:
            min_x, min_y = math.inf, math.inf
            max_x, max_y = -math.inf, -math.inf
            for node in area.nodes:
                if node.location is None:
                    continue
                min_x = min(min_x, node.location.x)
                min_y = min(min_y, node.location.y)
                max_x = max(max_x, node.location.x)
                max_y = max(max_y, node.location.y)

        image_path = self.game.data_path.joinpath(
            "assets", "maps",
            f"{area.map_name}.png") if self.game is not None else None
        if image_path is not None and image_path.exists():
            self._background_image = QtGui.QImage(os.fspath(image_path))
            min_x = area.extra.get("map_min_x", 0)
            min_y = area.extra.get("map_min_y", 0)
            max_x = self._background_image.width() - area.extra.get(
                "map_max_x", 0)
            max_y = self._background_image.height() - area.extra.get(
                "map_max_y", 0)
            self.image_bounds = BoundsInt(min_x, min_y, max_x, max_y)
            self.world_bounds = BoundsFloat(min_x, min_y, max_x, max_y)
        else:
            self.update_world_bounds()

        self.area_bounds = BoundsFloat(
            min_x=min_x,
            min_y=min_y,
            max_x=max_x,
            max_y=max_y,
        )
        self.area_size = QSizeF(
            max(max_x - min_x, 1),
            max(max_y - min_y, 1),
        )
        self.update()

    def highlight_node(self, node: Node):
        self.highlighted_node = node
        self.update()

    def set_state(self, state: Optional[State]):
        self.state = state
        self.highlighted_node = state.node
        self.update()

    def set_visible_nodes(self, visible_nodes: Optional[set[Node]]):
        self.visible_nodes = visible_nodes
        self.update()

    def is_node_visible(self, node: Node) -> bool:
        return self.visible_nodes is None or node in self.visible_nodes

    def is_connection_visible(self, requirement: Requirement) -> bool:
        return self.state is None or requirement.satisfied(
            self.state.resources, self.state.energy,
            self.state.resource_database)

    def _update_scale_variables(self):
        self.border_x = self.rect().width() * 0.05
        self.border_y = self.rect().height() * 0.05
        canvas_width = max(self.rect().width() - self.border_x * 2, 1)
        canvas_height = max(self.rect().height() - self.border_y * 2, 1)

        self.scale = min(canvas_width / self.area_size.width(),
                         canvas_height / self.area_size.height())

        self.canvas_size = QSizeF(canvas_width, canvas_width)

    def _nodes_at_position(self, qt_local_position: QPointF):
        return [
            node for node in self.area.nodes if node.location is not None and
            (self.game_loc_to_qt_local(node.location) -
             qt_local_position).manhattanLength() < 10
        ]

    def _other_areas_at_position(self, qt_local_position: QPointF):
        result = []

        for area in self.world.areas:
            if "total_boundings" not in area.extra or area == self.area:
                continue

            bounds = BoundsFloat.from_bounds(area.extra["total_boundings"])
            tl = self.game_loc_to_qt_local([bounds.min_x, bounds.min_y])
            br = self.game_loc_to_qt_local([bounds.max_x, bounds.max_y])
            rect = QRectF(tl, br)
            if rect.contains(qt_local_position):
                result.append(area)

        return result

    def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None:
        local_pos = QPointF(self.mapFromGlobal(event.globalPos()))
        local_pos -= self.get_area_canvas_offset()

        nodes_at_mouse = self._nodes_at_position(local_pos)
        if nodes_at_mouse:
            if len(nodes_at_mouse) == 1:
                self.SelectNodeRequest.emit(nodes_at_mouse[0])
            return

        areas_at_mouse = self._other_areas_at_position(local_pos)
        if areas_at_mouse:
            if len(areas_at_mouse) == 1:
                self.SelectAreaRequest.emit(areas_at_mouse[0])
            return

    def contextMenuEvent(self, event: QtGui.QContextMenuEvent) -> None:
        local_pos = QPointF(self.mapFromGlobal(event.globalPos()))
        local_pos -= self.get_area_canvas_offset()
        self._next_node_location = self.qt_local_to_game_loc(local_pos)

        menu = QtWidgets.QMenu(self)
        if self.state is None:
            menu.addAction(self._show_all_connections_action)
        if self.edit_mode:
            menu.addAction(self._create_node_action)
            menu.addAction(self._move_node_action)
            self._move_node_action.setEnabled(
                self.highlighted_node is not None)
            if self.highlighted_node is not None:
                self._move_node_action.setText(
                    f"Move {self.highlighted_node.name} here")

        # Areas Menu
        menu.addSeparator()
        areas_at_mouse = self._other_areas_at_position(local_pos)

        for area in areas_at_mouse:
            sub_menu = QtWidgets.QMenu(f"Area: {area.name}", self)
            sub_menu.addAction("View area").triggered.connect(
                functools.partial(self.SelectAreaRequest.emit, area))
            if self.edit_mode:
                sub_menu.addAction(
                    "Create dock here to this area").triggered.connect(
                        functools.partial(self.CreateDockRequest.emit,
                                          self._next_node_location, area))
            menu.addMenu(sub_menu)

        if not areas_at_mouse:
            sub_menu = QtGui.QAction("No areas here", self)
            sub_menu.setEnabled(False)
            menu.addAction(sub_menu)

        # Nodes Menu
        menu.addSeparator()
        nodes_at_mouse = self._nodes_at_position(local_pos)
        if self.highlighted_node in nodes_at_mouse:
            nodes_at_mouse.remove(self.highlighted_node)

        for node in nodes_at_mouse:
            if len(nodes_at_mouse) == 1:
                menu.addAction(node.name).setEnabled(False)
                sub_menu = menu
            else:
                sub_menu = QtWidgets.QMenu(node.name, self)

            sub_menu.addAction("Highlight this").triggered.connect(
                functools.partial(self.SelectNodeRequest.emit, node))
            view_connections = sub_menu.addAction("View connections to this")
            view_connections.setEnabled(
                (self.edit_mode and self.highlighted_node != node) or
                (node in self.area.connections.get(self.highlighted_node, {})))
            view_connections.triggered.connect(
                functools.partial(self.SelectConnectionsRequest.emit, node))

            if self.edit_mode:
                sub_menu.addSeparator()
                sub_menu.addAction(
                    "Replace connection with Trivial").triggered.connect(
                        functools.partial(
                            self.ReplaceConnectionsRequest.emit,
                            node,
                            Requirement.trivial(),
                        ))
                sub_menu.addAction("Remove connection").triggered.connect(
                    functools.partial(
                        self.ReplaceConnectionsRequest.emit,
                        node,
                        Requirement.impossible(),
                    ))
                if areas_at_mouse:
                    move_menu = QtWidgets.QMenu("Move to...", self)
                    for area in areas_at_mouse:
                        move_menu.addAction(area.name).triggered.connect(
                            functools.partial(
                                self.MoveNodeToAreaRequest.emit,
                                node,
                                area,
                            ))
                    sub_menu.addMenu(move_menu)

            if sub_menu != menu:
                menu.addMenu(sub_menu)

        if not nodes_at_mouse:
            sub_menu = QtGui.QAction("No other nodes here", self)
            sub_menu.setEnabled(False)
            menu.addAction(sub_menu)

        # Done

        menu.exec_(event.globalPos())

    def game_loc_to_qt_local(self, pos: Union[NodeLocation,
                                              list[float]]) -> QPointF:
        if isinstance(pos, NodeLocation):
            x = pos.x
            y = pos.y
        else:
            x, y = pos[0], pos[1]

        return QPointF(self.scale * (x - self.area_bounds.min_x),
                       self.scale * (self.area_bounds.max_y - y))

    def qt_local_to_game_loc(self, pos: QPointF) -> NodeLocation:
        return NodeLocation((pos.x() / self.scale) + self.area_bounds.min_x,
                            self.area_bounds.max_y - (pos.y() / self.scale), 0)

    def get_area_canvas_offset(self):
        return QPointF(
            (self.width() - self.area_size.width() * self.scale) / 2,
            (self.height() - self.area_size.height() * self.scale) / 2,
        )

    def paintEvent(self, event: QtGui.QPaintEvent) -> None:
        if self.world is None or self.area is None:
            return

        self._update_scale_variables()

        painter = QtGui.QPainter(self)
        painter.setPen(QtGui.Qt.white)
        painter.setFont(QtGui.QFont("Arial", 10))

        # Center what we're drawing
        painter.translate(self.get_area_canvas_offset())

        if self._background_image is not None:
            scaled_border_x = 8 * self.border_x / self.scale
            scaled_border_y = 8 * self.border_y / self.scale

            wbounds = self.world_bounds
            abounds = self.area_bounds

            # Calculate the top-left corner and bottom-right of the background image
            percent_x_start = (abounds.min_x - wbounds.min_x - scaled_border_x
                               ) / (wbounds.max_x - wbounds.min_x)
            percent_x_end = (abounds.max_x - wbounds.min_x +
                             scaled_border_x) / (wbounds.max_x - wbounds.min_x)
            percent_y_start = 1 - (abounds.max_y - wbounds.min_y +
                                   scaled_border_y) / (wbounds.max_y -
                                                       wbounds.min_y)
            percent_y_end = 1 - (abounds.min_y - wbounds.min_y -
                                 scaled_border_y) / (wbounds.max_y -
                                                     wbounds.min_y)

            painter.drawImage(
                QRectF(-scaled_border_x * self.scale,
                       -scaled_border_y * self.scale,
                       (scaled_border_x * 2 + self.area_size.width()) *
                       self.scale,
                       (scaled_border_y * 2 + self.area_size.height()) *
                       self.scale), self._background_image,
                QRectF(self.get_image_point(percent_x_start, percent_y_start),
                       self.get_image_point(percent_x_end, percent_y_end)))

        area = self.area
        if "polygon" in area.extra:
            points = [
                self.game_loc_to_qt_local(p) for p in area.extra["polygon"]
            ]
            painter.drawPolygon(points, QtGui.Qt.FillRule.OddEvenFill)

        def draw_connections_from(source_node: Node):
            if source_node.location is None:
                return

            for target_node, requirement in area.connections[
                    source_node].items():
                if target_node.location is None:
                    continue

                if not self.is_connection_visible(requirement):
                    painter.setPen(QtGui.Qt.darkGray)
                elif source_node == self.highlighted_node or self.state is not None:
                    painter.setPen(QtGui.Qt.white)
                else:
                    painter.setPen(QtGui.Qt.gray)

                source = self.game_loc_to_qt_local(source_node.location)
                target = self.game_loc_to_qt_local(target_node.location)
                line = QtCore.QLineF(source, target)
                line_len = line.length()

                if line_len == 0:
                    continue

                end_point = line.pointAt(1 - 7 / line_len)
                line.setPoints(end_point, line.pointAt(5 / line_len))
                painter.drawLine(line)

                line_angle = line.angle()
                line.setAngle(line_angle + 30)
                tri_point_1 = line.pointAt(15 / line_len)
                line.setAngle(line_angle - 30)
                tri_point_2 = line.pointAt(15 / line_len)

                arrow = QtGui.QPolygonF([end_point, tri_point_1, tri_point_2])
                painter.drawPolygon(arrow)

        brush = painter.brush()
        brush.setStyle(QtGui.Qt.BrushStyle.SolidPattern)
        painter.setBrush(brush)

        if self._show_all_connections_action.isChecked(
        ) or self.visible_nodes is not None:
            for node in area.nodes:
                if node != self.highlighted_node:
                    draw_connections_from(node)

        if self.highlighted_node is not None and self.highlighted_node in area.nodes:
            draw_connections_from(self.highlighted_node)

        painter.setPen(QtGui.Qt.white)

        for node in area.nodes:
            if node.location is None:
                continue

            if self.is_node_visible(node):
                brush.setColor(_color_for_node.get(type(node),
                                                   QtGui.Qt.yellow))
            else:
                brush.setColor(QtGui.Qt.darkGray)
            painter.setBrush(brush)

            p = self.game_loc_to_qt_local(node.location)
            if self.highlighted_node == node:
                painter.drawEllipse(p, 7, 7)
            painter.drawEllipse(p, 5, 5)
            centered_text(painter, p + QPointF(0, 15), node.name)