mirror of
https://github.com/borgbase/vorta
synced 2025-03-11 22:53:33 +00:00
Implement fresh TreeModel for TreeViews.
* src/vorta/views/partials/treemodel.py * tests/test_treemodel.py : Write tests.
This commit is contained in:
parent
33639aeaed
commit
3cd940455c
3 changed files with 1221 additions and 2 deletions
|
@ -26,8 +26,9 @@ from vorta.borg.umount import BorgUmountJob
|
||||||
from vorta.store.models import ArchiveModel, BackupProfileMixin
|
from vorta.store.models import ArchiveModel, BackupProfileMixin
|
||||||
from vorta.utils import (choose_file_dialog, format_archive_name, get_asset,
|
from vorta.utils import (choose_file_dialog, format_archive_name, get_asset,
|
||||||
get_mount_points, pretty_bytes)
|
get_mount_points, pretty_bytes)
|
||||||
from vorta.views.diff_result import DiffResult
|
from vorta.views import diff_result, extract_dialog
|
||||||
from vorta.views.extract_dialog import ExtractDialog
|
from vorta.views.diff_result import DiffResultDialog, DiffTree
|
||||||
|
from vorta.views.extract_dialog import ExtractDialog, ExtractTree
|
||||||
from vorta.views.source_tab import SizeItem
|
from vorta.views.source_tab import SizeItem
|
||||||
from vorta.views.utils import get_colored_icon
|
from vorta.views.utils import get_colored_icon
|
||||||
|
|
||||||
|
|
892
src/vorta/views/partials/treemodel.py
Normal file
892
src/vorta/views/partials/treemodel.py
Normal file
|
@ -0,0 +1,892 @@
|
||||||
|
"""
|
||||||
|
Implementation of a tree model for use with `QTreeView` based on (file) paths.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import bisect
|
||||||
|
import enum
|
||||||
|
import os.path as osp
|
||||||
|
from functools import reduce
|
||||||
|
from pathlib import PurePath
|
||||||
|
from typing import (Generic, List, Optional, Sequence, Tuple, TypeVar, Union,
|
||||||
|
overload)
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QAbstractItemModel, QModelIndex, QObject, Qt
|
||||||
|
|
||||||
|
#: A representation of a path
|
||||||
|
Path = Tuple[str, ...]
|
||||||
|
PathLike = Union[Path, Sequence[str]]
|
||||||
|
|
||||||
|
|
||||||
|
def relative_path(p1: PathLike, p2: PathLike) -> Path:
|
||||||
|
"""Get p2 relative to p1."""
|
||||||
|
if len(p2) <= len(p1):
|
||||||
|
return ()
|
||||||
|
|
||||||
|
return tuple(p2[len(p1):])
|
||||||
|
|
||||||
|
|
||||||
|
def path_to_str(path: PathLike) -> str:
|
||||||
|
"""Return a string representation of a path."""
|
||||||
|
if not path:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
return osp.join(*path)
|
||||||
|
|
||||||
|
|
||||||
|
#: Type of FileSystemItem's data
|
||||||
|
T = TypeVar('T')
|
||||||
|
FileSystemItemLike = Union[Tuple[Union[PurePath, Path], Optional[T]],
|
||||||
|
'FileSystemItem']
|
||||||
|
|
||||||
|
#: Default return value
|
||||||
|
A = TypeVar('A')
|
||||||
|
|
||||||
|
|
||||||
|
class FileSystemItem(Generic[T]):
|
||||||
|
"""
|
||||||
|
An item in the virtual file system.
|
||||||
|
|
||||||
|
..warning::
|
||||||
|
|
||||||
|
Do not edit `children` manually. Always use `add` or `remove` or
|
||||||
|
`sort`.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
path : Path
|
||||||
|
The path of this item.
|
||||||
|
data : Any
|
||||||
|
The data belonging to this item.
|
||||||
|
children : List[FileSystemItem]
|
||||||
|
The children of this item.
|
||||||
|
_subpath : str
|
||||||
|
The subpath of this item relative to its parent.
|
||||||
|
_parent : FileSystemItem or None
|
||||||
|
The parent of the item.
|
||||||
|
"""
|
||||||
|
__slots__ = ['path', 'children', 'data', '_parent', 'subpath']
|
||||||
|
|
||||||
|
def __init__(self, path: PathLike, data: T):
|
||||||
|
"""Init."""
|
||||||
|
self.path: Path = tuple(path)
|
||||||
|
self.data = data
|
||||||
|
self.subpath: str = None
|
||||||
|
self.children: List[FileSystemItem[T]] = []
|
||||||
|
self._parent: Optional[FileSystemItem[T]] = None
|
||||||
|
|
||||||
|
# @property
|
||||||
|
# def subpath(self) -> str:
|
||||||
|
# """
|
||||||
|
# Get the name of the item which is the subpath relative to its parent.
|
||||||
|
# """
|
||||||
|
# return self.path[-1]
|
||||||
|
|
||||||
|
# @property
|
||||||
|
# def children(self):
|
||||||
|
# """Get an iterable view of the item's children."""
|
||||||
|
# return self.child_map.values()
|
||||||
|
|
||||||
|
def add(self,
|
||||||
|
child: 'FileSystemItem[T]',
|
||||||
|
_subpath: str = None,
|
||||||
|
_check: bool = True):
|
||||||
|
"""
|
||||||
|
Add a child.
|
||||||
|
|
||||||
|
The parameters starting with an underscore exist for performance
|
||||||
|
reasons only. They should only be used if the operations that these
|
||||||
|
parameters toggle were performed already.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
child : FileSystemItem
|
||||||
|
The child to add.
|
||||||
|
_subpath : str, optional
|
||||||
|
Precalculated subpath, default is None.
|
||||||
|
_check : bool, optional
|
||||||
|
Whether to check for children with the same subpath (using `get`).
|
||||||
|
"""
|
||||||
|
if _subpath is not None:
|
||||||
|
child.subpath = _subpath
|
||||||
|
else:
|
||||||
|
child.subpath = path_to_str(relative_path(self.path, child.path))
|
||||||
|
|
||||||
|
i = bisect.bisect(self.children, child)
|
||||||
|
|
||||||
|
# check for a child with the same subpath
|
||||||
|
if _check and len(self.children) > i - 1 >= 0 \
|
||||||
|
and self.children[i - 1].subpath == child.subpath:
|
||||||
|
raise RuntimeError(
|
||||||
|
"The subpath must be unique to a parent's children.")
|
||||||
|
|
||||||
|
# add
|
||||||
|
child._parent = self
|
||||||
|
self.children.insert(i, child)
|
||||||
|
|
||||||
|
def addChildren(self, children: List['FileSystemItem[T]']):
|
||||||
|
"""
|
||||||
|
Add a list of children.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
children : List[FileSystemItem]
|
||||||
|
The children to add.
|
||||||
|
"""
|
||||||
|
for child in children:
|
||||||
|
self.add(child)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def remove(self, subpath: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def remove(self, index: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def remove(self, child: 'FileSystemItem[T]') -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def remove(self, child_subpath_index):
|
||||||
|
"""
|
||||||
|
Remove the given children.
|
||||||
|
|
||||||
|
The index or child to remove must be in the list
|
||||||
|
else an error will be raised.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
child_or_index : FileSystemItem or int
|
||||||
|
The instance to remove or its index in `children`.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
The given item is not a child of this one.
|
||||||
|
IndexError
|
||||||
|
The given index is not a valid one.
|
||||||
|
"""
|
||||||
|
if isinstance(child_subpath_index, FileSystemItem):
|
||||||
|
child = child_subpath_index
|
||||||
|
i = bisect.bisect_left(self.children, child)
|
||||||
|
if i < len(self.children) and self.children[i] == child:
|
||||||
|
del self.children[i]
|
||||||
|
|
||||||
|
elif isinstance(child_subpath_index, str):
|
||||||
|
subpath = child_subpath_index
|
||||||
|
i = bisect.bisect_left(self.children, subpath)
|
||||||
|
if i < len(self.children) and self.children[i].subpath == subpath:
|
||||||
|
del self.children[i]
|
||||||
|
|
||||||
|
elif isinstance(child_subpath_index, int):
|
||||||
|
i = child_subpath_index
|
||||||
|
del self.children[i]
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
"First argument passed to `{}.remove` has invalid type {}".
|
||||||
|
format(
|
||||||
|
type(self).__name__,
|
||||||
|
type(child_subpath_index).__name__))
|
||||||
|
|
||||||
|
def __getitem__(self, index: int):
|
||||||
|
"""
|
||||||
|
Get a an item.
|
||||||
|
|
||||||
|
This allows accessing the attributes in the same manner for instances
|
||||||
|
of this type and instances of `FileSystemItemLike`.
|
||||||
|
"""
|
||||||
|
if index == 0:
|
||||||
|
return self.path
|
||||||
|
elif index == 1:
|
||||||
|
return self.data
|
||||||
|
else:
|
||||||
|
raise IndexError("Index {} out of range(0, 2)".format(index))
|
||||||
|
|
||||||
|
def get(
|
||||||
|
self,
|
||||||
|
subpath: str,
|
||||||
|
default: Optional[A] = None
|
||||||
|
) -> Union[Tuple[int, 'FileSystemItem[T]'], Optional[A]]:
|
||||||
|
"""
|
||||||
|
Find direct child with given subpath.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
subpath : str
|
||||||
|
The items subpath relative to this.
|
||||||
|
default : Any, optional
|
||||||
|
The item to return if the child wasn't found, default None.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[int, FileSystemItem] or None
|
||||||
|
The index and item if found else `default`.
|
||||||
|
"""
|
||||||
|
i = bisect.bisect_left(self.children, subpath)
|
||||||
|
if i < len(self.children):
|
||||||
|
child = self.children[i]
|
||||||
|
if child.subpath == subpath:
|
||||||
|
return i, child
|
||||||
|
return default
|
||||||
|
|
||||||
|
def get_path(self, path: PathLike) -> Optional['FileSystemItem[T]']:
|
||||||
|
"""
|
||||||
|
Get the item with the given subpath relative to this item.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path : Path
|
||||||
|
The subpath.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def walk(fsi, pp):
|
||||||
|
if fsi is None:
|
||||||
|
return None
|
||||||
|
res = fsi.get(pp)
|
||||||
|
if not res:
|
||||||
|
return None
|
||||||
|
i, item = res
|
||||||
|
return item
|
||||||
|
|
||||||
|
fsi = reduce(walk, path, self)
|
||||||
|
return fsi
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""Get a string representation."""
|
||||||
|
return "FileSystemItem<'{}', '{}', {}, {}>".format(
|
||||||
|
self.path,
|
||||||
|
self.subpath,
|
||||||
|
self.data,
|
||||||
|
[c.subpath for c in self.children],
|
||||||
|
)
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
"""Lower than for bisect sorting."""
|
||||||
|
if isinstance(other, FileSystemItem):
|
||||||
|
return self.subpath < other.subpath
|
||||||
|
if isinstance(other, (list, tuple)):
|
||||||
|
for s, o in zip(self.path, other):
|
||||||
|
if s != o:
|
||||||
|
return s < o
|
||||||
|
else:
|
||||||
|
return len(self.path) < len(other)
|
||||||
|
else:
|
||||||
|
return self.subpath < other
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
"""Greater than for bisect sorting."""
|
||||||
|
if isinstance(other, FileSystemItem):
|
||||||
|
return self.subpath > other.subpath
|
||||||
|
if isinstance(other, (list, tuple)):
|
||||||
|
for s, o in zip(self.path, other):
|
||||||
|
if s != o:
|
||||||
|
return s > o
|
||||||
|
else:
|
||||||
|
return len(self.path) > len(other)
|
||||||
|
else:
|
||||||
|
return self.subpath > other
|
||||||
|
|
||||||
|
|
||||||
|
class FileTreeModel(QAbstractItemModel, Generic[T]):
|
||||||
|
"""
|
||||||
|
FileTreeModel managing a virtual file system with variable file data.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
mode: DisplayMode
|
||||||
|
The tree display mode of the model.
|
||||||
|
|
||||||
|
Methods
|
||||||
|
-------
|
||||||
|
_make_filesystemitem(path, data, children)
|
||||||
|
Construct a `FileSystemItem`.
|
||||||
|
_merge_data(item, data)
|
||||||
|
Add the given data to the item.
|
||||||
|
_flat_filter
|
||||||
|
Return whether an item is part of the flat model representation.
|
||||||
|
flags
|
||||||
|
columnCount
|
||||||
|
headerData
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
class DisplayMode(enum.Enum):
|
||||||
|
"""
|
||||||
|
The tree display modes available for the model.
|
||||||
|
|
||||||
|
"""
|
||||||
|
#: normal file tree
|
||||||
|
TREE = enum.auto()
|
||||||
|
|
||||||
|
#: combine items in the tree having a single child with that child
|
||||||
|
SIMPLIFIED_TREE = enum.auto()
|
||||||
|
|
||||||
|
#: simple list of items
|
||||||
|
FLAT = enum.auto()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
"""Init."""
|
||||||
|
super().__init__(parent)
|
||||||
|
self.root: FileSystemItem[T] = FileSystemItem([], None)
|
||||||
|
|
||||||
|
#: mode
|
||||||
|
self.mode: 'FileTreeModel.DisplayMode' = self.DisplayMode.TREE
|
||||||
|
|
||||||
|
#: flat representation of the tree
|
||||||
|
self._flattened: List[FileSystemItem] = []
|
||||||
|
|
||||||
|
def addItems(self, items: List[FileSystemItemLike[T]]):
|
||||||
|
"""
|
||||||
|
Add file system items to the model.
|
||||||
|
|
||||||
|
This method can be used for populating the model.
|
||||||
|
Calls `addItem` for each item.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
items : List[FileSystemItemLike]
|
||||||
|
The items.
|
||||||
|
"""
|
||||||
|
for item in items:
|
||||||
|
self.addItem(item)
|
||||||
|
|
||||||
|
def addItem(self, item: FileSystemItemLike[T]):
|
||||||
|
"""
|
||||||
|
Add a file system item to the model.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item : FileSystemItemLike
|
||||||
|
The item.
|
||||||
|
"""
|
||||||
|
self.beginResetModel()
|
||||||
|
|
||||||
|
path = item[0]
|
||||||
|
data = item[1]
|
||||||
|
|
||||||
|
if isinstance(path, PurePath):
|
||||||
|
path = path.parts
|
||||||
|
|
||||||
|
def child(tup, subpath):
|
||||||
|
fsi, i = tup
|
||||||
|
i += 1
|
||||||
|
return self._addChild(fsi, path[:i], subpath, None), i
|
||||||
|
|
||||||
|
fsi, dummy = reduce(child, path[:-1], (self.root, 0))
|
||||||
|
|
||||||
|
self._addChild(fsi, path, path[-1], data)
|
||||||
|
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
def _addChild(self, item: FileSystemItem[T], path: PathLike,
|
||||||
|
path_part: str, data: Optional[T]) -> FileSystemItem[T]:
|
||||||
|
"""
|
||||||
|
Add a child to an item.
|
||||||
|
|
||||||
|
This is called by `addItem` in a reduce statement. It should
|
||||||
|
add a new child with the given attributes to the given item.
|
||||||
|
This implementation provides a reasonable default, most subclasses
|
||||||
|
wont need to override this method. The implementation should make use
|
||||||
|
of `_make_filesystemitem`, `_merge_data`, `_add_children`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item : FileSystemItem
|
||||||
|
The item to add a new child to.
|
||||||
|
path : PathLike
|
||||||
|
The path of the new child.
|
||||||
|
path_part : str
|
||||||
|
The subpath of the new child relative to `item`.
|
||||||
|
data : Any or None
|
||||||
|
The data of the new child.
|
||||||
|
children : Any or None
|
||||||
|
The initial children of the item.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
FileSystemItem
|
||||||
|
[description]
|
||||||
|
"""
|
||||||
|
res = item.get(path_part)
|
||||||
|
if res:
|
||||||
|
i, child = res
|
||||||
|
if data is not None:
|
||||||
|
self._merge_data(child, data)
|
||||||
|
else:
|
||||||
|
child = self._make_filesystemitem(path, data)
|
||||||
|
|
||||||
|
if self._flat_filter(child):
|
||||||
|
i = bisect.bisect(self._flattened, child.path)
|
||||||
|
self._flattened.insert(i, child)
|
||||||
|
|
||||||
|
item.add(child, _subpath=path_part, _check=False)
|
||||||
|
|
||||||
|
# update parent data
|
||||||
|
self._process_child(child)
|
||||||
|
|
||||||
|
return child
|
||||||
|
|
||||||
|
def _make_filesystemitem(self, path: PathLike,
|
||||||
|
data: Optional[T]) -> FileSystemItem[T]:
|
||||||
|
"""
|
||||||
|
Construct a `FileSystemItem`.
|
||||||
|
|
||||||
|
The attributes are the attributes of a `FileSystemItemLike`.
|
||||||
|
This implementation already provides reasonable default that
|
||||||
|
subclasses can be used.
|
||||||
|
|
||||||
|
..warning:: Do always call `_addChild` to add a child to an item.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path : PathLike
|
||||||
|
The path of the item.
|
||||||
|
data : Any or None
|
||||||
|
The data.
|
||||||
|
children : Any or None
|
||||||
|
The initial children.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
FileSystemItem
|
||||||
|
The FileSystemItem for the internal tree structure.
|
||||||
|
"""
|
||||||
|
return FileSystemItem(path, data)
|
||||||
|
|
||||||
|
def _process_child(self, child: FileSystemItem[T]):
|
||||||
|
"""
|
||||||
|
Process a new child.
|
||||||
|
|
||||||
|
This can make some changes to the child's data like
|
||||||
|
setting a default value if the child's data is None.
|
||||||
|
This can also update the data of the parent.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
child : FileSystemItem
|
||||||
|
The child that was added.
|
||||||
|
"""
|
||||||
|
pass # Does nothing
|
||||||
|
|
||||||
|
def _merge_data(self, item: FileSystemItem[T], data: Optional[T]):
|
||||||
|
"""
|
||||||
|
Add the given data to the item.
|
||||||
|
|
||||||
|
This method is called by `_addChild` which in turn is called by
|
||||||
|
`addItem`. It gets an item in the virtual file system that was
|
||||||
|
added again with the given data. The data may be None.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
item : FileSystemItem
|
||||||
|
The item to merge the data in.
|
||||||
|
data : Any or None
|
||||||
|
The data to add.
|
||||||
|
"""
|
||||||
|
if not item.data:
|
||||||
|
item.data = data
|
||||||
|
|
||||||
|
def removeItem(self, path: Union[PurePath, PathLike]) -> None:
|
||||||
|
"""
|
||||||
|
Remove an item from the model.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path : PathLike or PurePath
|
||||||
|
The path of the item to remove.
|
||||||
|
"""
|
||||||
|
if isinstance(path, PurePath):
|
||||||
|
path = path.parts
|
||||||
|
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.beginResetModel()
|
||||||
|
|
||||||
|
parent = self.getItem(path[:-1])
|
||||||
|
|
||||||
|
if not parent:
|
||||||
|
return
|
||||||
|
|
||||||
|
res = parent.get(path[-1])
|
||||||
|
|
||||||
|
if not res:
|
||||||
|
return
|
||||||
|
|
||||||
|
i, item = res
|
||||||
|
|
||||||
|
# remove item and its children in flat representation
|
||||||
|
items_to_remove: List[FileSystemItem] = [item]
|
||||||
|
while items_to_remove:
|
||||||
|
to_remove = items_to_remove.pop()
|
||||||
|
|
||||||
|
fi = bisect.bisect_left(self._flattened, to_remove.path)
|
||||||
|
if fi < len(self._flattened) and self._flattened[fi] is to_remove:
|
||||||
|
del self._flattened[fi]
|
||||||
|
|
||||||
|
items_to_remove.extend(to_remove.children)
|
||||||
|
|
||||||
|
# remove item from tree representation
|
||||||
|
parent.remove(i)
|
||||||
|
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
def setMode(self, value: 'DisplayMode'):
|
||||||
|
"""
|
||||||
|
Set the display mode of the tree model.
|
||||||
|
|
||||||
|
In TREE mode (default) the tree will be displayed as is.
|
||||||
|
In SIMPLIFIED_TREE items will simplify the tree by combining
|
||||||
|
items with their single child if they posess only one.
|
||||||
|
In FLAT mode items will be displayed as a simple list. The items
|
||||||
|
shown can be filtered by `_flat_filter`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
value : bool
|
||||||
|
The new value for the attribute.
|
||||||
|
|
||||||
|
See also
|
||||||
|
--------
|
||||||
|
getMode: Get the current mode.
|
||||||
|
_flat_filter
|
||||||
|
"""
|
||||||
|
if value == self.mode:
|
||||||
|
return # nothing to do
|
||||||
|
|
||||||
|
self.beginResetModel()
|
||||||
|
self.mode = value
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
def getMode(self) -> bool:
|
||||||
|
"""
|
||||||
|
Get the display mode set.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DisplayMode
|
||||||
|
The current value.
|
||||||
|
|
||||||
|
See also
|
||||||
|
--------
|
||||||
|
setMode : Set the mode.
|
||||||
|
"""
|
||||||
|
return self.mode
|
||||||
|
|
||||||
|
def _flat_filter(self, item: FileSystemItem[T]) -> bool:
|
||||||
|
"""
|
||||||
|
Return whether an item is part of the flat model representation.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _simplify_filter(self, item: FileSystemItem[T]) -> bool:
|
||||||
|
"""
|
||||||
|
Return whether an item may be merged in simplified mode.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getItem(
|
||||||
|
self, path: Union[PurePath,
|
||||||
|
PathLike]) -> Optional[FileSystemItem[T]]:
|
||||||
|
"""
|
||||||
|
Get the item with the given path.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path : PathLike or PurePath
|
||||||
|
The path of the item.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Optional[FileSystemItem]
|
||||||
|
[description]
|
||||||
|
"""
|
||||||
|
if isinstance(path, PurePath):
|
||||||
|
path = path.parts
|
||||||
|
|
||||||
|
return self.root.get_path(path)
|
||||||
|
|
||||||
|
def data(self,
|
||||||
|
index: QModelIndex,
|
||||||
|
role: int = Qt.ItemDataRole.DisplayRole):
|
||||||
|
"""
|
||||||
|
Get the data for the given role and index.
|
||||||
|
|
||||||
|
The indexes internal pointer references the corresponding
|
||||||
|
`FileSystemItem`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
index : QModelIndex
|
||||||
|
The index of the item.
|
||||||
|
role : int, optional
|
||||||
|
The data role, by default Qt.ItemDataRole.DisplayRole
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Any
|
||||||
|
The data, return None if no data is available for the role.
|
||||||
|
"""
|
||||||
|
return super().data(index, role)
|
||||||
|
|
||||||
|
def rowCount(self, parent: 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.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
parent : QModelIndex, optional
|
||||||
|
The index of the parent item, by default QModelIndex()
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
The number of children.
|
||||||
|
"""
|
||||||
|
if parent.column() > 0:
|
||||||
|
return 0 # Only the first column has children
|
||||||
|
|
||||||
|
# flat mode
|
||||||
|
if self.mode == self.DisplayMode.FLAT:
|
||||||
|
if not parent.isValid():
|
||||||
|
return len(self._flattened)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# tree mode
|
||||||
|
if not parent.isValid():
|
||||||
|
parent_item: FileSystemItem = self.root
|
||||||
|
else:
|
||||||
|
parent_item = parent.internalPointer()
|
||||||
|
|
||||||
|
return len(parent_item.children)
|
||||||
|
|
||||||
|
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||||
|
"""
|
||||||
|
Returns the number of columns for the children of the given parent.
|
||||||
|
|
||||||
|
This corresponds to the number of data (column) entries shown
|
||||||
|
for each item in the tree view.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
parent : QModelIndex, optional
|
||||||
|
The index of the parent, by default QModelIndex()
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
The number of rows.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("Method `columnCount` of FileTreeModel" +
|
||||||
|
" must be implemented by subclasses.")
|
||||||
|
|
||||||
|
def indexPath(self, path: Union[PurePath, PathLike]) -> QModelIndex:
|
||||||
|
"""
|
||||||
|
Construct a `QModelIndex` for the given path.
|
||||||
|
|
||||||
|
If `combine` is enabled, the closest indexed parent path is returned.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path : PurePath or PathLike
|
||||||
|
The path to the item the index should point to.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
QModelIndex
|
||||||
|
The requested index.
|
||||||
|
"""
|
||||||
|
if isinstance(path, PurePath):
|
||||||
|
path = path.parts
|
||||||
|
|
||||||
|
# flat mode
|
||||||
|
if self.mode == self.DisplayMode.FLAT:
|
||||||
|
i = bisect.bisect_left(self._flattened, path)
|
||||||
|
if i < len(self._flattened) and self._flattened[i].path == path:
|
||||||
|
return self.index(i, 0)
|
||||||
|
return QModelIndex()
|
||||||
|
|
||||||
|
# tree mode
|
||||||
|
simplified = self.mode == self.DisplayMode.SIMPLIFIED_TREE
|
||||||
|
|
||||||
|
def step(tup, subpath):
|
||||||
|
index, i, item = tup
|
||||||
|
|
||||||
|
if not item:
|
||||||
|
return index, None
|
||||||
|
|
||||||
|
r, child = item.get(subpath)
|
||||||
|
|
||||||
|
if not child:
|
||||||
|
return QModelIndex(), None
|
||||||
|
|
||||||
|
if i <= -1:
|
||||||
|
i = r
|
||||||
|
|
||||||
|
if (simplified and len(child.children) == 1
|
||||||
|
and self._simplify_filter(child)):
|
||||||
|
return index, i, child
|
||||||
|
|
||||||
|
index = self.index(i if simplified else r, 0, index)
|
||||||
|
|
||||||
|
return index, -1, child
|
||||||
|
|
||||||
|
index, i, item = reduce(step, path, (QModelIndex(), -1, self.root))
|
||||||
|
|
||||||
|
return index
|
||||||
|
|
||||||
|
def index(self, row: int, column: int,
|
||||||
|
parent: QModelIndex = QModelIndex()) -> QModelIndex:
|
||||||
|
"""
|
||||||
|
Construct a `QModelIndex`.
|
||||||
|
|
||||||
|
Returns the index of the item in the model specified by
|
||||||
|
the given row, column and parent index.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
row : int
|
||||||
|
column : int
|
||||||
|
parent : QModelIndex, optional
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
QModelIndex
|
||||||
|
The requested index.
|
||||||
|
"""
|
||||||
|
# different behavior in flat and treemode
|
||||||
|
if self.mode == self.DisplayMode.FLAT:
|
||||||
|
if (0 <= row < len(self._flattened)
|
||||||
|
and 0 <= column < self.columnCount(parent)):
|
||||||
|
return self.createIndex(row, column, self._flattened[row])
|
||||||
|
|
||||||
|
return QModelIndex()
|
||||||
|
|
||||||
|
# valid index?
|
||||||
|
if not parent.isValid():
|
||||||
|
parent_item: FileSystemItem[T] = self.root
|
||||||
|
else:
|
||||||
|
parent_item = parent.internalPointer()
|
||||||
|
|
||||||
|
item = list(parent_item.children)[row]
|
||||||
|
|
||||||
|
if self.mode == self.DisplayMode.SIMPLIFIED_TREE:
|
||||||
|
# combine items with a single child with that child
|
||||||
|
while len(item.children) == 1 and self._simplify_filter(item):
|
||||||
|
item = item.children[0]
|
||||||
|
|
||||||
|
if (0 <= row < len(parent_item.children)
|
||||||
|
and 0 <= column < self.columnCount(parent)):
|
||||||
|
return self.createIndex(row, column, item)
|
||||||
|
|
||||||
|
return QModelIndex()
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def parent(self, child: QModelIndex) -> QModelIndex:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def parent(self) -> QObject:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def parent(self, child=None):
|
||||||
|
"""
|
||||||
|
Returns the parent of the model item with the given index.
|
||||||
|
|
||||||
|
If the item has no parent, an invalid QModelIndex is returned.
|
||||||
|
A common convention used in models that expose tree data structures
|
||||||
|
is that only items in the first column have children.
|
||||||
|
For that case, when reimplementing this function in a subclass
|
||||||
|
the column of the returned QModelIndex would be 0.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
child : QModelIndex
|
||||||
|
The index of the child item.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
QModelIndex
|
||||||
|
The index of the parent item.
|
||||||
|
"""
|
||||||
|
# overloaded variant to retrieve parent of model
|
||||||
|
if child is None:
|
||||||
|
return super().parent()
|
||||||
|
|
||||||
|
# variant to retrieve parent for data item
|
||||||
|
if not child.isValid():
|
||||||
|
return QModelIndex()
|
||||||
|
|
||||||
|
# different behavior in tree and flat mode
|
||||||
|
if self.mode == self.DisplayMode.FLAT:
|
||||||
|
return QModelIndex() # in flat mode their are no parents
|
||||||
|
|
||||||
|
child_item: FileSystemItem[T] = child.internalPointer()
|
||||||
|
parent_item = child_item._parent
|
||||||
|
|
||||||
|
if self.mode == self.DisplayMode.SIMPLIFIED_TREE:
|
||||||
|
# combine items with a single child with the child
|
||||||
|
while (parent_item is not self.root # do not call filter with root
|
||||||
|
and len(parent_item.children) == 1
|
||||||
|
and self._simplify_filter(parent_item)):
|
||||||
|
parent_item = parent_item._parent
|
||||||
|
|
||||||
|
if parent_item is self.root:
|
||||||
|
# Never return root item since it shouldn't be displayed
|
||||||
|
return QModelIndex()
|
||||||
|
|
||||||
|
row, item = parent_item._parent.get(parent_item.subpath)
|
||||||
|
return self.createIndex(row, 0, parent_item)
|
||||||
|
|
||||||
|
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
|
||||||
|
"""
|
||||||
|
Returns the item flags for the given index.
|
||||||
|
|
||||||
|
The base class implementation returns a combination of flags
|
||||||
|
that enables the item (ItemIsEnabled) and
|
||||||
|
allows it to be selected (ItemIsSelectable).
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
index : QModelIndex
|
||||||
|
The index.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Qt.ItemFlags
|
||||||
|
The flags.
|
||||||
|
"""
|
||||||
|
return super().flags(index)
|
||||||
|
|
||||||
|
def headerData(self,
|
||||||
|
section: int,
|
||||||
|
orientation: Qt.Orientation,
|
||||||
|
role: int = Qt.ItemDataRole.DisplayRole):
|
||||||
|
"""
|
||||||
|
Get the data for the given role and section in the given header.
|
||||||
|
|
||||||
|
The header is identified by its orientation.
|
||||||
|
For horizontal headers, the section number corresponds to
|
||||||
|
the column number. Similarly, for vertical headers,
|
||||||
|
the section number corresponds to the row number.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
section : int
|
||||||
|
The row or column number.
|
||||||
|
orientation : Qt.Orientation
|
||||||
|
The orientation of the header.
|
||||||
|
role : int, optional
|
||||||
|
The data role, by default Qt.ItemDataRole.DisplayRole
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Any
|
||||||
|
The data for the specified header section.
|
||||||
|
"""
|
||||||
|
return super().headerData(section, orientation, role)
|
326
tests/test_treemodel.py
Normal file
326
tests/test_treemodel.py
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
from pathlib import PurePath
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from PyQt5.QtCore import QModelIndex
|
||||||
|
|
||||||
|
from vorta.views.partials.treemodel import FileSystemItem, FileTreeModel
|
||||||
|
|
||||||
|
|
||||||
|
class TreeModelImp(FileTreeModel):
|
||||||
|
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def _make_filesystemitem(self, path, data):
|
||||||
|
return super()._make_filesystemitem(path, data)
|
||||||
|
|
||||||
|
def _merge_data(self, item, data):
|
||||||
|
return super()._merge_data(item, data)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFileSystemItem:
|
||||||
|
def test_tuple(self):
|
||||||
|
item = FileSystemItem(PurePath('test').parts, 0)
|
||||||
|
|
||||||
|
assert item[0] == item.path
|
||||||
|
assert item[1] == item.data
|
||||||
|
|
||||||
|
def test_add(self):
|
||||||
|
item = FileSystemItem(PurePath('test').parts, 0)
|
||||||
|
|
||||||
|
assert len(item.children) == 0
|
||||||
|
|
||||||
|
child = FileSystemItem(PurePath('test/hello').parts, 4)
|
||||||
|
|
||||||
|
item.add(child)
|
||||||
|
|
||||||
|
assert len(item.children) == 1
|
||||||
|
assert item.children[0] == child
|
||||||
|
assert child.subpath == 'hello'
|
||||||
|
assert child._parent == item
|
||||||
|
|
||||||
|
child = FileSystemItem(PurePath('test/hello').parts, 8)
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
# may not add a child with the same subpath
|
||||||
|
item.add(child)
|
||||||
|
|
||||||
|
def test_remove(self):
|
||||||
|
item = FileSystemItem(PurePath('test').parts, 0)
|
||||||
|
child1 = FileSystemItem(PurePath('test/a').parts, 4)
|
||||||
|
child2 = FileSystemItem(PurePath('test/b').parts, 3)
|
||||||
|
child3 = FileSystemItem(PurePath('test/c').parts, 2)
|
||||||
|
|
||||||
|
item.add(child1)
|
||||||
|
item.add(child2)
|
||||||
|
item.add(child3)
|
||||||
|
|
||||||
|
assert len(item.children) == 3
|
||||||
|
|
||||||
|
# test remove subpath
|
||||||
|
item.remove('b')
|
||||||
|
assert len(item.children) == 2
|
||||||
|
assert child2 not in item.children
|
||||||
|
|
||||||
|
# test remove item
|
||||||
|
item.remove(child3)
|
||||||
|
assert len(item.children) == 1
|
||||||
|
assert child3 not in item.children
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
item = FileSystemItem(PurePath('test').parts, 0)
|
||||||
|
child1 = FileSystemItem(PurePath('test/a').parts, 4)
|
||||||
|
child2 = FileSystemItem(PurePath('test/b').parts, 3)
|
||||||
|
child3 = FileSystemItem(PurePath('test/c').parts, 2)
|
||||||
|
|
||||||
|
item.add(child1)
|
||||||
|
item.add(child2)
|
||||||
|
item.add(child3)
|
||||||
|
|
||||||
|
# test get inexistent subpath
|
||||||
|
assert item.get('unknown') is None
|
||||||
|
assert item.get('unknown', default='default') == 'default'
|
||||||
|
|
||||||
|
# get subpath
|
||||||
|
res = item.get('a')
|
||||||
|
assert res is not None
|
||||||
|
assert res[1] == child1
|
||||||
|
|
||||||
|
res = item.get('b')
|
||||||
|
assert res is not None
|
||||||
|
assert res[1] == child2
|
||||||
|
|
||||||
|
# get subpath of empty list
|
||||||
|
assert child1.get('a') is None
|
||||||
|
|
||||||
|
def test_get_subpath(self):
|
||||||
|
item = FileSystemItem(('test',), 0)
|
||||||
|
child1 = FileSystemItem(PurePath('test/a').parts, 4)
|
||||||
|
child2 = FileSystemItem(PurePath('test/b').parts, 3)
|
||||||
|
child3 = FileSystemItem(PurePath('test/c').parts, 2)
|
||||||
|
|
||||||
|
item.add(child1)
|
||||||
|
item.add(child2)
|
||||||
|
item.add(child3)
|
||||||
|
|
||||||
|
child11 = FileSystemItem(PurePath('test/a/aa').parts, 4)
|
||||||
|
child1.add(child11)
|
||||||
|
|
||||||
|
assert item.get_path(PurePath('a/aa').parts) is child11
|
||||||
|
assert item.get_path(('b',)) is child2
|
||||||
|
|
||||||
|
|
||||||
|
class TestFileTreeModel:
|
||||||
|
def test_basic_setup(self):
|
||||||
|
model = TreeModelImp()
|
||||||
|
|
||||||
|
assert model.rowCount() == 0
|
||||||
|
|
||||||
|
# test FileTreeModel.addItem
|
||||||
|
model.addItem((('test',), 0))
|
||||||
|
assert model.rowCount() == 1
|
||||||
|
|
||||||
|
item = model.getItem(('test',))
|
||||||
|
assert item is not None
|
||||||
|
assert item.subpath == 'test'
|
||||||
|
assert len(item.children) == 0
|
||||||
|
assert item.data == 0 # test id
|
||||||
|
|
||||||
|
model.addItem((PurePath('/hello'), 1))
|
||||||
|
model.addItem(FileSystemItem(PurePath('/hello/test').parts, 2))
|
||||||
|
|
||||||
|
assert model.rowCount() == 2
|
||||||
|
|
||||||
|
item = model.getItem(('/',))
|
||||||
|
assert item is not None
|
||||||
|
assert item.subpath == '/'
|
||||||
|
assert len(item.children) == 1
|
||||||
|
|
||||||
|
item = model.getItem(PurePath('/hello/test').parts)
|
||||||
|
assert item is not None and item.data == 2
|
||||||
|
|
||||||
|
# test adding Item to existing parent
|
||||||
|
model.addItem((PurePath('test/subtest'), 3))
|
||||||
|
assert model.rowCount() == 2
|
||||||
|
item = model.getItem(PurePath('test/subtest').parts)
|
||||||
|
assert item is not None and item.data == 3
|
||||||
|
|
||||||
|
# test parent
|
||||||
|
assert (model.parent(model.indexPath(
|
||||||
|
PurePath('test/subtest'))) == model.indexPath(PurePath('test')))
|
||||||
|
|
||||||
|
# test index
|
||||||
|
item1 = model.getItem(('test',))
|
||||||
|
item2 = model.getItem(PurePath('test/subtest'))
|
||||||
|
|
||||||
|
index1 = model.index(1, 0)
|
||||||
|
assert index1.internalPointer() == item1
|
||||||
|
assert index1 == model.indexPath(PurePath('test'))
|
||||||
|
index2 = model.index(0, 0, index1)
|
||||||
|
assert index2.internalPointer() == item2
|
||||||
|
assert index2 == model.indexPath(PurePath('test/subtest'))
|
||||||
|
|
||||||
|
# test rowCount
|
||||||
|
assert model.rowCount() == 2
|
||||||
|
assert model.rowCount(index1) == 1
|
||||||
|
|
||||||
|
# test remove
|
||||||
|
model.removeItem(('test',))
|
||||||
|
assert model.rowCount() == 1
|
||||||
|
assert item1 not in model.root.children
|
||||||
|
|
||||||
|
model.removeItem(PurePath('/hello/test').parts)
|
||||||
|
assert model.rowCount() == 1
|
||||||
|
assert model.getItem(PurePath('/hello/test')) is None
|
||||||
|
item3 = model.getItem(PurePath('/hello'))
|
||||||
|
assert len(item3.children) == 0
|
||||||
|
|
||||||
|
def test_root(self):
|
||||||
|
model = TreeModelImp()
|
||||||
|
assert model.getItem(PurePath()) == model.root
|
||||||
|
|
||||||
|
def test_flat(self):
|
||||||
|
items = [
|
||||||
|
(PurePath('a'), 0),
|
||||||
|
(PurePath('a/a'), 1),
|
||||||
|
(PurePath('a/c'), 3),
|
||||||
|
(PurePath('a/b/a'), 4),
|
||||||
|
(PurePath('a/b/b'), 5),
|
||||||
|
(PurePath('a/b'), 2),
|
||||||
|
(PurePath('b'), 6),
|
||||||
|
(PurePath('b/a'), 7),
|
||||||
|
(PurePath('b/b'), 8),
|
||||||
|
]
|
||||||
|
|
||||||
|
model = TreeModelImp()
|
||||||
|
model.addItems(items)
|
||||||
|
|
||||||
|
# test flat representation
|
||||||
|
model.setMode(model.DisplayMode.FLAT)
|
||||||
|
|
||||||
|
assert model.rowCount() == len(items)
|
||||||
|
assert model.parent(model.index(4, 0)) == QModelIndex()
|
||||||
|
assert model.rowCount(model.index(3, 0)) == 0
|
||||||
|
|
||||||
|
item = model.getItem(PurePath('a/b/a'))
|
||||||
|
assert item is not None and item.data == 4
|
||||||
|
|
||||||
|
# test flat add
|
||||||
|
model.addItem((PurePath('a/a/a'), 10))
|
||||||
|
|
||||||
|
assert model.rowCount() == len(items) + 1
|
||||||
|
item = model.getItem(PurePath('a/a/a'))
|
||||||
|
assert item is not None and item.data == 10
|
||||||
|
|
||||||
|
# test flat remove
|
||||||
|
model.removeItem(PurePath('a/a/a'))
|
||||||
|
|
||||||
|
assert model.rowCount() == len(items)
|
||||||
|
assert item not in model._flattened
|
||||||
|
|
||||||
|
# test flat indexPath
|
||||||
|
index = model.indexPath(PurePath('a/b'))
|
||||||
|
assert index.internalPointer().data == 2
|
||||||
|
assert model._flattened[index.row()].data == 2
|
||||||
|
assert index.parent() == QModelIndex()
|
||||||
|
|
||||||
|
# test
|
||||||
|
model.setMode(model.DisplayMode.TREE)
|
||||||
|
|
||||||
|
assert model.rowCount() == 2
|
||||||
|
|
||||||
|
def test_simplified_tree(self):
|
||||||
|
items = [
|
||||||
|
(PurePath('a'), 0),
|
||||||
|
(PurePath('a/a'), 1),
|
||||||
|
(PurePath('a/c'), 3),
|
||||||
|
(PurePath('a/b/a'), 4),
|
||||||
|
(PurePath('a/b/b'), 5),
|
||||||
|
(PurePath('a/b'), 2),
|
||||||
|
(PurePath('b'), 6),
|
||||||
|
(PurePath('b/a'), 7),
|
||||||
|
(PurePath('b/b'), 8),
|
||||||
|
(PurePath('c'), 9),
|
||||||
|
(PurePath('c/a'), 10),
|
||||||
|
(PurePath('c/a/b'), 11),
|
||||||
|
(PurePath('c/a/b/c'), 12),
|
||||||
|
(PurePath('c/a/b/a'), 13),
|
||||||
|
(PurePath('c/a/b/a/b/c'), 14),
|
||||||
|
(PurePath('c/a/b/b/c/a'), 15),
|
||||||
|
]
|
||||||
|
|
||||||
|
model = TreeModelImp()
|
||||||
|
model.addItems(items)
|
||||||
|
|
||||||
|
# test tree representation
|
||||||
|
model.setMode(model.DisplayMode.SIMPLIFIED_TREE)
|
||||||
|
|
||||||
|
assert model.rowCount() == 3
|
||||||
|
|
||||||
|
a = model.index(0, 0)
|
||||||
|
assert model.rowCount(a) == 3
|
||||||
|
ab = model.index(1, 0, a)
|
||||||
|
assert model.rowCount(ab) == 2
|
||||||
|
assert model.parent(ab) == a
|
||||||
|
|
||||||
|
# test combined representation
|
||||||
|
|
||||||
|
cab = model.index(2, 0)
|
||||||
|
assert model.rowCount(cab) == 3
|
||||||
|
assert cab.internalPointer().data == 11
|
||||||
|
assert model.rowCount(cab) == 3
|
||||||
|
assert model.parent(cab) == QModelIndex()
|
||||||
|
cabc = model.index(2, 0, cab)
|
||||||
|
cababc = model.index(0, 0, cab)
|
||||||
|
cabbca = model.index(1, 0, cab)
|
||||||
|
assert cababc.internalPointer().data == 14
|
||||||
|
assert model.parent(cababc).internalId() == cab.internalId()
|
||||||
|
assert cabbca.internalPointer().data == 15
|
||||||
|
assert model.parent(cabbca).internalId() == cab.internalId()
|
||||||
|
assert cabc.internalPointer().data == 12
|
||||||
|
|
||||||
|
# test combined add
|
||||||
|
model.addItem((PurePath('a/a/a'), 30))
|
||||||
|
|
||||||
|
aaa = model.index(0, 0, a)
|
||||||
|
assert aaa.internalPointer().data == 30
|
||||||
|
assert model.rowCount(aaa) == 0
|
||||||
|
|
||||||
|
model.addItem((PurePath('c/a/a'), 31))
|
||||||
|
|
||||||
|
ca = model.index(2, 0)
|
||||||
|
assert ca.internalPointer().data == 10
|
||||||
|
assert ca.parent() == QModelIndex()
|
||||||
|
assert model.rowCount(ca) == 2
|
||||||
|
caa = model.index(0, 0, ca)
|
||||||
|
assert caa.internalPointer().data == 31
|
||||||
|
assert caa.parent().internalId() == ca.internalId()
|
||||||
|
|
||||||
|
# test combined remove
|
||||||
|
model.removeItem(PurePath('a/a/a').parts)
|
||||||
|
|
||||||
|
aa = model.index(0, 0, a)
|
||||||
|
assert aa.internalPointer().data == 1
|
||||||
|
assert model.rowCount(aa) == 0
|
||||||
|
|
||||||
|
model.removeItem(PurePath('c/a/a'))
|
||||||
|
|
||||||
|
cab = model.index(2, 0)
|
||||||
|
assert cab.internalPointer().data == 11
|
||||||
|
assert model.rowCount(cab) == 3
|
||||||
|
|
||||||
|
# test combined indexPath
|
||||||
|
index = model.indexPath(PurePath('a/b'))
|
||||||
|
assert index.internalPointer().data == 2
|
||||||
|
assert index.parent() == a
|
||||||
|
|
||||||
|
index = model.indexPath(PurePath('c/a/b'))
|
||||||
|
assert model.parent(index) == QModelIndex()
|
||||||
|
assert index.internalPointer().data == 11
|
||||||
|
assert model.rowCount(index) == 3
|
||||||
|
|
||||||
|
index = model.indexPath(PurePath('c/a'))
|
||||||
|
assert index == QModelIndex()
|
||||||
|
|
||||||
|
# test mode change
|
||||||
|
model.setMode(model.DisplayMode.TREE)
|
||||||
|
|
||||||
|
assert model.rowCount() == 3
|
Loading…
Add table
Reference in a new issue