class ToggleAutoLink(Command):
    keyword: str class ToggleProgress(Command):
    pass class FilterBy(Command):
    """Activate or deactivate filtering"""

    pattern: str class EdgeType(IntEnum):
    BLOCKER = 1
    PARENT = 2 class HoldSelect(Command):
    """Saves current selection into the "previous selection" state""" class ToggleClose(Command):
    """Close an open selected goal. Re-open a closed selected goal"""

    root: int = 0 class Delete(Command):
    """Remove given or selected goal whether it exists. Do nothing in other case"""

    goal_id: int = 0 class ToggleLink(Command):
    """Create or remove a link between two given goals, if possible"""

    lower: int = 0
    upper: int = 0
    edge_type: EdgeType = EdgeType.BLOCKER class Add(Command):
    """Add a new goal to the existing tree"""

    name: str
    add_to: int = 0
    edge_type: EdgeType = EdgeType.PARENT class Select(Command):
    """Select a goal by its id whether it exist. Do nothing in other case"""

    goal_id: int class Insert(Command):
    """Add an intermediate goal between two selected goals"""

    name: str class Rename(Command):
    """Change a name of the given goal"""

    new_name: str
    goal_id: int = 0 class ToggleSwitchableView(Command):
    """Switch between "only switchable goals" and "all goals" views""" class ToggleOpenView(Command):
    """Switch between "only open goals" and "all goals" views""" class Renderer:
    DEFAULT_WIDTH = 4

    def __init__(self, goals: Graph, width_limit=DEFAULT_WIDTH) -> None:
        self.width_limit = width_limit
        original_graph: Dict[int, Any] = goals.q(
            keys="name,edge,open,select,switchable"
        )
        self.graph: Dict[GoalId, Any] = {k: v for k, v in original_graph.items()}
        self.edges: Dict[GoalId, List[GoalId]] = {
            key: [e[0] for e in values["edge"]] for key, values in self.graph.items()
        }
        self.layers: Dict[int, Layer] = defaultdict(list)
        self.positions: Dict[GoalId, int] = {}
        self.edge_types: Dict[Tuple[GoalId, GoalId], EdgeType] = {
            (parent, child): edge_type
            for parent in self.graph
            for child, edge_type in self.graph[parent]["edge"]
        }
        self.result_edge_options: Dict[str, Tuple[int, int, int]] = {}

    def build(self) -> RenderResult:
        self.split_by_layers()
        self.reorder()
        self.update_graph()
        self.build_index()
        return RenderResult(self.graph, self.result_edge_options)

    def split_by_layers(self) -> None:
        unsorted_goals: Dict[GoalId, List[GoalId]] = dict(self.edges)
        sorted_goals: Set[GoalId] = set()
        incoming_edges: Set[GoalId] = set()
        outgoing_edges: List[GoalId] = []
        current_layer: int = 0
        while unsorted_goals:
            new_layer: Layer = []
            for goal, edges_len in self.candidates_for_new_layer(
                sorted_goals, unsorted_goals
            ):
                unsorted_goals.pop(goal)
                sorted_goals.add(goal)
                new_layer.append(goal)
                back_edges: List[GoalId] = [
                    k for k, vs in self.edges.items() if goal in vs
                ]
                outgoing_edges.extend(iter(back_edges))
                if (len(new_layer) >= self.width_limit and edges_len < 1) or (
                    len(outgoing_edges) >= self.width_limit
                ):
                    break
            incoming_edges = incoming_edges.difference(set(new_layer))
            for original_id in incoming_edges:
                new_goal_name: str = f"{original_id}_{current_layer}"
                self.edges[new_goal_name] = [
                    g
                    for g in self.edges[original_id]
                    if g in sorted_goals and g not in new_layer
                ]
                new_edge_type: EdgeType = EdgeType.BLOCKER
                for g in self.edges[new_goal_name]:
                    self.edges[original_id].remove(g)
                    self.edge_types[new_goal_name, g] = self.edge_types[original_id, g]
                    new_edge_type = max(new_edge_type, self.edge_types[original_id, g])
                self.edges[original_id].append(new_goal_name)
                self.edge_types[original_id, new_goal_name] = new_edge_type
                new_layer.append(new_goal_name)
                sorted_goals.add(new_goal_name)
            self.layers[current_layer] = new_layer
            current_layer += 1
            incoming_edges.update(outgoing_edges)
            outgoing_edges.clear()
        self.positions = {
            g: idx
            for layer in self.layers.values()
            for idx, g in enumerate(layer)
            if g is not None
        }

    @staticmethod
    def candidates_for_new_layer(
        sorted_goals: Set[GoalId], unsorted_goals: Dict[GoalId, List[GoalId]]
    ) -> List[Tuple[GoalId, int]]:
        candidates: List[Tuple[GoalId, int]] = [
            (goal, len(edges))
            for goal, edges in unsorted_goals.items()
            if all(v in sorted_goals for v in edges)
        ]
        candidates.sort(key=lambda x: x[1], reverse=True)
        return candidates

    def reorder(self) -> None:
        for curr_layer in sorted(self.layers.keys(), reverse=True)[:-1]:
            fixed_line: Layer = self.layers[curr_layer]
            random_line: Layer = self.layers[curr_layer - 1]
            deltas: Dict[GoalId, int] = self.count_deltas(fixed_line)
            new_positions: Dict[GoalId, int] = {
                g: int(self.positions[g] + deltas.get(g, 0))
                for g in random_line
                if g is not None
            }

            placed_line: Layer = self.place(new_positions)
            self.positions.update(
                {g: idx for idx, g in enumerate(placed_line) if g is not None}
            )
            self.layers[curr_layer - 1] = placed_line

    def count_deltas(self, fixed_line: Layer) -> Dict[GoalId, int]:
        deltas: Dict[GoalId, List[int]] = defaultdict(list)
        for goal in fixed_line:
            if goal is not None:
                for e in self.edges[goal]:
                    deltas[e].append(self.positions[goal] - self.positions[e])
        return {k: safe_average(v) for k, v in deltas.items()}

    def update_graph(self) -> None:
        for row in sorted(self.layers.keys()):
            real_col: int = 0
            for col, goal_id in enumerate(self.layers[row]):
                if goal_id is None:
                    continue
                if goal_id in self.graph:
                    self.graph[goal_id]["col1"] = real_col
                    real_col += 1
                else:
                    self.graph[goal_id] = {
                        "name": "",
                        "edge": [],
                        "edge_render": [],
                        "switchable": False,
                        "select": None,
                        "open": True,
                    }
                self.graph[goal_id].update(
                    {
                        "row": row,
                        "col": col,
                        "edge_render": [
                            (child, self.edge_types[goal_id, child])
                            for child in self.edges[goal_id]
                        ],
                    }
                )

    def build_index(self) -> None:
        result_index: Dict[int, Dict[int, GoalId]] = {}
        for goal_id, attrs in self.graph.items():
            if (row := attrs["row"]) not in result_index:
                result_index[row] = {}
            result_index[row][attrs["col"]] = goal_id

        for row_vals in result_index.values():
            left: int = 0
            edges: List[str] = []
            phase: str = "goals"

            for _, new_goal_id in sorted(row_vals.items(), key=lambda x: x[0]):
                if isinstance(new_goal_id, int):
                    if phase == "edges":
                        self._write_edges(edges, left)
                        edges = []
                    phase = "goals"
                    left = new_goal_id
                else:
                    phase = "edges"
                    edges.append(new_goal_id)
            self._write_edges(edges, left)

    def _write_edges(self, edges: List[str], left: int) -> None:
        self.result_edge_options.update(
            {e: (left, i + 1, len(edges) + 1) for i, e in enumerate(edges)}
        )

    def place(self, source: Dict[GoalId, int]) -> Layer:
        result: Layer = []
        unplaced: List[Tuple[GoalId, int]] = sorted(list(source.items()), key=goal_key)
        while unplaced:
            value, index = unplaced.pop(0)
            if len(result) < index + 1:
                result.extend([None] * (index + 1 - len(result)))
            if result[index] is None:
                result[index] = value
            else:
                unplaced.insert(0, (value, index + 1))
        while len(result) > self.width_limit and None in result:
            result.remove(None)
        return result class GeometryProvider(Protocol):
    def top_left(self, row, col):
        raise NotImplementedError

    def top_right(self, row, col):
        raise NotImplementedError

    def bottom_left(self, row, col):
        raise NotImplementedError

    def bottom_right(self, row, col):
        raise NotImplementedError def render_lines(
    gp: GeometryProvider, render_result: RenderResult
) -> List[Tuple[EdgeType, int, int, str]]:
    edges = {}
    lines: List[Tuple[EdgeType, int, int, str]] = []

    for goal_id, attrs in render_result.graph.items():
        for e_target, e_type in attrs["edge_render"]:
            target_attrs = render_result.graph[e_target]
            if isinstance(goal_id, int):
                row, col1 = attrs["row"], attrs["col1"]
                start = middle_point(
                    gp.top_left(row, col1), gp.top_right(row, col1), 1, 2
                )
            else:
                left_id, p, q = render_result.edge_opts[goal_id]
                left = render_result.graph[left_id]["col1"] if left_id > 0 else -1
                x1 = gp.top_right(attrs["row"], left)
                x2 = gp.top_left(attrs["row"], left + 1)
                start = middle_point(x1, x2, p, q)

                if goal_id not in edges:
                    edges[goal_id] = {"bottom": start, "style": e_type}
                else:
                    edges[goal_id]["bottom"] = start
                    edges[goal_id]["style"] = max(edges[goal_id]["style"], e_type)
            if isinstance(e_target, int):
                row, col1 = target_attrs["row"], target_attrs["col1"]
                end = middle_point(
                    gp.bottom_left(row, col1), gp.bottom_right(row, col1), 1, 2
                )
            else:
                left_id, p, q = render_result.edge_opts[e_target]
                left = render_result.graph[left_id]["col1"] if left_id > 0 else -1
                x1 = gp.bottom_right(target_attrs["row"], left)
                x2 = gp.bottom_left(target_attrs["row"], left + 1)
                end = middle_point(x1, x2, p, q)

                if e_target not in edges:
                    edges[e_target] = {"top": end, "style": e_type}
                else:
                    edges[e_target]["top"] = end
                    edges[e_target]["style"] = max(edges[e_target]["style"], e_type)
            lines.append((e_type, start, end, f"{goal_id}-{e_target}"))

    for e in edges.values():
        lines.append((e["style"], e["bottom"], e["top"], ""))

    return lines def save(goals: Graph, filename: str) -> None:
    if path.isfile(filename):
        connection = sqlite3.connect(filename)
        run_migrations(connection)
        save_updates(goals, connection)
    else:
        connection = sqlite3.connect(filename)
        save_connection(goals, connection)

    connection.close() def load(filename: str, message_fn: Callable[[str], None] = None) -> Enumeration:
    zoom_data: ZoomData = []
    autolink_data: AutoLinkData = []
    if path.isfile(filename):
        connection = sqlite3.connect(filename)
        run_migrations(connection)
        cur = connection.cursor()
        names = list(cur.execute("select * from goals"))
        edges = list(cur.execute("select parent, child, reltype from edges"))
        settings = list(cur.execute("select * from settings"))
        zoom_data = list(cur.execute("select * from zoom"))
        autolink_data = list(cur.execute("select * from autolink"))
        cur.close()
        goals = Goals.build(names, edges, settings, message_fn)
    else:
        goals = Goals("Rename me", message_fn)
    result = Enumeration(all_layers(goals, zoom_data, autolink_data))
    result.verify()
    return result def split_long(line: str) -> str:
    margin = 20
    parts = []
    space_position = line.find(" ", margin)
    while space_position > 0:
        part, line = line[:space_position], line[(space_position + 1) :]
        parts.append(part)
        space_position = line.find(" ", margin)
    parts.append(line)
    return "\n".join(parts) class Ui_GoalBody(object):
    def setupUi(self, GoalBody):
        GoalBody.setObjectName("GoalBody")
        GoalBody.resize(228, 72)
        sizePolicy = QtWidgets.QSizePolicy(
            QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed
        )
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(GoalBody.sizePolicy().hasHeightForWidth())
        GoalBody.setSizePolicy(sizePolicy)
        self.horizontalLayout = QtWidgets.QHBoxLayout(GoalBody)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.frame = QtWidgets.QFrame(GoalBody)
        self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
        self.frame.setObjectName("frame")
        self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.frame)
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")
        self.check_open = QtWidgets.QCheckBox(self.frame)
        sizePolicy = QtWidgets.QSizePolicy(
            QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed
        )
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.check_open.sizePolicy().hasHeightForWidth())
        self.check_open.setSizePolicy(sizePolicy)
        self.check_open.setText("")
        self.check_open.setObjectName("check_open")
        self.horizontalLayout_3.addWidget(self.check_open)
        self.label_number = QtWidgets.QLabel(self.frame)
        sizePolicy = QtWidgets.QSizePolicy(
            QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed
        )
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.label_number.sizePolicy().hasHeightForWidth())
        self.label_number.setSizePolicy(sizePolicy)
        font = QtGui.QFont()
        font.setPointSize(12)
        font.setBold(True)
        font.setWeight(75)
        self.label_number.setFont(font)
        self.label_number.setText("")
        self.label_number.setObjectName("label_number")
        self.horizontalLayout_3.addWidget(self.label_number)
        self.label_goal_name = QtWidgets.QLabel(self.frame)
        sizePolicy = QtWidgets.QSizePolicy(
            QtWidgets.QSizePolicy.MinimumExpanding,
            QtWidgets.QSizePolicy.MinimumExpanding,
        )
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.label_goal_name.sizePolicy().hasHeightForWidth()
        )
        self.label_goal_name.setSizePolicy(sizePolicy)
        self.label_goal_name.setText("")
        self.label_goal_name.setObjectName("label_goal_name")
        self.horizontalLayout_3.addWidget(self.label_goal_name)
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.horizontalLayout_3.addLayout(self.horizontalLayout_2)
        self.horizontalLayout.addWidget(self.frame)

        self.retranslateUi(GoalBody)
        QtCore.QMetaObject.connectSlotsByName(GoalBody)

    def retranslateUi(self, GoalBody):
        _translate = QtCore.QCoreApplication.translate
        GoalBody.setWindowTitle(_translate("GoalBody", "Form")) class ToggleZoom(Command):
    """Hide or show all goals blocked by the current one"""
import sys
from argparse import ArgumentParser
from os.path import dirname, join, realpath
from PyQt5.QtCore import pyqtSignal, Qt, QRect, QPoint  # type: ignore
from PyQt5.QtGui import QPainter, QPen  # type: ignore
from PyQt5.QtWidgets import (  # type: ignore
    QApplication,
    QMainWindow,
    QWidget,
    QGridLayout,
    QFileDialog,
)
from PyQt5.uic import loadUi  # type: ignore
from siebenapp.autolink import ToggleAutoLink
from siebenapp.progress_view import ToggleProgress
from siebenapp.filter_view import FilterBy
from siebenapp.domain import (
    EdgeType,
    HoldSelect,
    ToggleClose,
    Delete,
    ToggleLink,
    Add,
    Select,
    Insert,
    Rename,
)
from siebenapp.switchable_view import ToggleSwitchableView
from siebenapp.open_view import ToggleOpenView
from siebenapp.render import Renderer, GeometryProvider, render_lines
from siebenapp.system import save, load, split_long
from siebenapp.ui.goalwidget import Ui_GoalBody  # type: ignore
from siebenapp.zoom import ToggleZoom

    def setupData(self, render_result):
        self.render_result = render_result

    def itemAt(self, row, col):
        if col < 0:
            g = self.itemAt(row, 0).geometry()
            return QRect(0, g.topLeft().y(), 0, g.height())
        item = self.layout().itemAtPosition(row, col)
        if item is not None:
            return item
        g = self.itemAt(row, col - 1).geometry()
        return QRect(g.topRight().x() + 100, g.topRight().y(), 0, g.height())

    def top_left(self, row, col):
        w = self.itemAt(row, col)
        return w.topLeft() if isinstance(w, QRect) else w.geometry().topLeft()

    def top_right(self, row, col):
        w = self.itemAt(row, col)
        return w.topRight() if isinstance(w, QRect) else w.geometry().topRight()

    def bottom_left(self, row, col):
        w = self.itemAt(row, col)
        return w.bottomLeft() if isinstance(w, QRect) else w.geometry().bottomLeft()

    def bottom_right(self, row, col):
        w = self.itemAt(row, col)
        return w.bottomRight() if isinstance(w, QRect) else w.geometry().bottomRight()

    def paintEvent(self, event):
        painter = QPainter(self)
        lines = render_lines(self, self.render_result)

        for edge_type, start, end, _ in lines:
            painter.setPen(self.EDGE_PENS[edge_type])
            painter.drawLine(start, end)


class SiebenApp(QMainWindow):
    refresh = pyqtSignal()
    quit_app = pyqtSignal()

    def __init__(self, db, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.refresh.connect(self.save_and_render)
        self.quit_app.connect(QApplication.instance().quit)
        self.db = db
        self.goals = load(db, self.show_user_message)
        self.force_refresh = True
        self.columns = Renderer.DEFAULT_WIDTH

    def setup(self):
        self.action_New.triggered.connect(self.show_new_dialog)
        self.action_Open.triggered.connect(self.show_open_dialog)
        self.action_Hotkeys.triggered.connect(self.hotkeys.show)
        self.action_About.triggered.connect(self.about.show)
        self.toggleOpen.clicked.connect(self.with_refresh(self.toggle_open_view, False))
        self.toggleSwitchable.clicked.connect(
            self.with_refresh(self.toggle_switchable_view, False)
        )
        self.toggleProgress.clicked.connect(
            self.with_refresh(self.toggle_progress_view, False)
        )
        # Re-creation of scrollAreaWidgetContents looks like dirty hack,
        # but at the current moment I haven't found a better solution.
        # Widget creation in __init__ does not work: lines disappear.
        self.scrollAreaWidgetContents = CentralWidget()
        # End of 'looks like dirty hack'
        self.scrollArea.setWidget(self.scrollAreaWidgetContents)
        self._update_title()
        self.refresh.emit()

    def _update_title(self):
        self.setWindowTitle(f"{self.db} - SiebenApp")

    def close_goal(self, goal_id):
        def inner():
            self.goals.accept_all(Select(goal_id), ToggleClose())
            self.refresh.emit()

        return inner

    def save_and_render(self):
        if not self.goals.events() and not self.force_refresh:
            return
        self.force_refresh = False
        self.statusBar().clearMessage()
        save(self.goals, self.db)
        for child in self.scrollAreaWidgetContents.children():
            if isinstance(child, GoalWidget):
                child.deleteLater()
        render_result = Renderer(self.goals, self.columns).build()
        if "setupData" in dir(self.scrollAreaWidgetContents):
            self.scrollAreaWidgetContents.setupData(render_result)
        for goal_id, attributes in render_result.graph.items():
            if isinstance(goal_id, int):
                widget = GoalWidget()
                self.scrollAreaWidgetContents.layout().addWidget(
                    widget, attributes["row"], attributes["col1"]
                )
                widget.setup_data(goal_id, attributes)
                widget.clicked.connect(self.select_number(goal_id))
                widget.check_open.clicked.connect(self.close_goal(goal_id))
        self.scrollAreaWidgetContents.update()

    def keyPressEvent(self, event):
        key_handlers = {
            Qt.Key_1: self.select_number(1),
            Qt.Key_2: self.select_number(2),
            Qt.Key_3: self.select_number(3),
            Qt.Key_4: self.select_number(4),
            Qt.Key_5: self.select_number(5),
            Qt.Key_6: self.select_number(6),
            Qt.Key_7: self.select_number(7),
            Qt.Key_8: self.select_number(8),
            Qt.Key_9: self.select_number(9),
            Qt.Key_0: self.select_number(0),
            Qt.Key_A: self.start_edit("Add new goal", self.emit_add),
            Qt.Key_C: self.with_refresh(self.goals.accept, ToggleClose()),

            Qt.Key_D: self.with_refresh(self.goals.accept, Delete()),