Пример #1
0
def _make_wire_item(x1, y1, x2, y2):
    path = QPainterPath()

    path.moveTo(x1, y1)
    path.lineTo(x2, y2)

    path_item = QGraphicsPathItem(path)

    stroker = QPainterPathStroker(QPen(Qt.black, 5, c=Qt.RoundCap))
    stroke_path = stroker.createStroke(path)
    stroke_item = QGraphicsPathItem(stroke_path)

    path_item.setPen(QPen(Qt.white, 5, c=Qt.RoundCap))
    stroke_item.setPen(QPen(Qt.black, 1.5, c=Qt.RoundCap))

    group = QGraphicsItemGroup()
    group.addToGroup(path_item)
    group.addToGroup(stroke_item)

    group.setFlag(QGraphicsItem.ItemIsSelectable)

    return group
Пример #2
0
class SectionScene(QGraphicsScene):
    def __init__(self, section, model, commodity_types, process_cores):
        super().__init__()
        self._grid_size = QSize(GRID_WIDTH, GRID_HEIGHT)
        self._drop_indicator = self.init_drop_indicator()
        self._process_items = QGraphicsItemGroup()
        self._commodity_items = List([])
        self._bounding_rect = BoundingRect(section, model.process_list,
                                           commodity_types)
        self._clicked_item = None
        self._item_mouse_offset = None
        self._connect_line = None
        self._items_border = QRect()
        self._grid_border = QRect()
        self._section = section
        self._model = model
        self._cores = process_cores

        self._edit_mode = SelectConnect.SELECT
        self._draft_mode = False

        self.init_scene()

    @property
    def edit_mode(self):
        return self._edit_mode

    @edit_mode.setter
    def edit_mode(self, value):
        if value in SelectConnect:
            self._edit_mode = value
            if value == SelectConnect.SELECT:
                self._process_items.setFlag(QGraphicsItem.ItemIsMovable)
            else:
                self._process_items.setFlag(QGraphicsItem.ItemIsMovable, False)
        else:
            raise TypeError

    @property
    def draft_mode(self):
        return self._draft_mode

    @draft_mode.setter
    def draft_mode(self, value):
        if value in [True, False]:
            self._draft_mode = value
            self._process_items.setFlag(QGraphicsItem.ItemIsMovable, value)
        else:
            raise TypeError

    def init_scene(self):
        """initialize content of scene based on list of elements"""
        for process in self._model.process_list:
            if process.core.section == self._section:
                process_item = self.draw_process(process)
                self.draw_connection(process_item)
                self.draw_connection(process_item, False)
        self.update_commodities()

    def draw_commodity(self, commodity):
        """create commodity item"""
        commodity_item = CommodityItem(commodity.locations[self._section],
                                       self._bounding_rect)
        commodity_item.setData(0, commodity)
        commodity_item.setData(1, 0)
        self.addItem(commodity_item)

        return commodity_item

    def draw_connection(self, process_item, input_com=True):
        commodity_types = []
        process = process_item.data(0)
        connections = process_item.data(1)

        # define necessary connections to commodity item
        commodities = process.inputs if input_com else process.outputs
        for commodity in commodities:
            if commodity.commodity_type not in commodity_types:
                commodity_types.append(commodity.commodity_type)

        commodity_difference = PROCESS_SIZE / (len(commodity_types) + 1)
        for commodity_type in commodity_types:
            commodity_position = commodity_type.locations[self._section]
            item_position = process.coordinate.x() + PROCESS_SIZE / 2
            # set item_position to left/right border of process depending on commodity_position
            if commodity_position < item_position:
                item_position -= PROCESS_SIZE

            start_position = commodity_position if input_com else item_position
            end_position = item_position if input_com else commodity_position

            commodity_item = [
                item for item in self._commodity_items
                if item.data(0) is commodity_type
            ]
            connection_item = [
                connection for connection in connections
                if connection.data(1) is commodity_item
            ]

            if commodity_item:
                commodity_item = commodity_item[0]
            else:
                # create new commodity item
                commodity_item = self.draw_commodity(commodity_type)
                self._commodity_items.add(commodity_item)

            if connection_item:
                connection_item = connection_item[0]
            else:
                # ignore second double arrow for storage processes if start_position is left of end_position
                if (process.core.category is ProcessCategory.STORAGE) & (
                        end_position - start_position < 0):
                    return

                # create new connection item
                connection_item = ConnectionItem(
                    end_position - start_position,
                    process.core.category is ProcessCategory.STORAGE)
                commodity_item.setData(1, commodity_item.data(1) + 1)
                connection_item.setData(1, commodity_item)
                connection_list = process_item.data(1)
                connection_list.append(connection_item)
                process_item.setData(1, connection_list)

            connection_item.setX(start_position)
            index = commodity_types.index(commodity_type) + 1
            connection_item.setY(process.coordinate.y() - PROCESS_SIZE / 2 +
                                 commodity_difference * index)
            self.addItem(connection_item)

    def draw_process(self, process):
        """create process item and add to scene & process items list"""
        process_item = ProcessItem(process.core.icon)
        process_item.setData(0, process)
        process_item.setData(1, [])
        process_item.setPos(process.coordinate)
        process_item.setFlag(QGraphicsItem.ItemIsMovable)

        self._process_items.prepareGeometryChange()
        self._process_items.addToGroup(process_item)
        self.addItem(process_item)

        return process_item

    def delete_process(self, item):
        process = item.data(0)
        self._model.process_list.remove(process)

        # remove all connection items and the commodity item if applicable
        for connection_item in item.data(1):
            commodity_item = connection_item.data(1)
            commodity_references = commodity_item.data(1) - 1
            commodity_item.setData(1, commodity_references)
            if commodity_references == 0:
                # remove commodity of section count and commodity item
                commodity_item.data(0).locations.remove(self._section)
                self._commodity_items.remove(commodity_item)
                self.removeItem(commodity_item)
            self.removeItem(connection_item)

        # reduce connection count of all commodities connected to process
        for input_com in process.inputs:
            input_com.connection_count[self._section] -= 1
            if input_com.connection_count[self._section] == 0:
                input_com.connection_count.remove(self._section)
                if not input_com.connection_count:
                    self._model.commodity_list.remove(input_com)

        for output in process.outputs:
            output.connection_count[self._section] -= 1
            if output.connection_count[self._section] == 0:
                output.connection_count.remove(self._section)
                if not output.connection_count:
                    self._model.commodity_list.remove(output)

        self._process_items.removeFromGroup(item)
        self.removeItem(item)

        self.update_commodities()

    def init_drop_indicator(self):
        rect = QRect(-DROP_INDICATOR_SIZE / 2, -DROP_INDICATOR_SIZE / 2,
                     DROP_INDICATOR_SIZE, DROP_INDICATOR_SIZE)
        brush = QBrush(Qt.darkBlue)
        pen = QPen(Qt.darkBlue, 1)
        ellipse = self.addEllipse(rect, pen, brush)
        ellipse.setVisible(False)
        return ellipse

    def disable_drop_indicator(self):
        self._drop_indicator.setX(-DROP_INDICATOR_SIZE / 2)
        self._drop_indicator.setY(-DROP_INDICATOR_SIZE / 2)
        self._drop_indicator.setVisible(False)
        self.update()

    def align_drop_indicator(self, point):
        """align the drop indicator to grid while mouse movement"""
        # move drop indicator only within grid boundaries
        if not self.get_grid_border(-MIN_GRID_MARGIN).contains(
                point.toPoint()):
            self._drop_indicator.setVisible(False)
            return

        # calculate the nearby position of grid interceptions
        grid_x = (round(point.x() / self._grid_size.width() / 2 - 1 / 2) * 2 +
                  1) * self._grid_size.width()
        grid_y = round(
            point.y() / self._grid_size.height()) * self._grid_size.height()

        # move drop indicator to grid interception
        self._drop_indicator.setX(grid_x)
        self._drop_indicator.setY(grid_y)
        self._drop_indicator.setVisible(True)
        self.update()

    def get_grid_border(self, margin=0):
        def get_raster_length(length, raster):
            return (int((length + MIN_GRID_MARGIN + raster / 2) / raster) -
                    1 / 2) * raster

        left = get_raster_length(self.sceneRect().left(),
                                 self._grid_size.width())
        right = get_raster_length(self.sceneRect().right(),
                                  self._grid_size.width())
        top = get_raster_length(self.sceneRect().top(),
                                self._grid_size.height())
        bottom = get_raster_length(self.sceneRect().bottom(),
                                   self._grid_size.height())

        return QRect(left, top, right - left, bottom - top).marginsAdded(
            QMargins(margin, margin, margin, margin))

    def execute_connection(self, item, position):
        if not self._connect_line:
            if self.show_connection_menu(item, position):
                return
        else:
            selected_commodity = self._connect_line.data(1)
            if isinstance(item, ProcessItem):
                # create incoming connection
                if selected_commodity in item.data(0).core.inputs:
                    if self._connect_line.data(0) is None:
                        # establish connection from commodity
                        if selected_commodity not in item.data(0).inputs:
                            self.connect_process(item.data(0),
                                                 selected_commodity, False)
                            self.draw_connection(item)
                    else:
                        # establish connection between processes
                        self.connect_processes(selected_commodity,
                                               self._connect_line.data(0),
                                               item)
            elif item is None:
                # create outgoing connection
                menu = QMenu()
                menu.addAction("> Commodity")

                if menu.exec_(QCursor.pos()):
                    self.connect_process(
                        self._connect_line.data(0).data(0), selected_commodity)
                    self.draw_connection(self._connect_line.data(0), False)

        # end connection
        self.views()[1].setMouseTracking(False)
        self.removeItem(self._connect_line)
        self._connect_line = None

    def show_connection_menu(self, item, position):
        menu = QMenu()
        # add all output commodities of process to menu
        for commodity in item.data(0).core.outputs:
            action = menu.addAction(str(commodity))
            action.setData(commodity)

        # execute menu
        action = menu.exec_(QCursor.pos())
        if action:
            # start connection
            line_pen = QPen(Qt.lightGray, 1, Qt.DashLine)
            line = QLineF(position, position)
            self._connect_line = self.addLine(line, line_pen)
            self._connect_line.setData(0, item)
            self._connect_line.setData(1, action.data())
            self.views()[1].setMouseTracking(True)
            return True

        return False

    def connect_processes(self, selected_commodity, start_process_item,
                          end_process_item):
        # establish connection
        connect_commodity = None
        commodity_in_input = False
        commodity_in_output = False
        start_process = start_process_item.data(0)
        end_process = end_process_item.data(0)

        # check if selected_commodity is in outputs or inputs of related processes
        outputs = start_process.outputs
        if selected_commodity in outputs:
            commodity_in_output = True
            connect_commodity = outputs[outputs.index(selected_commodity)]

        inputs = end_process.inputs
        if selected_commodity in inputs:
            commodity_in_input = True
            connect_commodity = inputs[inputs.index(selected_commodity)]

        # determine necessary connection steps
        if commodity_in_input & commodity_in_output:
            # selected_commodity already exists in input and output -> connection exists
            QMessageBox.warning(self.views()[1], "Connection exists",
                                "Connection already established",
                                QMessageBox.Ok)
            return
        elif commodity_in_input:
            # add commodity to output of start process
            self.connect_process(start_process, connect_commodity)
            self.draw_connection(start_process_item, False)
        elif commodity_in_output:
            # add commodity to input of end process
            self.connect_process(end_process, connect_commodity, False)
            self.draw_connection(end_process_item)
        else:
            # establish new commodity in both processes
            connect_commodity = selected_commodity.copy()

            self.connect_process(start_process, connect_commodity)
            self.connect_process(end_process, connect_commodity, False)
            self.draw_connection(start_process_item, False)
            self.draw_connection(end_process_item)

    def connect_process(self, process, commodity, output=True):
        if self._section not in commodity.connection_count.keys():
            commodity.connection_count[self._section] = 0
        if commodity not in self._model.commodity_list:
            self._model.commodity_list.add(commodity)
        commodity.connection_count[self._section] += 1

        commodity_list = process.outputs if output else process.inputs
        commodity_list.add(commodity)
        # add commodity in input and output list
        if process.core.category is ProcessCategory.STORAGE:
            commodity_list = process.outputs if not output else process.inputs
            commodity_list.add(commodity)

        self.set_commodity_position(commodity.commodity_type, process, output)

    def set_commodity_position(self, commodity_type, process, right=True):
        """changes the commodity position according to newly connected process"""
        if self._section not in commodity_type.locations.keys():
            # set initial position to right of process
            location = process.coordinate.x() + self._grid_size.width() * (
                1 if right else -1)
            commodity_type.locations[self._section] = location
        else:
            # todo change position based on newly connected process
            pass

    def update_commodities(self):
        """update length of commodity line based on bounding rect"""
        self._bounding_rect.update()
        top_border = self._bounding_rect.top()
        height_border = self._bounding_rect.height()
        for commodity_item in self._commodity_items:
            commodity_item.update_line(top_border, height_border)

    def drawBackground(self, painter, rect):
        if self.draft_mode:
            border_rect = self.get_grid_border()
            grid_rect = self.get_grid_border(-self._grid_size.width() / 2)

            line_list = []
            # horizontal grid lines
            for line_coordinate in range(grid_rect.top(), border_rect.bottom(),
                                         self._grid_size.height()):
                line_list.append(
                    QLineF(border_rect.left(), line_coordinate,
                           border_rect.right(), line_coordinate))
            # vertical process lines
            left_border = int((grid_rect.left() + self._grid_size.width()) / (2 * self._grid_size.width())) * \
                self._grid_size.width() * 2 - self._grid_size.width()
            for line_coordinate in range(left_border, border_rect.right(),
                                         self._grid_size.width() * 2):
                line_list.append(
                    QLineF(line_coordinate, border_rect.top(), line_coordinate,
                           border_rect.bottom()))

            # vertical commodity lines
            left_border = int(
                grid_rect.left() /
                (2 * self._grid_size.width())) * self._grid_size.width() * 2
            for line_coordinate in range(left_border, border_rect.right(),
                                         self._grid_size.width() * 2):
                line_list.append(
                    QLineF(line_coordinate - 1, border_rect.top(),
                           line_coordinate - 1, border_rect.bottom()))
                line_list.append(
                    QLineF(line_coordinate + 1, border_rect.top(),
                           line_coordinate + 1, border_rect.bottom()))

            painter.setPen(QPen(Qt.lightGray, 1))
            painter.drawLines(line_list)

        super().drawBackground(painter, rect)

    def mousePressEvent(self, event):
        # ignore right button click
        if event.button() == Qt.RightButton:
            return

        self._clicked_item = self.itemAt(event.scenePos(), QTransform())
        if self.draft_mode & (self.edit_mode == SelectConnect.SELECT):
            if isinstance(self._clicked_item, ProcessItem):
                self._clicked_item.setOpacity(0.5)
                self._drop_indicator.setPos(self._clicked_item.pos())
                self._drop_indicator.setVisible(True)
                # set offset between mouse and center of item for mouseMoveEvent
                self._item_mouse_offset = self._clicked_item.mapFromScene(
                    event.scenePos())
            else:
                self._clicked_item = None

    def mouseMoveEvent(self, event):
        if self.draft_mode:
            if self.edit_mode == SelectConnect.SELECT:
                if self._clicked_item:
                    # move drop indicator along grid and process item according to mouse position
                    self.align_drop_indicator(event.scenePos() -
                                              self._item_mouse_offset)
                    self._clicked_item.setPos(event.scenePos() -
                                              self._item_mouse_offset)
            elif self._connect_line:
                # update connect line in CONNECT mode
                line = self._connect_line.line()
                # add offset to position to avoid itemAt function to return lineItem
                line.setP2(event.scenePos() - QPoint(2, 2))
                self._connect_line.setLine(line)

        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        # normal mode
        if not self.draft_mode:
            if event.button() == Qt.LeftButton:
                # toggle sidebar with properties
                clicked_item = self.itemAt(event.scenePos(), QTransform())
                if self._clicked_item:
                    if isinstance(clicked_item, ProcessItem):
                        self.views()[0].sidebar_toggled.emit(
                            clicked_item.data(0))
                        self.views()[0].commodity_clicked.emit(None)
                    elif isinstance(clicked_item, CommodityItem):
                        self.views()[0].sidebar_toggled.emit(None)
                        self.views()[0].commodity_clicked.emit(
                            clicked_item.data(0))
                    else:
                        self.views()[0].sidebar_toggled.emit(None)
                        self.views()[0].commodity_clicked.emit(None)
                else:
                    self.views()[0].sidebar_toggled.emit(None)
                    self.views()[0].commodity_clicked.emit(None)
            return

        # draft mode in selection
        if self.edit_mode == SelectConnect.SELECT:
            # release process item
            if self._clicked_item:
                self._clicked_item.setOpacity(1.0)
                # remove clicked item from collision list with drop indicator
                collision_items = self.collidingItems(self._drop_indicator)
                if collision_items:
                    collision_items.remove(self._clicked_item)
                # avoid placement if colliding with other items
                if not collision_items:
                    self._clicked_item.setPos(self._drop_indicator.scenePos())
                    self._clicked_item.data(
                        0).coordinate = self._drop_indicator.pos()
                    self._bounding_rect.update()
                self.disable_drop_indicator()
                self._clicked_item = None
        else:
            # connecting processes
            if event.button() == Qt.LeftButton:
                clicked_item = self.itemAt(event.scenePos(), QTransform())
                self.execute_connection(clicked_item, event.scenePos())

        super().mouseReleaseEvent(event)

    def dragEnterEvent(self, event):
        pass

    def dragLeaveEvent(self, event):
        self.disable_drop_indicator()

    def dragMoveEvent(self, event):
        if self.draft_mode & (self.edit_mode == SelectConnect.SELECT):
            self.align_drop_indicator(event.scenePos())

    def dropEvent(self, event):
        """add new process to list and process item to scene"""
        # prevent process placement if item exists there
        if len(self.collidingItems(self._drop_indicator)) > 0:
            return

        core_name = event.mimeData().data(
            MimeType.PROCESS_CORE.value).data().decode('UTF-8')
        process_core = list(
            filter(lambda element: element.name == core_name, self._cores))[0]

        # define name based on process core and existing names
        remainder_name = [
            process.name.split(process_core.name)[1].strip()
            for process in self._model.process_list
            if process_core.name in process.name
        ]
        if not remainder_name:
            process_name = process_core.name
        elif max(remainder_name).isdigit():
            process_name = process_core.name + " " + str(
                int(max(remainder_name)) + 1)
        else:
            process_name = process_core.name + " 1"

        # create new process based on process core and add to element list
        process = Process(process_name, self._drop_indicator.pos(),
                          process_core)
        self._model.add_process(process)

        # create process item and connections
        self.draw_process(process)
        self.update_commodities()

        self.disable_drop_indicator()

    def contextMenuEvent(self, event):
        """context menu to interact with process items - delete item"""
        # prevent context menu of scene not in draft mode
        if not self.draft_mode:
            return

        if self._edit_mode == SelectConnect.SELECT:
            # open context menu only for process items
            self._clicked_item = self.itemAt(event.scenePos(), QTransform())
            if self._clicked_item:
                if isinstance(self._clicked_item, ProcessItem):
                    menu = QMenu()
                    delete_action = QAction("Delete", None)
                    delete_action.triggered.connect(
                        lambda: self.delete_process(self._clicked_item))
                    menu.addAction(delete_action)
                    menu.exec_(event.screenPos())
                    self._clicked_item = None
        else:
            # open context menu for commodities of other sections
            menu = QMenu()
            section_list = [
                item for item in OverviewSelection if item is not self._section
                and item is not OverviewSelection.OVERVIEW
            ]
            for section in section_list:
                submenu = QMenu(section.name)
                section_coms = [
                    commodity for commodity in self._model.commodity_list
                    if section in commodity.connection_count.keys()
                ]
                for commodity in section_coms:
                    action = submenu.addAction(str(commodity))
                    action.setData(commodity)

                # only add sub menu if commodities are available
                if section_coms:
                    menu.addMenu(submenu)

            # execute menu
            action = menu.exec_(QCursor.pos())
            if action:
                # start connection
                line_pen = QPen(Qt.lightGray, 1, Qt.DashLine)
                line = QLineF(event.scenePos(), event.scenePos())
                self._connect_line = self.addLine(line, line_pen)
                self._connect_line.setData(0, None)
                self._connect_line.setData(1, action.data())
                self.views()[1].setMouseTracking(True)

    def setSceneRect(self, rect):
        """set sceneRect to view boundaries if necessary space is less"""
        super().setSceneRect(
            self._bounding_rect.scene_rect(rect, 2 * MIN_GRID_MARGIN))