######################################################################################################################
# Copyright (C) 2017-2022 Spine project consortium
# Copyright Spine Toolbox contributors
# This file is part of Spine Items.
# Spine Items is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################
"""Contains a generic File list model and an Item for that model."""
from collections import namedtuple
from itertools import combinations, takewhile
import json
from pathlib import Path
from PySide6.QtCore import QAbstractItemModel, QFileInfo, QModelIndex, Qt, Signal, QMimeData
from PySide6.QtWidgets import QFileIconProvider
from PySide6.QtGui import QStandardItemModel, QStandardItem, QPixmap, QPainter, QIcon, QColor
from spine_engine.project_item.project_item_resource import extract_packs, CmdLineArg, LabelArg
from spinetoolbox.helpers import plain_to_rich
[docs]class FileListModel(QAbstractItemModel):
"""A model for files to be shown in a file tree view."""
[docs] FileItem = namedtuple("FileItem", ["resource"])
[docs] PackItem = namedtuple("PackItem", ["label", "resources"])
def __init__(self, header_label="", draggable=False):
"""
Args:
header_label (str): header label
draggable (bool): if True, the top level items are drag and droppable
"""
super().__init__()
self._header_label = header_label
self._draggable = draggable
self._single_resources = list()
self._pack_resources = list()
[docs] def rowCount(self, parent=QModelIndex()):
if not parent.isValid():
return len(self._single_resources) + len(self._pack_resources)
parent_row = parent.row()
if parent_row < len(self._single_resources):
return 0
return len(self._pack_resources[parent_row - len(self._single_resources)].resources)
[docs] def columnCount(self, parent=QModelIndex()):
return 1
[docs] def data(self, index, role=Qt.ItemDataRole.DisplayRole):
"""Returns data associated with given role at given index."""
if not index.isValid():
return None
if role == Qt.ItemDataRole.DisplayRole:
row = index.row()
pack_label = index.internalPointer()
if pack_label is None:
if row < len(self._single_resources):
resource = self._single_resources[row].resource
return resource.label
return self._pack_resources[row - len(self._single_resources)].label
return self._pack_resources[self._pack_index(pack_label)].resources[row].path
if role == Qt.ItemDataRole.DecorationRole:
row = index.row()
pack_label = index.internalPointer()
if pack_label is None:
if row < len(self._single_resources):
resource = self._single_resources[row].resource
else:
return None
else:
resource = self._pack_resources[self._pack_index(pack_label)].resources[row]
if resource.hasfilepath:
return QFileIconProvider().icon(QFileInfo(resource.path))
if role == Qt.ItemDataRole.ToolTipRole:
row = index.row()
pack_label = index.internalPointer()
if pack_label is None:
if row < len(self._single_resources):
resource = self._single_resources[row].resource
else:
return None
else:
resource = self._pack_resources[self._pack_index(pack_label)].resources[row]
if resource.type_ == "database":
return resource.url
return (
resource.path
if resource.hasfilepath
else plain_to_rich(f"This file will be generated by {resource.provider_name} upon execution.")
)
return None
[docs] def flags(self, index):
if index.internalPointer() is None:
if index.row() < len(self._single_resources):
flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemNeverHasChildren
else:
flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled
if self._draggable:
flags = flags | Qt.ItemIsDragEnabled
return flags
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemNeverHasChildren
[docs] def mimeData(self, indexes):
data = QMimeData()
text = json.dumps(("labels", ";;".join([index.data() for index in indexes])))
data.setText(text)
return data
[docs] def resource(self, index):
"""Returns the resource at given index.
Args:
index (QModelIndex): index
Returns:
ProjectItemResource: resource
"""
pack_label = index.internalPointer()
if pack_label is None:
row = index.row()
if row < len(self._single_resources):
return self._single_resources[row].resource
pack_resources = self._pack_resources[row - len(self._single_resources)].resources
return pack_resources[0] if pack_resources else None
return self._pack_resources[self._pack_index(pack_label)].resources[index.row()]
[docs] def parent(self, index):
pack_label = index.internalPointer()
if pack_label is None:
return QModelIndex()
return self.createIndex(len(self._single_resources) + self._pack_index(pack_label), 0)
[docs] def index(self, row, column, parent=QModelIndex()):
if not parent.isValid():
return self.createIndex(row, column, None)
parent_row = parent.row()
if parent_row < len(self._single_resources):
return QModelIndex()
pack_label = self._pack_resources[parent_row - len(self._single_resources)].label
return self.createIndex(row, column, pack_label)
[docs] def update(self, resources):
"""Updates the model according to given list of resources.
Args:
resources (Iterable of ProjectItemResource): resources
"""
self.beginResetModel()
single_resources, pack_resources = extract_packs(resources)
new_singles = [self.FileItem(r) for r in single_resources]
new_packs = [
self.PackItem(label, [r for r in r_list if r.hasfilepath]) for label, r_list in pack_resources.items()
]
self._single_resources = new_singles
self._pack_resources = new_packs
self.endResetModel()
[docs] def duplicate_paths(self):
"""Checks if resources in the model have duplicate file paths.
Returns:
set of str: set of duplicate file paths
"""
single_paths = [Path(item.resource.path) for item in self._single_resources if item.resource.hasfilepath]
pack_paths = [Path(r.path) for item in self._pack_resources for r in item.resources if r.hasfilepath]
paths = single_paths + pack_paths
duplicates = set()
seen = set()
for path in paths:
if str(path) in seen:
duplicates.add(str(path))
else:
seen.add(str(path))
return duplicates
[docs] def _pack_index(self, pack_label):
"""Finds a pack's index in pack resources list.
Args:
pack_label (str): pack label
Returns:
int: index to pack resources list
"""
return len(list(takewhile(lambda item: item.label != pack_label, self._pack_resources)))
[docs]class CommandLineArgItem(QStandardItem):
def __init__(self, text="", rank=None, selectable=False, editable=False, drag_enabled=False, drop_enabled=False):
super().__init__(text)
self.setEditable(editable)
self.setDropEnabled(drop_enabled)
self.setDragEnabled(drag_enabled)
self.setSelectable(selectable)
self.set_rank(rank)
[docs] def set_rank(self, rank):
if rank is not None:
icon = self._make_icon(rank)
self.setIcon(icon)
@staticmethod
[docs] def _make_icon(rank=None):
pixmap = QPixmap(16, 16)
pixmap.fill(Qt.white)
painter = QPainter(pixmap)
painter.drawText(0, 0, 16, 16, Qt.AlignCenter, f"{rank}:")
painter.end()
return QIcon(pixmap)
[docs] def setData(self, value, role=Qt.ItemDataRole.UserRole + 1):
if role != Qt.ItemDataRole.EditRole:
return super().setData(value, role=role)
if value != self.data(role=role):
self.model().replace_arg(self.row(), CmdLineArg(value))
return False
[docs]class NewCommandLineArgItem(CommandLineArgItem):
def __init__(self):
super().__init__("Type arg, or drag and drop from Available resources...", selectable=True, editable=True)
gray_color = qApp.palette().text().color() # pylint: disable=undefined-variable
gray_color.setAlpha(128)
self.setForeground(gray_color)
[docs] def setData(self, value, role=Qt.ItemDataRole.UserRole + 1):
if role != Qt.ItemDataRole.EditRole:
return super().setData(value, role=role)
if value != self.data(role=role):
self.model().append_arg(CmdLineArg(value))
return False
[docs]class CommandLineArgsModel(QStandardItemModel):
[docs] args_updated = Signal(list)
def __init__(self, parent=None):
super().__init__(parent)
self.setHorizontalHeaderItem(0, QStandardItem("Command line arguments"))
self._args = []
@property
[docs] def args(self):
return self._args
[docs] def append_arg(self, arg):
self.args_updated.emit(self._args + [arg])
[docs] def replace_arg(self, row, arg):
new_args = self._args.copy()
new_args[row] = arg
self.args_updated.emit(new_args)
[docs] def mimeData(self, indexes):
data = QMimeData()
text = json.dumps(("rows", ";;".join([str(index.row()) for index in indexes])))
data.setText(text)
return data
[docs] def dropMimeData(self, data, drop_action, row, column, parent):
head, contents = json.loads(data.text())
if head == "rows":
rows = [int(x) for x in contents.split(";;")]
head = [arg for k, arg in enumerate(self._args[:row]) if k not in rows]
body = [self._args[k] for k in rows]
tail = [arg for k, arg in enumerate(self._args[row:]) if k + row not in rows]
new_args = head + body + tail
self.args_updated.emit(new_args)
return True
if head == "labels":
new_args = self._args[:row] + [LabelArg(arg) for arg in contents.split(";;")] + self._args[row:]
self.args_updated.emit(new_args)
return True
return False
@staticmethod
[docs] def _reset_root(root, args, child_params, has_empty_row=True):
last_row = root.rowCount()
if has_empty_row:
last_row -= 1
count = len(args) - last_row
for _ in range(count):
root.insertRow(last_row, [CommandLineArgItem(**child_params)])
if count < 0:
count = -count
first = last_row - count
root.removeRows(first, count)
for k, arg in enumerate(args):
child = root.child(k)
child.set_rank(k)
child.setText(str(arg))
color = QColor("red") if arg.missing else None
child.setData(color, role=Qt.ForegroundRole)
[docs]class JumpCommandLineArgsModel(CommandLineArgsModel):
def __init__(self, parent=None):
super().__init__(parent)
self.invisibleRootItem().appendRow(NewCommandLineArgItem())
[docs] def reset_model(self, args):
self._args = args
self._reset_root(
self.invisibleRootItem(), args, dict(editable=True, selectable=True, drag_enabled=True), has_empty_row=True
)
[docs] def canDropMimeData(self, data, drop_action, row, column, parent):
return row >= 0