manuskript/manuskript/models/worldModel.py
2021-04-02 14:15:17 +01:00

409 lines
13 KiB
Python

#!/usr/bin/env python
# --!-- coding: utf8 --!--
from PyQt5.QtCore import QModelIndex, QSize
from PyQt5.QtCore import Qt, QMimeData, QByteArray
from PyQt5.QtGui import QStandardItem, QBrush, QFontMetrics
from PyQt5.QtGui import QStandardItemModel, QColor
from PyQt5.QtWidgets import QMenu, QAction, qApp
from manuskript.enums import World, Model
from manuskript.functions import mainWindow
from manuskript.ui import style as S
from manuskript.models.searchableModel import searchableModel
from manuskript.models.searchableItem import searchableItem
from manuskript.searchLabels import WorldSearchLabels
class worldModel(QStandardItemModel, searchableModel):
def __init__(self, parent):
QStandardItemModel.__init__(self, 0, len(World), parent)
self.mw = mainWindow()
###############################################################################
# SELECTION
###############################################################################
def selectedItem(self):
"""Returns the item selected in mw.treeWorld. invisibleRootItem if None."""
index = self.selectedIndex()
item = self.itemFromIndex(index)
if item:
return item
else:
return self.invisibleRootItem()
def selectedIndex(self):
"""Returns the selected index in the treeView."""
if self.mw.treeWorld.selectedIndexes():
return self.mw.treeWorld.currentIndex()
else:
return QModelIndex()
def selectedIndexes(self):
return self.mw.treeWorld.selectedIndexes()
###############################################################################
# GETTERS
###############################################################################
def ID(self, index):
"""Returns the ID of the given index."""
index = index.sibling(index.row(), World.ID)
return self.data(index)
def name(self, index):
"""Returns the name of the given index."""
index = index.sibling(index.row(), World.name)
return self.data(index)
def description(self, index):
index = index.sibling(index.row(), World.description)
return self.data(index)
def conflict(self, index):
index = index.sibling(index.row(), World.conflict)
return self.data(index)
def passion(self, index):
index = index.sibling(index.row(), World.passion)
return self.data(index)
def itemID(self, item):
"""Returns the ID of the given item."""
index = self.indexFromItem(item)
return self.ID(index)
def children(self, item):
"""Returns a list of all item's children."""
c = []
for i in range(item.rowCount()):
c.append(item.child(i))
return c
def listAll(self):
"""Returns a list of tuple ``(name, ID, path)`` for all items."""
lst = []
def readAll(item):
name = item.text()
ID = self.itemID(item)
path = self.path(item)
if name and ID:
lst.append((name, ID, path))
for c in self.children(item):
readAll(c)
readAll(self.invisibleRootItem())
return lst
def indexByID(self, ID):
"""Returns the index of item whose ID is ID."""
return self.indexFromItem(self.itemByID(ID))
def itemByID(self, ID):
"""Returns the item whose ID is ID."""
def browse(item):
if self.itemID(item) == ID:
return item
for c in self.children(item):
r = browse(c)
if r:
return r
r = browse(self.invisibleRootItem())
return r if r else None
def path(self, item):
"""Returns the path to the item in the form of 'ancestor > ... > grand-parent > parent'."""
path = []
while item.parent():
item = item.parent()
path.append(item.text())
path = " > ".join(path)
return path
###############################################################################
# ADDING AND REMOVE
###############################################################################
def addItem(self, title=None, parent=None):
"""Adds an item, and returns it."""
if not parent:
parent = self.selectedItem()
if not title:
title = self.tr("New item")
name = QStandardItem(title)
_id = QStandardItem(self.getUniqueID())
row = [name, _id] + [QStandardItem() for i in range(2, len(World))]
parent.appendRow(row)
self.mw.treeWorld.setExpanded(self.selectedIndex(), True)
self.mw.treeWorld.setCurrentIndex(self.indexFromItem(name))
return name
def getUniqueID(self):
"""Returns an unused ID"""
parentItem = self.invisibleRootItem()
vals = []
def collectIDs(item):
vals.append(int(self.itemID(item)))
for c in self.children(item):
collectIDs(c)
for c in self.children(parentItem):
collectIDs(c)
k = 0
while k in vals:
k += 1
return str(k)
def removeItem(self):
while self.selectedIndexes():
index = self.selectedIndexes()[0]
self.removeRows(index.row(), 1, index.parent())
###############################################################################
# DRAG & DROP
###############################################################################
"""Mime type for worldModel"""
MIME_TYPE = "application/x.manuskript.worldmodel"
def mimeTypes(self):
"""Returns available MIME types
Returns only worldModel MIME type to allow only internal drag & drop"""
return [self.MIME_TYPE]
def mimeData(self, indexes):
"""Returns dragged data as MIME data"""
mime_data = QMimeData()
"""set MIME type"""
mime_data.setData(self.MIME_TYPE, QByteArray())
"""row index is just a pair of item parent and row number"""
row_indexes = []
for index in indexes:
item = self.itemFromIndex(index)
parent = item.parent()
if parent == None:
parent = self.invisibleRootItem()
row_indexes.append((parent, item.row()))
def copyRowWithChildren(row_index):
"""copy row and its children except for those that are in
row_indexes to avoid duplicates"""
parent, row_i = row_index
row = []
for column_i in range(parent.columnCount()):
original = parent.child(row_i, column_i)
copy = original.clone()
for child_row_i in range(original.rowCount()):
child_row_index = (original, child_row_i)
if child_row_index not in row_indexes:
child_row = copyRowWithChildren(child_row_index)
copy.appendRow(child_row)
row.append(copy)
return row
rows = []
for i in row_indexes:
"""copy not move, because these rows will be deleted automatically
after dropMimeData"""
rows.append(copyRowWithChildren(i))
"""mime_data.rows available only in the application"""
mime_data.rows = rows
return mime_data
def dropMimeData(self, mime_data, action, row_i, column_i, parent):
"""insert MIME data"""
parent_item = self.itemFromIndex(parent)
if not parent_item:
parent_item = self.invisibleRootItem()
"""if place for drop is not specified row_i equals -1"""
if row_i == -1:
for row in mime_data.rows:
parent_item.appendRow(row)
else:
"""reverse list of rows, because QStandardItem::insertRow inserts
before the index"""
for row in reversed(mime_data.rows):
parent_item.insertRow(row_i, row)
return True
###############################################################################
# TEMPLATES
###############################################################################
def dataSets(self):
"""Returns sets of empty data that can guide the writer for world building."""
dataset = {
self.tr("Fantasy world building"): [
(self.tr("Physical"), [
self.tr("Climate"),
self.tr("Topography"),
self.tr("Astronomy"),
self.tr("Natural resources"),
self.tr("Wild life"),
self.tr("Flora"),
self.tr("History"),
self.tr("Races"),
self.tr("Diseases"),
]),
(self.tr("Cultural"), [
self.tr("Customs"),
self.tr("Food"),
self.tr("Languages"),
self.tr("Education"),
self.tr("Dresses"),
self.tr("Science"),
self.tr("Calendar"),
self.tr("Bodily language"),
self.tr("Ethics"),
self.tr("Religion"),
self.tr("Government"),
self.tr("Politics"),
self.tr("Gender roles"),
self.tr("Music and arts"),
self.tr("Architecture"),
self.tr("Military"),
self.tr("Technology"),
self.tr("Courtship"),
self.tr("Demography"),
self.tr("Transportation"),
self.tr("Medicine"),
]),
(self.tr("Magic system"), [
self.tr("Rules"),
self.tr("Organization"),
self.tr("Magical objects"),
self.tr("Magical places"),
self.tr("Magical races"),
]),
self.tr("Important places"),
self.tr("Important objects"),
]
}
return dataset
def emptyDataMenu(self):
"""Returns a menu with the empty data sets."""
self.menu = QMenu("menu")
for name in self.dataSets():
a = QAction(name, self.menu)
a.triggered.connect(self.setEmptyData)
self.menu.addAction(a)
return self.menu
def setEmptyData(self):
"""Called from the menu generated with ``emptyDataMenu``."""
act = self.sender()
data = self.dataSets()[act.text()]
def addItems(data, parent):
for d in data:
if len(d) == 1 or type(d) == str:
self.addItem(d, parent)
else:
i = self.addItem(d[0], parent)
addItems(d[1], i)
addItems(data, None)
self.mw.treeWorld.expandAll()
###############################################################################
# APPEARANCE
###############################################################################
def data(self, index, role=Qt.EditRole):
level = 0
i = index
while i.parent() != QModelIndex():
i = i.parent()
level += 1
if role == Qt.BackgroundRole:
if level == 0:
return QBrush(QColor(S.highlightLight))
if role == Qt.TextAlignmentRole:
if level == 0:
return Qt.AlignCenter
if role == Qt.FontRole:
if level in [0, 1]:
f = qApp.font()
f.setBold(True)
return f
if role == Qt.ForegroundRole:
if level == 0:
return QBrush(QColor(S.highlightedTextDark))
if role == Qt.SizeHintRole:
fm = QFontMetrics(qApp.font())
h = fm.height()
if level == 0:
return QSize(0, h + 12)
elif level == 1:
return QSize(0, h + 6)
return QStandardItemModel.data(self, index, role)
#######################################################################
# Search
#######################################################################
def searchableItems(self):
def readAll(item):
items = [WorldItemSearchWrapper(item, self.itemID(item), self.indexFromItem(item), self.data)]
for c in self.children(item):
items += readAll(c)
return items
return readAll(self.invisibleRootItem())
class WorldItemSearchWrapper(searchableItem):
def __init__(self, item, itemID, itemIndex, getColumnData):
super().__init__(WorldSearchLabels)
self.item = item
self.itemID = itemID
self.itemIndex = itemIndex
self.getColumnData = getColumnData
def searchModel(self):
return Model.World
def searchID(self):
return self.itemID
def searchTitle(self, column):
return self.item.text()
def searchPath(self, column):
def _path(item):
path = []
if item.parent():
path += _path(item.parent())
path.append(item.text())
return path
return [self.translate("World")] + _path(self.item) + [self.translate(self.searchColumnLabel(column))]
def searchData(self, column):
return self.getColumnData(self.itemIndex.sibling(self.itemIndex.row(), column))