#!/usr/bin/env python # --!-- coding: utf8 --!-- from PyQt5.QtCore import QModelIndex from PyQt5.QtCore import QSignalMapper from PyQt5.QtCore import Qt from PyQt5.QtGui import QBrush from PyQt5.QtGui import QStandardItem from PyQt5.QtGui import QStandardItemModel from PyQt5.QtWidgets import QAction, QMenu from manuskript.enums import Plot, PlotStep, Model from manuskript.functions import toInt, mainWindow from manuskript.models.searchResultModel import searchResultModel from manuskript.searchLabels import PlotSearchLabels, PLOT_STEP_COLUMNS_OFFSET from manuskript.functions import search from manuskript.models.searchableModel import searchableModel from manuskript.models.searchableItem import searchableItem class plotModel(QStandardItemModel, searchableModel): def __init__(self, parent): QStandardItemModel.__init__(self, 0, 3, parent) self.setHorizontalHeaderLabels([i.name for i in Plot]) self.mw = mainWindow() self.updatePlotPersoButton() ############################################################################### # QUERIES ############################################################################### def getPlotsByImportance(self): plots = [[], [], []] for i in range(self.rowCount()): importance = self.item(i, Plot.importance).text() ID = self.item(i, Plot.ID).text() plots[2 - toInt(importance)].append(ID) return plots def getSubPlotsByID(self, ID): index = self.getIndexFromID(ID) if not index.isValid(): return index = index.sibling(index.row(), Plot.steps) item = self.itemFromIndex(index) lst = [] for i in range(item.rowCount()): if item.child(i, PlotStep.ID): _ID = item.child(i, PlotStep.ID).text() # Don't know why sometimes name is None (while drag'n'dropping # several items) if item.child(i, PlotStep.name): name = item.child(i, PlotStep.name).text() else: name = "" # Don't know why sometimes summary is None if item.child(i, PlotStep.summary): summary = item.child(i, PlotStep.summary).text() else: summary = "" lst.append((_ID, name, summary)) return lst def getPlotNameByID(self, ID): for i in range(self.rowCount()): _ID = self.item(i, Plot.ID).text() if _ID == ID or toInt(_ID) == ID: name = self.item(i, Plot.name).text() return name return None def getPlotImportanceByRow(self, row): for i in range(self.rowCount()): if i == row: importance = self.item(i, Plot.importance).text() return importance return "0" # Default to "Minor" def getSubPlotTextsByID(self, plotID, subplotRaw): """Returns a tuple (name, summary) for the subplot whose raw in the model is ``subplotRaw``, of plot whose ID is ``plotID``. """ plotIndex = self.getIndexFromID(plotID) name = plotIndex.child(subplotRaw, PlotStep.name).data() summary = plotIndex.child(subplotRaw, PlotStep.summary).data() return name, summary def getIndexFromID(self, ID): for i in range(self.rowCount()): _ID = self.item(i, Plot.ID).text() if _ID == ID or toInt(_ID) == ID: return self.index(i, 0) return QModelIndex() def currentIndex(self): i = self.mw.lstPlots.currentIndex() if i.isValid(): return i else: return None ############################################################################### # ADDING / REMOVING ############################################################################### def addPlot(self, name="New plot"): if not name: name="New Plot" p = QStandardItem(self.tr(name)) _id = QStandardItem(self.getUniqueID()) importance = QStandardItem(str(0)) self.appendRow([p, _id, importance, QStandardItem("Characters"), QStandardItem(), QStandardItem(), QStandardItem("Resolution steps")]) return p, _id def getUniqueID(self, parent=QModelIndex()): """Returns an unused ID""" parentItem = self.itemFromIndex(parent) vals = [] for i in range(self.rowCount(parent)): index = self.index(i, Plot.ID, parent) # item = self.item(i, Plot.ID) if index.isValid() and index.data(): vals.append(int(index.data())) k = 0 while k in vals: k += 1 return str(k) def removePlot(self, index): self.takeRow(index.row()) ############################################################################### # SUBPLOTS ############################################################################### def headerData(self, section, orientation, role=Qt.DisplayRole): if role == Qt.DisplayRole: if orientation == Qt.Horizontal: if section == PlotStep.name: return self.tr("Name") elif section == PlotStep.meta: return self.tr("Meta") else: return "" else: return "" else: return QStandardItemModel.headerData(self, section, orientation, role) def data(self, index, role=Qt.DisplayRole): if index.parent().isValid() and \ index.parent().column() == Plot.steps and \ index.column() == PlotStep.meta: if role == Qt.TextAlignmentRole: return Qt.AlignRight | Qt.AlignVCenter elif role == Qt.ForegroundRole: return QBrush(Qt.gray) else: return QStandardItemModel.data(self, index, role) else: return QStandardItemModel.data(self, index, role) def addSubPlot(self): index = self.mw.lstPlots.currentPlotIndex() if not index.isValid(): return parent = index.sibling(index.row(), Plot.steps) parentItem = self.item(index.row(), Plot.steps) if not parentItem: return p = QStandardItem(self.tr("New step")) _id = QStandardItem(self.getUniqueID(parent)) summary = QStandardItem() currentIndex = self.mw.lstSubPlots.selectionModel().selectedIndexes() if currentIndex: # We use last item of selection in case of many currentIndex = currentIndex[-1] row = currentIndex.row() + 1 parentItem.insertRow(row, [p, _id, QStandardItem(), summary]) # Select last index self.mw.lstSubPlots.setCurrentIndex(currentIndex.sibling(row, 0)) else: # Don't know why, if summary is in third position, then drag/drop deletes it... parentItem.appendRow([p, _id, QStandardItem(), summary]) # Select last index self.mw.lstSubPlots.setCurrentIndex( parent.child(self.rowCount(parent) - 1, 0)) def removeSubPlot(self): """ Remove all selected subplots / plot steps, in mw.lstSubPlots. """ parent = self.mw.lstSubPlots.rootIndex() if not parent.isValid(): return parentItem = self.itemFromIndex(parent) while self.mw.lstSubPlots.selectionModel().selectedRows(): i = self.mw.lstSubPlots.selectionModel().selectedRows()[0] parentItem.takeRow(i.row()) def flags(self, index): parent = index.parent() if parent.isValid(): # this is a subitem return Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled else: return QStandardItemModel.flags(self, index) ############################################################################### # PLOT PERSOS ############################################################################### def addPlotPerso(self, v): index = self.mw.lstPlots.currentPlotIndex() if index.isValid(): if not self.item(index.row(), Plot.characters): self.setItem(index.row(), Plot.characters, QStandardItem()) item = self.item(index.row(), Plot.characters) # We check that the PersoID is not in the list yet for i in range(item.rowCount()): if item.child(i).text() == str(v): return item.appendRow(QStandardItem(str(v))) def removePlotPerso(self): index = self.mw.lstPlotPerso.currentIndex() if not index.isValid(): return parent = index.parent() parentItem = self.itemFromIndex(parent) parentItem.takeRow(index.row()) def updatePlotPersoButton(self): menu = QMenu(self.mw) menus = [] for i in [self.tr("Main"), self.tr("Secondary"), self.tr("Minor")]: m = QMenu(i, menu) menus.append(m) menu.addMenu(m) mpr = QSignalMapper(menu) for i in range(self.mw.mdlCharacter.rowCount()): a = QAction(self.mw.mdlCharacter.name(i), menu) a.setIcon(self.mw.mdlCharacter.icon(i)) a.triggered.connect(mpr.map) mpr.setMapping(a, int(self.mw.mdlCharacter.ID(i))) imp = toInt(self.mw.mdlCharacter.importance(i)) menus[2 - imp].addAction(a) # Disabling empty menus for m in menus: if not m.actions(): m.setEnabled(False) mpr.mapped.connect(self.addPlotPerso) self.mw.btnAddPlotPerso.setMenu(menu) ####################################################################### # Search ####################################################################### def searchableItems(self): items = [] for i in range(self.rowCount()): items.append(plotItemSearchWrapper(i, self.item, self.mw.mdlCharacter.getCharacterByID)) return items class plotItemSearchWrapper(searchableItem): def __init__(self, rowIndex, getItem, getCharacterByID): self.rowIndex = rowIndex self.getItem = getItem self.getCharacterByID = getCharacterByID super().__init__(PlotSearchLabels) def searchOccurrences(self, searchRegex, column): results = [] plotName = self.getItem(self.rowIndex, Plot.name).text() if column >= PLOT_STEP_COLUMNS_OFFSET: results += self.searchInPlotSteps(self.rowIndex, plotName, column, column - PLOT_STEP_COLUMNS_OFFSET, searchRegex, False) else: item_name = self.getItem(self.rowIndex, Plot.name).text() if column == Plot.characters: charactersList = self.getItem(self.rowIndex, Plot.characters) for i in range(charactersList.rowCount()): characterID = charactersList.child(i).text() character = self.getCharacterByID(characterID) if character: columnText = character.name() characterResults = search(searchRegex, columnText) if len(characterResults): # We will highlight the full character row in the plot characters list, so we # return the row index instead of the match start and end positions. results += [ searchResultModel(Model.Plot, self.getItem(self.rowIndex, Plot.ID).text(), column, self.translate(item_name), self.searchPath(column), [(i, 0)], context) for start, end, context in search(searchRegex, columnText)] else: results += super().searchOccurrences(searchRegex, column) if column == Plot.name: results += self.searchInPlotSteps(self.rowIndex, plotName, Plot.name, PlotStep.name, searchRegex, False) elif column == Plot.summary: results += self.searchInPlotSteps(self.rowIndex, plotName, Plot.summary, PlotStep.summary, searchRegex, True) return results def searchModel(self): return Model.Plot def searchID(self): return self.getItem(self.rowIndex, Plot.ID).text() def searchTitle(self, column): return self.getItem(self.rowIndex, Plot.name).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("Plot")] + _path(self.getItem(self.rowIndex, Plot.name)) + [self.translate(self.searchColumnLabel(column))] def searchData(self, column): return self.getItem(self.rowIndex, column).text() def plotStepPath(self, plotName, plotStepName, column): return [self.translate("Plot"), plotName, plotStepName, self.translate(self.searchColumnLabel(column))] def searchInPlotSteps(self, plotIndex, plotName, plotColumn, plotStepColumn, searchRegex, searchInsidePlotStep): results = [] # Plot step info can be found in two places: the own list of plot steps (this is the case for ie. name and meta # fields) and "inside" the plot step once it is selected in the list (as it's the case for the summary). if searchInsidePlotStep: # We are searching *inside* the plot step, so we return both the row index (for selecting the right plot # step in the list), and (start, end) positions of the match inside the text field for highlighting it. getSearchData = lambda rowIndex, start, end, context: ([(rowIndex, 0), (start, end)], context) else: # We are searching *in the plot step row*, so we only return the row index for selecting the right plot # step in the list when highlighting search results. getSearchData = lambda rowIndex, start, end, context: ([(rowIndex, 0)], context) item = self.getItem(plotIndex, Plot.steps) for i in range(item.rowCount()): if item.child(i, PlotStep.ID): plotStepName = item.child(i, PlotStep.name).text() plotStepText = item.child(i, plotStepColumn).text() # We will highlight the full plot step row in the plot steps list, so we # return the row index instead of the match start and end positions. results += [searchResultModel(Model.PlotStep, self.getItem(plotIndex, Plot.ID).text(), plotStepColumn, self.translate(plotStepName), self.plotStepPath(plotName, plotStepName, plotColumn), *getSearchData(i, start, end, context)) for start, end, context in search(searchRegex, plotStepText)] return results