import logging from copy import copy from dataclasses import dataclass, field from enum import IntEnum, auto from pathlib import Path from typing import Any, Optional, Self from PySide6.QtCore import ( QAbstractListModel, QAbstractItemModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property ) from PySide6.QtQml import QmlElement from kodereviewer.project import Project from kodereviewer.data import ChangedFile, Label, PullRequest QML_IMPORT_NAME = "org.deprecated.kodereviewer" QML_IMPORT_MAJOR_VERSION = 1 logger = logging.getLogger(__name__) @dataclass class FileItem: path: str icon: str name: str parent: Self | None patch: str | None = None children: list[Self] = field(default_factory=list) def append(self, child: Self): self.children.append(child) def child(self, row: int) -> Self | None: if row >= 0 and row < len(self.children): return self.children[row] return None def row(self) -> int: """Reports the item's location within it's parent.""" if self.parent is None: return 0 for i, child in enumerate(self.parent.children): if self is child: return i raise Exception('Should not happen!') def column_count(self) -> int: """Only the filename.""" return 1 def is_file(self) -> bool: return self.patch is not None def __str__(self) -> str: return f'{self.name} | {id(self.parent)} | {[x.name for x in self.children]}' @QmlElement class TreeFileModel(QAbstractItemModel): filenames: list[str] = field(default_factory=list) dir_mapping: dict[str, FileItem] = field(default_factory=dict) root_node: FileItem _pull_request: Optional[PullRequest] class Roles(IntEnum): Filename = Qt.ItemDataRole.UserRole + 1 Path = auto() IconName = auto() Patch = auto() IsFile = auto() def __init__(self): super().__init__() self.root_node = FileItem('./', '', './', parent=None) self._pull_request = None def load_files(self, files: list[ChangedFile]): self.dir_mapping: dict[str, FileItem] = {} root_node = FileItem('./', '', './', parent=None) for file in files: p = Path(file.filename) directories = p.parts[:-1] fname = p.name current_path = Path('') parent: FileItem = root_node for dir in directories: current_path = current_path / dir if str(current_path) not in self.dir_mapping: self.dir_mapping[str(current_path)] = FileItem( path=str(current_path), name=current_path.name, icon='inode-directory-symbolic', parent=parent ) parent.append(self.dir_mapping[str(current_path)]) parent = self.dir_mapping[str(current_path)] file_item = FileItem(path=str(p), name=fname, icon='document-open-symbolic', patch=file.patch, parent=parent) parent.append(file_item) self.root_node = root_node logger.info(self.root_node) pullRequestChanged = Signal() def get_pull_request(self) -> Optional[PullRequest]: return self._pull_request def set_pull_request(self, pull_request: Optional[PullRequest]) -> None: if pull_request is None: return if self._pull_request is not None and self._pull_request == pull_request: return self.beginResetModel() self._pull_request = pull_request self._pull_request.filesLoaded.connect(self._reset_model) self.endResetModel() self.pullRequestChanged.emit() pullRequest = Property(PullRequest, fget=get_pull_request, fset=set_pull_request, notify=pullRequestChanged) def _reset_model(self) -> None: logger.info('reseting model') self.beginResetModel() self.load_files(self._pull_request.files) self.endResetModel() def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex: """Returns the index of the item in the model specified by the given row, column and parent index.""" if not self.hasIndex(row, column, parent): return QModelIndex() item: FileItem if not parent.isValid(): item = self.root_node else: item = parent.internalPointer() if item.child(row): return self.createIndex(row, column, item.child(row)) return QModelIndex() def parent(self, index: QModelIndex) -> QModelIndex: """Returns the parent of the model item with the given index If the item has no parent, an invalid QModelIndex is returned. """ if not index.isValid(): return QModelIndex() item: FileItem = index.internalPointer() parent: FileItem = item.parent if parent == self.root_node: return QModelIndex() return self.createIndex(parent.row(), 0, parent) def rowCount(self, index: QModelIndex = QModelIndex()) -> int: """Returns the number of rows under the given parent When the parent is valid it means that rowCount is returning the number of children of parent. """ parent: FileItem if index.isValid(): parent = index.internalPointer() else: parent = self.root_node return len(parent.children) def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: return 1 def data(self, index: QModelIndex | QPersistentModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any: if not index.isValid(): return '' item: FileItem = index.internalPointer() if role == Qt.ItemDataRole.DisplayRole: return item.name if role == self.Roles.Filename: return item.name if role == self.Roles.Path: return item.path if role == self.Roles.IconName: return item.icon if role == self.Roles.Patch: return item.patch if role == self.Roles.IsFile: return item.is_file() return None def roleNames(self) -> dict[int, QByteArray]: return { self.Roles.Filename: QByteArray(b'filename'), self.Roles.IconName: QByteArray(b'iconName'), self.Roles.Path: QByteArray(b'path'), self.Roles.Patch: QByteArray(b'patch'), self.Roles.IsFile: QByteArray(b'isFile'), } @QmlElement class FileModel(QAbstractListModel): _pull_request: Optional[PullRequest] class Roles(IntEnum): Sha = Qt.ItemDataRole.UserRole + 1 Filename = auto() Status = auto() Additions = auto() Deletions = auto() Changes = auto() Patch = auto() def __init__(self): super().__init__() self._pull_request = None pullRequestChanged = Signal() def get_pull_request(self) -> Optional[PullRequest]: return self._pull_request def set_pull_request(self, pull_request: Optional[PullRequest]) -> None: if pull_request is None: return if self._pull_request is not None and self._pull_request == pull_request: return self.beginResetModel() self._pull_request = pull_request self._pull_request.filesLoaded.connect(self._reset_model) self.endResetModel() self.pullRequestChanged.emit() pullRequest = Property(PullRequest, fget=get_pull_request, fset=set_pull_request, notify=pullRequestChanged) def _reset_model(self) -> None: self.beginResetModel() self.endResetModel() def data(self, index: QModelIndex | QPersistentModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> object: if self._pull_request is None: return None file: ChangedFile = self._pull_request.files[index.row()] if role == self.Roles.Sha: return file.sha if role == self.Roles.Filename: return file.filename if role == self.Roles.Additions: return file.additions if role == self.Roles.Deletions: return file.deletions if role == self.Roles.Changes: return file.changes if role == self.Roles.Patch: return file.patch if role == Qt.ItemDataRole.DisplayRole: return file.filename return None def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: if self._pull_request is None: return 0 return len(self._pull_request.files) def roleNames(self) -> dict[int, QByteArray]: return { self.Roles.Sha: QByteArray(b'sha'), self.Roles.Filename: QByteArray(b'filename'), self.Roles.Additions: QByteArray(b'additions'), self.Roles.Deletions: QByteArray(b'deletions'), self.Roles.Changes: QByteArray(b'changes'), self.Roles.Patch: QByteArray(b'patch'), }