From 420f562c1b7cce170123c87e61ca8732055e9172 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Thu, 3 Mar 2016 16:38:38 +0100 Subject: [PATCH 01/33] New CharacterModel (changes name and uses QAbstractItemModel instead of QStandardItemModel) --- manuskript/enums.py | 4 +- manuskript/functions.py | 6 +- manuskript/mainWindow.py | 104 +++---- manuskript/models/characterModel.py | 281 ++++++++++++++++++ manuskript/models/outlineModel.py | 2 +- manuskript/models/persosModel.py | 183 ------------ manuskript/models/plotModel.py | 10 +- manuskript/models/references.py | 15 +- manuskript/ui/cheatSheet.py | 19 +- manuskript/ui/editors/editorWidget.py | 2 +- manuskript/ui/mainWindow.py | 40 +-- manuskript/ui/mainWindow.ui | 8 +- manuskript/ui/views/basicItemView.py | 4 +- ...{persoTreeView.py => characterTreeView.py} | 88 ++++-- manuskript/ui/views/cmbOutlinePersoChoser.py | 14 +- manuskript/ui/views/metadataView.py | 4 +- manuskript/ui/views/outlineBasics.py | 8 +- manuskript/ui/views/outlineDelegates.py | 28 +- manuskript/ui/views/outlineView.py | 10 +- manuskript/ui/views/propertiesView.py | 4 +- manuskript/ui/views/storylineView.py | 14 +- manuskript/ui/welcome.py | 4 +- 22 files changed, 492 insertions(+), 360 deletions(-) create mode 100644 manuskript/models/characterModel.py delete mode 100644 manuskript/models/persosModel.py rename manuskript/ui/views/{persoTreeView.py => characterTreeView.py} (59%) diff --git a/manuskript/enums.py b/manuskript/enums.py index 8fa68a58..591812e0 100644 --- a/manuskript/enums.py +++ b/manuskript/enums.py @@ -9,7 +9,7 @@ from enum import Enum #def enum(**enums): #return type(str('Enum'), (), enums) -class Perso(Enum): +class Character(Enum): name = 0 ID = 1 importance = 2 @@ -21,8 +21,6 @@ class Perso(Enum): summaryPara = 8 summaryFull = 9 notes = 10 - infoName = 11 - infoData = 12 class Plot(Enum): name = 0 diff --git a/manuskript/functions.py b/manuskript/functions.py index 2e442fdb..69799f79 100644 --- a/manuskript/functions.py +++ b/manuskript/functions.py @@ -121,9 +121,9 @@ def outlineItemColors(item): # POV colors["POV"] = QColor(Qt.transparent) POV = item.data(Outline.POV.value) - for i in range(mw.mdlPersos.rowCount()): - if mw.mdlPersos.ID(i) == POV: - colors["POV"] = iconColor(mw.mdlPersos.icon(i)) + for i in range(mw.mdlCharacter.rowCount()): + if mw.mdlCharacter.ID(i) == POV: + colors["POV"] = iconColor(mw.mdlCharacter.icon(i)) # Label lbl = item.data(Outline.label.value) diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index 9fadcf25..4d5959bc 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -9,13 +9,13 @@ from PyQt5.QtWidgets import QMainWindow, QHeaderView, qApp, QMenu, QActionGroup, QLabel from manuskript import settings -from manuskript.enums import Perso, Subplot, Plot, World +from manuskript.enums import Character, Subplot, Plot, World from manuskript.functions import AUC, wordCount, appPath from manuskript.loadSave import loadStandardItemModelXML, loadFilesFromZip from manuskript.loadSave import saveFilesToZip from manuskript.loadSave import saveStandardItemModelXML +from manuskript.models.characterModel import characterModel from manuskript.models.outlineModel import outlineModel -from manuskript.models.persosModel import persosModel from manuskript.models.plotModel import plotModel from manuskript.models.worldModel import worldModel from manuskript.settingsWindow import settingsWindow @@ -144,15 +144,18 @@ class MainWindow(QMainWindow, Ui_MainWindow): # PERSOS ############################################################################### - def changeCurrentPerso(self, trash=None): + def changeCurrentCharacter(self, trash=None): + """ - index = self.lstPersos.currentPersoIndex() - - if not index.isValid(): + @return: + """ + c = self.lstCharacters.currentCharacter() + if not c: self.tabPlot.setEnabled(False) return self.tabPersos.setEnabled(True) + index = c.index() for w in [ self.txtPersoName, @@ -169,27 +172,24 @@ class MainWindow(QMainWindow, Ui_MainWindow): w.setCurrentModelIndex(index) # Button color - self.mdlPersos.updatePersoColor(index) + self.updateCharacterColor(c.ID()) - # Perso Infos + # Character Infos self.tblPersoInfos.setRootIndex(index) - if self.mdlPersos.rowCount(index): + if self.mdlCharacter.rowCount(index): self.updatePersoInfoView() def updatePersoInfoView(self): - # Hide columns - for i in range(self.mdlPersos.columnCount()): - self.tblPersoInfos.hideColumn(i) - self.tblPersoInfos.showColumn(Perso.infoName.value) - self.tblPersoInfos.showColumn(Perso.infoData.value) - - self.tblPersoInfos.horizontalHeader().setSectionResizeMode( - Perso.infoName.value, QHeaderView.ResizeToContents) - self.tblPersoInfos.horizontalHeader().setSectionResizeMode( - Perso.infoData.value, QHeaderView.Stretch) + self.tblPersoInfos.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) + self.tblPersoInfos.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) self.tblPersoInfos.verticalHeader().hide() + def updateCharacterColor(self, ID): + c = self.mdlCharacter.getCharacterByID(ID) + color = c.color().name() + self.btnPersoColor.setStyleSheet("background:{};".format(color)) + ############################################################################### # PLOTS ############################################################################### @@ -340,7 +340,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.saveTimerNoChanges.setSingleShot(True) self.mdlFlatData.dataChanged.connect(self.startTimerNoChanges) self.mdlOutline.dataChanged.connect(self.startTimerNoChanges) - self.mdlPersos.dataChanged.connect(self.startTimerNoChanges) + self.mdlCharacter.dataChanged.connect(self.startTimerNoChanges) self.mdlPlots.dataChanged.connect(self.startTimerNoChanges) self.mdlWorld.dataChanged.connect(self.startTimerNoChanges) # self.mdlPersosInfos.dataChanged.connect(self.startTimerNoChanges) @@ -467,8 +467,8 @@ class MainWindow(QMainWindow, Ui_MainWindow): files.append((saveStandardItemModelXML(self.mdlFlatData), "flatModel.xml")) - files.append((saveStandardItemModelXML(self.mdlPersos), - "perso.xml")) + # files.append((saveStandardItemModelXML(self.mdlCharacter), + # "perso.xml")) files.append((saveStandardItemModelXML(self.mdlWorld), "world.xml")) files.append((saveStandardItemModelXML(self.mdlLabels), @@ -491,7 +491,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): def loadEmptyDatas(self): self.mdlFlatData = QStandardItemModel(self) - self.mdlPersos = persosModel(self) + self.mdlCharacter = characterModel(self) # self.mdlPersosProxy = persosProxyModel(self) # self.mdlPersosInfos = QStandardItemModel(self) self.mdlLabels = QStandardItemModel(self) @@ -513,7 +513,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): errors.append("flatModel.xml") if "perso.xml" in files: - loadStandardItemModelXML(self.mdlPersos, + loadStandardItemModelXML(self.mdlCharacter, files["perso.xml"], fromString=True) else: errors.append("perso.xml") @@ -570,7 +570,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): def makeUIConnections(self): "Connections that have to be made once only, event when new project is loaded." - self.lstPersos.currentItemChanged.connect(self.changeCurrentPerso, AUC) + self.lstCharacters.currentItemChanged.connect(self.changeCurrentCharacter, AUC) self.txtPlotFilter.textChanged.connect(self.lstPlots.setFilter, AUC) self.lstPlots.currentItemChanged.connect(self.changeCurrentPlot, AUC) @@ -622,29 +622,29 @@ class MainWindow(QMainWindow, Ui_MainWindow): widget.setCurrentModelIndex(self.mdlFlatData.index(0, col)) # Persos - self.lstPersos.setPersosModel(self.mdlPersos) - self.tblPersoInfos.setModel(self.mdlPersos) + self.lstCharacters.setCharactersModel(self.mdlCharacter) + self.tblPersoInfos.setModel(self.mdlCharacter) - self.btnAddPerso.clicked.connect(self.mdlPersos.addPerso, AUC) - self.btnRmPerso.clicked.connect(self.mdlPersos.removePerso, AUC) - self.btnPersoColor.clicked.connect(self.mdlPersos.chosePersoColor, AUC) + self.btnAddPerso.clicked.connect(self.mdlCharacter.addCharacter, AUC) + self.btnRmPerso.clicked.connect(self.lstCharacters.removeCharacter, AUC) + self.btnPersoColor.clicked.connect(self.lstCharacters.choseCharacterColor, AUC) - self.btnPersoAddInfo.clicked.connect(self.mdlPersos.addPersoInfo, AUC) - self.btnPersoRmInfo.clicked.connect(self.mdlPersos.removePersoInfo, AUC) + self.btnPersoAddInfo.clicked.connect(self.lstCharacters.addCharacterInfo, AUC) + self.btnPersoRmInfo.clicked.connect(self.lstCharacters.removeCharacterInfo, AUC) for w, c in [ - (self.txtPersoName, Perso.name.value), - (self.sldPersoImportance, Perso.importance.value), - (self.txtPersoMotivation, Perso.motivation.value), - (self.txtPersoGoal, Perso.goal.value), - (self.txtPersoConflict, Perso.conflict.value), - (self.txtPersoEpiphany, Perso.epiphany.value), - (self.txtPersoSummarySentence, Perso.summarySentence.value), - (self.txtPersoSummaryPara, Perso.summaryPara.value), - (self.txtPersoSummaryFull, Perso.summaryFull.value), - (self.txtPersoNotes, Perso.notes.value) + (self.txtPersoName, Character.name.value), + (self.sldPersoImportance, Character.importance.value), + (self.txtPersoMotivation, Character.motivation.value), + (self.txtPersoGoal, Character.goal.value), + (self.txtPersoConflict, Character.conflict.value), + (self.txtPersoEpiphany, Character.epiphany.value), + (self.txtPersoSummarySentence, Character.summarySentence.value), + (self.txtPersoSummaryPara, Character.summaryPara.value), + (self.txtPersoSummaryFull, Character.summaryFull.value), + (self.txtPersoNotes, Character.notes.value) ]: - w.setModel(self.mdlPersos) + w.setModel(self.mdlCharacter) w.setColumn(c) self.tabPersos.setEnabled(False) @@ -672,10 +672,10 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.tabPlot.setEnabled(False) self.mdlPlots.updatePlotPersoButton() - self.mdlPersos.dataChanged.connect(self.mdlPlots.updatePlotPersoButton) + self.mdlCharacter.dataChanged.connect(self.mdlPlots.updatePlotPersoButton) self.lstOutlinePlots.setPlotModel(self.mdlPlots) self.lstOutlinePlots.setShowSubPlot(True) - self.plotPersoDelegate = outlinePersoDelegate(self.mdlPersos, self) + self.plotPersoDelegate = outlinePersoDelegate(self.mdlCharacter, self) self.lstPlotPerso.setItemDelegate(self.plotPersoDelegate) self.plotDelegate = plotDelegate(self) self.lstSubPlots.setItemDelegateForColumn(Subplot.meta.value, self.plotDelegate) @@ -702,18 +702,18 @@ class MainWindow(QMainWindow, Ui_MainWindow): # Outline self.treeRedacOutline.setModel(self.mdlOutline) - self.treeOutlineOutline.setModelPersos(self.mdlPersos) + self.treeOutlineOutline.setModelCharacters(self.mdlCharacter) self.treeOutlineOutline.setModelLabels(self.mdlLabels) self.treeOutlineOutline.setModelStatus(self.mdlStatus) - self.redacMetadata.setModels(self.mdlOutline, self.mdlPersos, + self.redacMetadata.setModels(self.mdlOutline, self.mdlCharacter, self.mdlLabels, self.mdlStatus) - self.outlineItemEditor.setModels(self.mdlOutline, self.mdlPersos, + self.outlineItemEditor.setModels(self.mdlOutline, self.mdlCharacter, self.mdlLabels, self.mdlStatus) self.treeOutlineOutline.setModel(self.mdlOutline) # self.redacEditor.setModel(self.mdlOutline) - self.storylineView.setModels(self.mdlOutline, self.mdlPersos, self.mdlPlots) + self.storylineView.setModels(self.mdlOutline, self.mdlCharacter, self.mdlPlots) self.treeOutlineOutline.selectionModel().selectionChanged.connect(lambda: self.outlineItemEditor.selectionChanged( @@ -735,10 +735,10 @@ class MainWindow(QMainWindow, Ui_MainWindow): # Debug self.mdlFlatData.setVerticalHeaderLabels(["Infos générales", "Summary"]) self.tblDebugFlatData.setModel(self.mdlFlatData) - self.tblDebugPersos.setModel(self.mdlPersos) - self.tblDebugPersosInfos.setModel(self.mdlPersos) + self.tblDebugPersos.setModel(self.mdlCharacter) + self.tblDebugPersosInfos.setModel(self.mdlCharacter) self.tblDebugPersos.selectionModel().currentChanged.connect( - lambda: self.tblDebugPersosInfos.setRootIndex(self.mdlPersos.index( + lambda: self.tblDebugPersosInfos.setRootIndex(self.mdlCharacter.index( self.tblDebugPersos.selectionModel().currentIndex().row(), Perso.name.value)), AUC) diff --git a/manuskript/models/characterModel.py b/manuskript/models/characterModel.py new file mode 100644 index 00000000..72f525bf --- /dev/null +++ b/manuskript/models/characterModel.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- +from PyQt5.QtCore import QModelIndex, Qt, QAbstractItemModel, QVariant +from PyQt5.QtGui import QIcon, QPixmap, QColor + +from manuskript.functions import randomColor, iconColor, mainWindow +from manuskript.enums import Character as C + + +class characterModel(QAbstractItemModel): + + def __init__(self, parent): + QAbstractItemModel.__init__(self, parent) + + self.characters = [] + +############################################################################### +# QAbstractItemModel subclassed +############################################################################### + + def rowCount(self, parent=QModelIndex()): + if parent.isValid(): + c = parent.internalPointer() + return len(c.infos) + else: + return len(self.characters) + + def columnCount(self, parent=QModelIndex()): + if parent.isValid(): + # Returns characters infos + return 2 + else: + return 1 + + def data(self, index, role=Qt.DisplayRole): + c = index.internalPointer() + if type(c) == Character: + if role == Qt.DisplayRole: + if index.column() in c._data: + return c._data[index.column()] + else: + return QVariant() + + elif type(c) == CharacterInfo: + if role == Qt.DisplayRole or role == Qt.EditRole: + if index.column() == 0: + return c.description + elif index.column() == 1: + return c.value + + def setData(self, index, value, role=Qt.EditRole): + c = index.internalPointer() + if type(c) == Character: + if role == Qt.EditRole: + # We update only if data is different + if index.column() not in c._data or c._data[index.column()] != value: + c._data[index.column()] = value + self.dataChanged.emit(index, index) + return True + + elif type(c) == CharacterInfo: + if role == Qt.EditRole: + if index.column() == 0: + c.description = value + elif index.column() == 1: + c.value = value + self.dataChanged.emit(index, index) + return True + + return False + + def index(self, row, column, parent=QModelIndex()): + if not parent.isValid(): + return self.createIndex(row, column, self.characters[row]) + + else: + c = parent.internalPointer() + if row < len(c.infos): + return self.createIndex(row, column, c.infos[row]) + else: + return QModelIndex() + + def indexFromItem(self, item, column=0): + if not item: + return QModelIndex() + + row = self.characters.index(item) + col = column + return self.createIndex(row, col, item) + + def parent(self, index): + if not index.isValid(): + return QModelIndex() + + child = index.internalPointer() + + if type(child) == Character: + return QModelIndex() + + elif type(child) == CharacterInfo: + return child.character.index() + + def flags(self, index): + if index.parent().isValid(): + return QAbstractItemModel.flags(self, index) | Qt.ItemIsEditable + else: + return QAbstractItemModel.flags(self, index) + +############################################################################### +# CHARACTER QUERRIES +############################################################################### + + def character(self, row): + return self.characters[row] + + def name(self, row): + return self.character(row).name() + + def icon(self, row): + return self.character(row).icon + + def ID(self, row): + return self.character(row).ID() + + def importance(self, row): + return self.character(row).importance() + +############################################################################### +# MODEL QUERRIES +############################################################################### + + def getCharactersByImportance(self): + """ + Lists characters by importance. + + @return: array of array of ´character´, by importance. + """ + r = [[], [], []] + for c in self.characters: + r[2-int(c.importance())].append(c) + return r + + def getCharacterByID(self, ID): + for c in self.characters: + if c.ID() == ID: + return c + return None + +############################################################################### +# ADDING / REMOVING +############################################################################### + + def addCharacter(self): + """ + Creates a new character + @return: nothing + """ + c = Character(model=self, name=self.tr("New character")) + self.beginInsertRows(QModelIndex(), len(self.characters), len(self.characters)) + self.characters.append(c) + self.endInsertRows() + + def removeCharacter(self, ID): + """ + Removes character whose ID is ID... + @param ID: the ID of the character to remove + @return: nothing + """ + c = self.getCharacterByID(ID) + self.beginRemoveRows(QModelIndex(), self.characters.index(c), self.characters.index(c)) + self.characters.remove(c) + +############################################################################### +# CHARACTER INFOS +############################################################################### + + def headerData(self, section, orientation, role=Qt.DisplayRole): + if role == Qt.DisplayRole and orientation == Qt.Horizontal: + if section == 0: + return self.tr("Name") + elif section == 1: + return self.tr("Value") + else: + return C(section).name + + def addCharacterInfo(self, ID): + c = self.getCharacterByID(ID) + self.beginInsertRows(c.index(), len(c.infos), len(c.infos)) + c.infos.append(CharacterInfo(c, description="Description", value="Value")) + self.endInsertRows() + + mainWindow().updatePersoInfoView() + + def removeCharacterInfo(self, ID): + c = self.getCharacterByID(ID) + + rm = [] + for idx in mainWindow().tblPersoInfos.selectedIndexes(): + if not idx.row() in rm: + rm.append(idx.row()) + + rm.sort() + rm.reverse() + for r in rm: + self.beginRemoveRows(c.index(), r, r) + c.infos.pop(r) + self.endRemoveRows() + +############################################################################### +# CHARACTER +############################################################################### + +class Character(): + def __init__(self, model, name): + self._model = model + + self._data = {} + self._data[C.name.value] = name + self.assignUniqueID() + self.assignRandomColor() + self._data[C.importance.value] = "0" + + self.infos = [] + + def name(self): + return self._data[C.name.value] + + def importance(self): + return self._data[C.importance.value] + + def ID(self): + return self._data[C.ID.value] + + def index(self, column=0): + return self._model.indexFromItem(self, column) + + def assignRandomColor(self): + """ + Assigns a random color the the character. + """ + color = randomColor(QColor(Qt.white)) + self.setColor(color) + + def setColor(self, color): + """ + Sets the character's color + @param color: QColor. + """ + px = QPixmap(32, 32) + px.fill(color) + self.icon = QIcon(px) + try: + self._model.dataChanged.emit(self.index(), self.index()) + except: + # If it is the initialisation, won't be able to emit + pass + + def color(self): + """ + Returns character's color in QColor + @return: QColor + """ + return iconColor(self.icon) + + def assignUniqueID(self, parent=QModelIndex()): + """Assigns an unused character ID.""" + vals = [] + for c in self._model.characters: + vals.append(c.ID()) + + k = 0 + while k in vals: + k += 1 + + self._data[C.ID.value] = k + +class CharacterInfo(): + def __init__(self, character, description="", value=""): + self.description = description + self.value = value + self.character = character \ No newline at end of file diff --git a/manuskript/models/outlineModel.py b/manuskript/models/outlineModel.py index a3450a8b..6fc547df 100644 --- a/manuskript/models/outlineModel.py +++ b/manuskript/models/outlineModel.py @@ -855,7 +855,7 @@ class outlineItem(): for c in columns: if c == Outline.POV.value: - searchIn = mainWindow.mdlPersos.getPersoNameByID(self.POV()) + searchIn = mainWindow.mdlCharacter.getPersoNameByID(self.POV()) elif c == Outline.status.value: searchIn = mainWindow.mdlStatus.item(toInt(self.status()), 0).text() diff --git a/manuskript/models/persosModel.py b/manuskript/models/persosModel.py deleted file mode 100644 index bd261911..00000000 --- a/manuskript/models/persosModel.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env python -# --!-- coding: utf8 --!-- -from PyQt5.QtCore import QModelIndex, Qt -from PyQt5.QtGui import QStandardItemModel, QStandardItem, QColor, QPixmap, QIcon -from PyQt5.QtWidgets import QColorDialog - -from manuskript.enums import Perso -from manuskript.enums import Plot -from manuskript.functions import iconColor -from manuskript.functions import mainWindow -from manuskript.functions import randomColor -from manuskript.functions import toInt - - -class persosModel(QStandardItemModel): - - def __init__(self, parent): - QStandardItemModel.__init__(self, 0, 3, parent) - self.setHorizontalHeaderLabels([i.name for i in Perso]) - self.mw = mainWindow() - # self._proxy = plotsProxyModel() - # self._proxy.setSourceModel(self) - -############################################################################### -# PERSOS QUERRIES -############################################################################### - - def name(self, row): - return self.item(row, Perso.name.value).text() - - def icon(self, row): - return self.item(row, Perso.name.value).icon() - - def ID(self, row): - return self.item(row, Perso.ID.value).text() - - def importance(self, row): - return self.item(row, Perso.importance.value).text() - -############################################################################### -# MODEL QUERRIES -############################################################################### - - def getPersosByImportance(self): - persos = [[], [], []] - for i in range(self.rowCount()): - importance = self.item(i, Perso.importance.value).text() - ID = self.item(i, Perso.ID.value).text() - persos[2-toInt(importance)].append(ID) - return persos - - def getPersoNameByID(self, ID): - index = self.getIndexFromID(ID) - if index.isValid(): - return self.name(index.row()) - return "" - - def getIndexFromID(self, ID): - for i in range(self.rowCount()): - _ID = self.item(i, Perso.ID.value).text() - if _ID == ID or toInt(_ID) == ID: - return self.index(i, 0) - return QModelIndex() - - def getPersoColorByID(self, ID): - idx = self.getIndexFromID(ID) - return self.getPersoColorName(idx) - - def getPersoColorName(self, index): - icon = self.item(index.row()).icon() - return iconColor(icon).name() if icon else "" - - def currentListIndex(self): - i = self.mw.lstPersos.currentIndex() - if i .isValid(): - return i - else: - return None - - def currentPersoIndex(self): - return self.mw.lstPersos.currentPersoIndex() - -############################################################################### -# ADDING / REMOVING -############################################################################### - - def addPerso(self): - """Creates a perso by adding a row in mdlPersos - and a column in mdlPersosInfos with same ID""" - p = QStandardItem(self.tr("New character")) - self.setPersoColor(p, randomColor(QColor(Qt.white))) - - pid = self.getUniqueID() - self.appendRow([p, QStandardItem(pid), QStandardItem("0")]) - - def getUniqueID(self, parent=QModelIndex()): - """Returns an unused perso ID (row 1).""" - vals = [] - for i in range(self.rowCount(parent)): - index = self.index(i, Perso.ID.value, parent) - if index.isValid() and index.data(): - vals.append(int(index.data())) - - k = 0 - while k in vals: - k += 1 - return str(k) - - def removePerso(self): - index = self.currentPersoIndex() - self.takeRow(index.row()) - - def setPersoColor(self, item, color): - px = QPixmap(32, 32) - px.fill(color) - item.setIcon(QIcon(px)) - - def chosePersoColor(self): - idx = self.currentPersoIndex() - item = self.item(idx.row(), Perso.name.value) - if item: - color = iconColor(item.icon()) - else: - color = Qt.white - self.colorDialog = QColorDialog(color, self.mw) - color = self.colorDialog.getColor(color) - if color.isValid(): - self.setPersoColor(item, color) - self.updatePersoColor(idx) - -############################################################################### -# UI -############################################################################### - - def updatePersoColor(self, idx): - # idx = self.currentPersoIndex() - color = self.getPersoColorName(idx) - self.mw.btnPersoColor.setStyleSheet("background:{};".format(color)) - -############################################################################### -# PERSO INFOS -############################################################################### - - def headerData(self, section, orientation, role=Qt.DisplayRole): - if role == Qt.DisplayRole and orientation == Qt.Horizontal: - if section == Perso.infoName.value: - return self.tr("Name") - elif section == Perso.infoData.value: - return self.tr("Value") - else: - return Perso(section).name - else: - return QStandardItemModel.headerData(self, section, orientation, role) - - def addPersoInfo(self): - perso = self.itemFromIndex(self.currentPersoIndex()) - row = perso.rowCount() - perso.setChild(row, Perso.infoName.value, QStandardItem("")) - perso.setChild(row, Perso.infoData.value, QStandardItem("")) - - self.mw.updatePersoInfoView() - - def removePersoInfo(self): - perso = self.itemFromIndex(self.currentPersoIndex()) - - rm = [] - for idx in self.mw.tblPersoInfos.selectedIndexes(): - if not idx.row() in rm: - rm.append(idx.row()) - - rm.sort() - rm.reverse() - for r in rm: - perso.takeRow(r) - - def listPersoInfos(self, index): - infos = [] - for i in range(self.rowCount(index)): - name = self.data(index.child(i, Perso.infoName.value)) - val = self.data(index.child(i, Perso.infoData.value)) - infos.append((name, val)) - - return infos diff --git a/manuskript/models/plotModel.py b/manuskript/models/plotModel.py index 456b8504..b7f4949f 100644 --- a/manuskript/models/plotModel.py +++ b/manuskript/models/plotModel.py @@ -212,13 +212,13 @@ class plotModel(QStandardItemModel): menu.addMenu(m) mpr = QSignalMapper(menu) - for i in range(self.mw.mdlPersos.rowCount()): - a = QAction(self.mw.mdlPersos.name(i), menu) - a.setIcon(self.mw.mdlPersos.icon(i)) + 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.mdlPersos.ID(i))) + mpr.setMapping(a, int(self.mw.mdlCharacter.ID(i))) - imp = toInt(self.mw.mdlPersos.importance(i)) + imp = toInt(self.mw.mdlCharacter.importance(i)) menus[2 - imp].addAction(a) diff --git a/manuskript/models/references.py b/manuskript/models/references.py index 6ee37ba7..6aeeb374 100644 --- a/manuskript/models/references.py +++ b/manuskript/models/references.py @@ -11,7 +11,7 @@ import re from PyQt5.QtWidgets import qApp from manuskript.enums import Outline -from manuskript.enums import Perso +from manuskript.enums import Character from manuskript.enums import Plot from manuskript.enums import Subplot from manuskript.functions import mainWindow @@ -104,7 +104,7 @@ def infos(ref): if item.POV(): POV = "{text}".format( ref=persoReference(item.POV()), - text=mainWindow().mdlPersos.getPersoNameByID(item.POV())) + text=mainWindow().mdlCharacter.getCharacterByID(item.POV()).name()) # The status of the scene status = item.status() @@ -175,7 +175,7 @@ def infos(ref): # A character elif _type == PersoLetter: - m = mainWindow().mdlPersos + m = mainWindow().mdlCharacter index = m.getIndexFromID(_ref) name = m.name(index.row()) @@ -272,7 +272,7 @@ def infos(ref): Plot.result.value)) # Characters - pM = mainWindow().mdlPersos + pM = mainWindow().mdlCharacter item = m.item(index.row(), Plot.persos.value) characters = "" if item: @@ -412,7 +412,7 @@ def shortInfos(ref): infos["type"] = PersoLetter - m = mainWindow().mdlPersos + m = mainWindow().mdlCharacter item = m.item(int(_ref), Perso.name.value) if item: infos["title"] = item.text() @@ -516,7 +516,7 @@ def refToLink(ref): text = item.title() elif _type == PersoLetter: - m = mainWindow().mdlPersos + m = mainWindow().mdlCharacter text = m.item(int(_ref), Perso.name.value).text() elif _type == PlotLetter: @@ -620,11 +620,12 @@ def open(ref): if _type == PersoLetter: mw = mainWindow() + # FIXME item = mw.lstPersos.getItemByID(_ref) if item: mw.tabMain.setCurrentIndex(mw.TabPersos) - mw.lstPersos.setCurrentItem(item) + mw.lstCharacters.setCurrentItem(item) return True print("Ref not found") diff --git a/manuskript/ui/cheatSheet.py b/manuskript/ui/cheatSheet.py index fc445ab7..1ab6c2b0 100644 --- a/manuskript/ui/cheatSheet.py +++ b/manuskript/ui/cheatSheet.py @@ -4,7 +4,7 @@ from PyQt5.QtCore import pyqtSignal, Qt, QTimer, QRect from PyQt5.QtGui import QBrush, QCursor, QPalette, QFontMetrics from PyQt5.QtWidgets import QWidget, QListWidgetItem, QToolTip, QStyledItemDelegate, QStyle -from manuskript.enums import Perso +from manuskript.enums import Character from manuskript.enums import Plot from manuskript.functions import lightBlue from manuskript.functions import mainWindow @@ -36,7 +36,7 @@ class cheatSheet(QWidget, Ui_cheatSheet): self.line.hide() self.outlineModel = None - self.persoModel = None + self.characterModel = None self.plotModel = None self.worldModel = None @@ -53,12 +53,12 @@ class cheatSheet(QWidget, Ui_cheatSheet): def setModels(self): mw = mainWindow() self.outlineModel = mw.mdlOutline - self.persoModel = mw.mdlPersos + self.characterModel = mw.mdlCharacter self.plotModel = mw.mdlPlots self.worldModel = mw.mdlWorld self.outlineModel.dataChanged.connect(self.populateTimer.start) - self.persoModel.dataChanged.connect(self.populateTimer.start) + self.characterModel.dataChanged.connect(self.populateTimer.start) self.plotModel.dataChanged.connect(self.populateTimer.start) self.worldModel.dataChanged.connect(self.populateTimer.start) @@ -75,15 +75,12 @@ class cheatSheet(QWidget, Ui_cheatSheet): self.list.hide() def populate(self): - if self.persoModel: + if self.characterModel: d = [] - for r in range(self.persoModel.rowCount()): - name = self.persoModel.item(r, Perso.name.value).text() - ID = self.persoModel.item(r, Perso.ID.value).text() - imp = self.persoModel.item(r, Perso.importance.value).text() - imp = [self.tr("Minor"), self.tr("Secondary"), self.tr("Main")][int(imp)] - d.append((name, ID, imp)) + for c in self.characterModel.characters: + imp = [self.tr("Minor"), self.tr("Secondary"), self.tr("Main")][int(c.importance())] + d.append((c.name(), c.ID(), imp)) self.data[(self.tr("Characters"), Ref.PersoLetter)] = d diff --git a/manuskript/ui/editors/editorWidget.py b/manuskript/ui/editors/editorWidget.py index 0451377f..20487b38 100644 --- a/manuskript/ui/editors/editorWidget.py +++ b/manuskript/ui/editors/editorWidget.py @@ -173,7 +173,7 @@ class editorWidget(QWidget, Ui_editorWidget_ui): elif item and item.isFolder() and self.folderView == "outline": self.stack.setCurrentIndex(3) - self.outlineView.setModelPersos(mainWindow().mdlPersos) + self.outlineView.setModelCharacters(mainWindow().mdlCharacter) self.outlineView.setModelLabels(mainWindow().mdlLabels) self.outlineView.setModelStatus(mainWindow().mdlStatus) self.outlineView.setModel(self.mw.mdlOutline) diff --git a/manuskript/ui/mainWindow.py b/manuskript/ui/mainWindow.py index 793c2d8e..9d8762b7 100644 --- a/manuskript/ui/mainWindow.py +++ b/manuskript/ui/mainWindow.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'manuskript/ui/mainWindow.ui' # -# Created: Wed Mar 2 00:30:17 2016 +# Created: Thu Mar 3 13:40:20 2016 # by: PyQt5 UI code generator 5.2.1 # # WARNING! All changes made in this file will be lost! @@ -337,12 +337,12 @@ class Ui_MainWindow(object): self.groupBox.setObjectName("groupBox") self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.groupBox) self.verticalLayout_8.setObjectName("verticalLayout_8") - self.lstPersos = persoTreeView(self.groupBox) - self.lstPersos.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.lstPersos.setDragEnabled(True) - self.lstPersos.setObjectName("lstPersos") - self.lstPersos.headerItem().setText(0, "1") - self.verticalLayout_8.addWidget(self.lstPersos) + self.lstCharacters = characterTreeView(self.groupBox) + self.lstCharacters.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.lstCharacters.setDragEnabled(True) + self.lstCharacters.setObjectName("lstCharacters") + self.lstCharacters.headerItem().setText(0, "1") + self.verticalLayout_8.addWidget(self.lstCharacters) self.horizontalLayout_14 = QtWidgets.QHBoxLayout() self.horizontalLayout_14.setObjectName("horizontalLayout_14") self.btnAddPerso = QtWidgets.QPushButton(self.groupBox) @@ -1166,7 +1166,7 @@ class Ui_MainWindow(object): self.retranslateUi(MainWindow) self.stack.setCurrentIndex(1) - self.tabMain.setCurrentIndex(6) + self.tabMain.setCurrentIndex(2) self.tabSummary.setCurrentIndex(0) self.tabPersos.setCurrentIndex(0) self.tabPlot.setCurrentIndex(0) @@ -1303,18 +1303,18 @@ class Ui_MainWindow(object): self.actCompile.setShortcut(_translate("MainWindow", "F6")) self.actToolFrequency.setText(_translate("MainWindow", "&Frequency Analyzer")) -from manuskript.ui.views.storylineView import storylineView -from manuskript.ui.views.textEditView import textEditView -from manuskript.ui.views.lineEditView import lineEditView -from manuskript.ui.views.treeView import treeView -from manuskript.ui.editors.mainEditor import mainEditor -from manuskript.ui.views.basicItemView import basicItemView -from manuskript.ui.views.persoTreeView import persoTreeView -from manuskript.ui.views.plotTreeView import plotTreeView -from manuskript.ui.views.outlineView import outlineView -from manuskript.ui.views.metadataView import metadataView +from manuskript.ui.search import search from manuskript.ui.cheatSheet import cheatSheet from manuskript.ui.views.textEditCompleter import textEditCompleter -from manuskript.ui.sldImportance import sldImportance +from manuskript.ui.views.lineEditView import lineEditView from manuskript.ui.welcome import welcome -from manuskript.ui.search import search +from manuskript.ui.views.characterTreeView import characterTreeView +from manuskript.ui.sldImportance import sldImportance +from manuskript.ui.views.plotTreeView import plotTreeView +from manuskript.ui.views.basicItemView import basicItemView +from manuskript.ui.views.outlineView import outlineView +from manuskript.ui.views.metadataView import metadataView +from manuskript.ui.views.treeView import treeView +from manuskript.ui.editors.mainEditor import mainEditor +from manuskript.ui.views.storylineView import storylineView +from manuskript.ui.views.textEditView import textEditView diff --git a/manuskript/ui/mainWindow.ui b/manuskript/ui/mainWindow.ui index 1a5390d1..7e34ea70 100644 --- a/manuskript/ui/mainWindow.ui +++ b/manuskript/ui/mainWindow.ui @@ -124,7 +124,7 @@ QTabWidget::Rounded - 6 + 2 true @@ -714,7 +714,7 @@ - + Qt::ScrollBarAlwaysOff @@ -2396,9 +2396,9 @@ QListView::item:hover { 1 - persoTreeView + characterTreeView QTreeWidget -
manuskript.ui.views.persoTreeView.h
+
manuskript.ui.views.characterTreeView.h
cheatSheet diff --git a/manuskript/ui/views/basicItemView.py b/manuskript/ui/views/basicItemView.py index 2da5fe96..42dc1cb3 100644 --- a/manuskript/ui/views/basicItemView.py +++ b/manuskript/ui/views/basicItemView.py @@ -14,8 +14,8 @@ class basicItemView(QWidget, Ui_basicItemView): self.txtSummaryFull.setColumn(Outline.summaryFull.value) self.txtGoal.setColumn(Outline.setGoal.value) - def setModels(self, mdlOutline, mdlPersos, mdlLabels, mdlStatus): - self.cmbPOV.setModels(mdlPersos, mdlOutline) + def setModels(self, mdlOutline, mdlCharacter, mdlLabels, mdlStatus): + self.cmbPOV.setModels(mdlCharacter, mdlOutline) self.txtSummarySentence.setModel(mdlOutline) self.txtSummaryFull.setModel(mdlOutline) self.txtGoal.setModel(mdlOutline) diff --git a/manuskript/ui/views/persoTreeView.py b/manuskript/ui/views/characterTreeView.py similarity index 59% rename from manuskript/ui/views/persoTreeView.py rename to manuskript/ui/views/characterTreeView.py index fd60d192..71e354a1 100644 --- a/manuskript/ui/views/persoTreeView.py +++ b/manuskript/ui/views/characterTreeView.py @@ -2,12 +2,16 @@ # --!-- coding: utf8 --!-- from PyQt5.QtCore import QSize, QModelIndex, Qt from PyQt5.QtGui import QPixmap, QColor, QIcon, QBrush -from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem +from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QColorDialog -from manuskript.enums import Perso +from manuskript.enums import Character +from manuskript.functions import iconColor, mainWindow -class persoTreeView(QTreeWidget): +class characterTreeView(QTreeWidget): + """ + A QTreeWidget that displays characters from a characterModel in respect of their importance. + """ def __init__(self, parent=None): QTreeWidget.__init__(self, parent) self._model = None @@ -24,7 +28,7 @@ class persoTreeView(QTreeWidget): self._rootItem = QTreeWidgetItem() self.insertTopLevelItem(0, self._rootItem) - def setPersosModel(self, model): + def setCharactersModel(self, model): self._model = model self._model.dataChanged.connect(self.updateMaybe) self._model.rowsInserted.connect(self.updateMaybe2) @@ -39,11 +43,11 @@ class persoTreeView(QTreeWidget): if topLeft.parent() != QModelIndex(): return - if topLeft.column() <= Perso.name.value <= bottomRight.column(): + if topLeft.column() <= Character.name.value <= bottomRight.column(): # Update name self.updateNames() - elif topLeft.column() <= Perso.importance.value <= bottomRight.column(): + elif topLeft.column() <= Character.importance.value <= bottomRight.column(): # Importance changed self.updateItems() @@ -56,16 +60,17 @@ class persoTreeView(QTreeWidget): for i in range(self.topLevelItemCount()): item = self.topLevelItem(i) - for c in range(item.childCount()): - sub = item.child(c) + for child in range(item.childCount()): + sub = item.child(child) ID = sub.data(0, Qt.UserRole) - if ID: + if ID is not None: # Update name - name = self._model.getPersoNameByID(ID) + c = self._model.getCharacterByID(ID) + name = c.name() sub.setText(0, name) # Update icon px = QPixmap(32, 32) - color = QColor(self._model.getPersoColorByID(ID)) + color = c.color() px.fill(color) sub.setIcon(0, QIcon(px)) @@ -78,10 +83,12 @@ class persoTreeView(QTreeWidget): self._updating = True self.clear() - persos = self._model.getPersosByImportance() + characters = self._model.getCharactersByImportance() h = [self.tr("Main"), self.tr("Secondary"), self.tr("Minor")] + for i in range(3): + # Create category item cat = QTreeWidgetItem(self, [h[i]]) cat.setBackground(0, QBrush(QColor(Qt.blue).lighter(190))) cat.setForeground(0, QBrush(Qt.darkBlue)) @@ -92,36 +99,67 @@ class persoTreeView(QTreeWidget): self.addTopLevelItem(cat) # cat.setChildIndicatorPolicy(cat.DontShowIndicator) - for ID in persos[i]: - name = self._model.getPersoNameByID(ID) + for c in characters[i]: + name = c.name() + # Check if name passes filter if not self._filter.lower() in name.lower(): continue + item = QTreeWidgetItem(cat, [name]) - item.setData(0, Qt.UserRole, ID) + item.setData(0, Qt.UserRole, c.ID()) px = QPixmap(32, 32) - color = QColor(self._model.getPersoColorByID(ID)) + color = QColor(c.color()) px.fill(color) item.setIcon(0, QIcon(px)) - if ID == self._lastID: + if c.ID() == self._lastID: self.setCurrentItem(item) self.expandAll() self._updating = False - def getItemByID(self, ID): - for t in range(self.topLevelItemCount()): - for i in range(self.topLevelItem(t).childCount()): - item = self.topLevelItem(t).child(i) - if item.data(0, Qt.UserRole) == ID: - return item + def removeCharacter(self): + """ + Removes selected character. + """ + ID = self.currentCharacterID() + if ID: + self._model.removeCharacter(ID) - def currentPersoIndex(self): + def choseCharacterColor(self): + ID = self.currentCharacterID() + c = self._model.getCharacterByID(ID) + if c: + color = iconColor(c.icon) + else: + color = Qt.white + self.colorDialog = QColorDialog(color, mainWindow()) + color = self.colorDialog.getColor(color) + if color.isValid(): + c.setColor(color) + mainWindow().updateCharacterColor(ID) + + def addCharacterInfo(self): + self._model.addCharacterInfo(self.currentCharacterID()) + + def removeCharacterInfo(self): + self._model.removeCharacterInfo(self.currentCharacterID(), + ) + + def currentCharacterID(self): ID = None if self.currentItem(): ID = self.currentItem().data(0, Qt.UserRole) - return self._model.getIndexFromID(ID) + return ID + + def currentCharacter(self): + """ + Returns the selected character + @return: Character + """ + ID = self.currentCharacterID() + return self._model.getCharacterByID(ID) def mouseDoubleClickEvent(self, event): item = self.currentItem() diff --git a/manuskript/ui/views/cmbOutlinePersoChoser.py b/manuskript/ui/views/cmbOutlinePersoChoser.py index 6747b510..dfb9449d 100644 --- a/manuskript/ui/views/cmbOutlinePersoChoser.py +++ b/manuskript/ui/views/cmbOutlinePersoChoser.py @@ -18,9 +18,9 @@ class cmbOutlinePersoChoser(QComboBox): self._updating = False self._various = False - def setModels(self, mdlPersos, mdlOutline): - self.mdlPersos = mdlPersos - self.mdlPersos.dataChanged.connect(self.updateItems) + def setModels(self, mdlCharacter, mdlOutline): + self.mdlCharacters = mdlCharacter + self.mdlCharacters.dataChanged.connect(self.updateItems) self.mdlOutline = mdlOutline self.mdlOutline.dataChanged.connect(self.update) self.updateItems() @@ -37,14 +37,14 @@ class cmbOutlinePersoChoser(QComboBox): self.setItemData(self.count() - 1, QBrush(QColor(Qt.blue).lighter(190)), Qt.BackgroundRole) item = self.model().item(self.count() - 1) item.setFlags(Qt.ItemIsEnabled) - for i in range(self.mdlPersos.rowCount()): - imp = toInt(self.mdlPersos.importance(i)) + for i in range(self.mdlCharacters.rowCount()): + imp = toInt(self.mdlCharacters.importance(i)) if not 2 - imp == importance: continue - self.addItem(self.mdlPersos.icon(i), self.mdlPersos.name(i), self.mdlPersos.ID(i)) - self.setItemData(self.count() - 1, self.mdlPersos.name(i), Qt.ToolTipRole) + self.addItem(self.mdlCharacters.icon(i), self.mdlCharacters.name(i), self.mdlCharacters.ID(i)) + self.setItemData(self.count() - 1, self.mdlCharacters.name(i), Qt.ToolTipRole) self._various = False diff --git a/manuskript/ui/views/metadataView.py b/manuskript/ui/views/metadataView.py index 4791f5b1..0f06d6eb 100644 --- a/manuskript/ui/views/metadataView.py +++ b/manuskript/ui/views/metadataView.py @@ -16,8 +16,8 @@ class metadataView(QWidget, Ui_metadataView): self.txtNotes.setColumn(Outline.notes.value) self.revisions.setEnabled(False) - def setModels(self, mdlOutline, mdlPersos, mdlLabels, mdlStatus): - self.properties.setModels(mdlOutline, mdlPersos, mdlLabels, mdlStatus) + def setModels(self, mdlOutline, mdlCharacter, mdlLabels, mdlStatus): + self.properties.setModels(mdlOutline, mdlCharacter, mdlLabels, mdlStatus) self.txtSummarySentence.setModel(mdlOutline) self.txtSummaryFull.setModel(mdlOutline) self.txtNotes.setModel(mdlOutline) diff --git a/manuskript/ui/views/outlineBasics.py b/manuskript/ui/views/outlineBasics.py index 4d548f60..1a40dac3 100644 --- a/manuskript/ui/views/outlineBasics.py +++ b/manuskript/ui/views/outlineBasics.py @@ -83,12 +83,12 @@ class outlineBasics(QAbstractItemView): self.menuPOV.addMenu(m) mpr = QSignalMapper(self.menuPOV) - for i in range(mw.mdlPersos.rowCount()): - a = QAction(mw.mdlPersos.icon(i), mw.mdlPersos.name(i), self.menuPOV) + for i in range(mw.mdlCharacter.rowCount()): + a = QAction(mw.mdlCharacter.icon(i), mw.mdlCharacter.name(i), self.menuPOV) a.triggered.connect(mpr.map) - mpr.setMapping(a, int(mw.mdlPersos.ID(i))) + mpr.setMapping(a, int(mw.mdlCharacter.ID(i))) - imp = toInt(mw.mdlPersos.importance(i)) + imp = toInt(mw.mdlCharacter.importance(i)) menus[2 - imp].addAction(a) diff --git a/manuskript/ui/views/outlineDelegates.py b/manuskript/ui/views/outlineDelegates.py index 1dcd0c57..1172f766 100644 --- a/manuskript/ui/views/outlineDelegates.py +++ b/manuskript/ui/views/outlineDelegates.py @@ -6,7 +6,7 @@ from PyQt5.QtWidgets import QStyledItemDelegate, QStyleOptionViewItem, QStyle, Q from PyQt5.QtWidgets import qApp from manuskript import settings -from manuskript.enums import Perso, Outline +from manuskript.enums import Character, Outline from manuskript.functions import outlineItemColors, mixColors, colorifyPixmap, toInt, toFloat, drawProgress @@ -93,17 +93,17 @@ class outlineTitleDelegate(QStyledItemDelegate): class outlinePersoDelegate(QStyledItemDelegate): - def __init__(self, mdlPersos, parent=None): + def __init__(self, mdlCharacter, parent=None): QStyledItemDelegate.__init__(self, parent) - self.mdlPersos = mdlPersos + self.mdlCharacter = mdlCharacter def sizeHint(self, option, index): # s = QStyledItemDelegate.sizeHint(self, option, index) item = QModelIndex() - for i in range(self.mdlPersos.rowCount()): - if self.mdlPersos.ID(i) == index.data(): - item = self.mdlPersos.index(i, Perso.name.value) + for i in range(self.mdlCharacter.rowCount()): + if self.mdlCharacter.ID(i) == index.data(): + item = self.mdlCharacter.index(i, Character.name.value) opt = QStyleOptionViewItem(option) self.initStyleOption(opt, item) @@ -136,13 +136,13 @@ class outlinePersoDelegate(QStyledItemDelegate): editor.setItemData(editor.count() - 1, QBrush(QColor(Qt.blue).lighter(190)), Qt.BackgroundRole) item = editor.model().item(editor.count() - 1) item.setFlags(Qt.ItemIsEnabled) - for i in range(self.mdlPersos.rowCount()): - imp = toInt(self.mdlPersos.importance(i)) + for i in range(self.mdlCharacter.rowCount()): + imp = toInt(self.mdlCharacter.importance(i)) if not 2 - imp == importance: continue # try: - editor.addItem(self.mdlPersos.icon(i), self.mdlPersos.name(i), self.mdlPersos.ID(i)) - editor.setItemData(editor.count() - 1, self.mdlPersos.name(i), Qt.ToolTipRole) + editor.addItem(self.mdlCharacter.icon(i), self.mdlCharacter.name(i), self.mdlCharacter.ID(i)) + editor.setItemData(editor.count() - 1, self.mdlCharacter.name(i), Qt.ToolTipRole) # except: # pass @@ -159,9 +159,9 @@ class outlinePersoDelegate(QStyledItemDelegate): ##option.rect.setWidth(option.rect.width() + 18) item = QModelIndex() - for i in range(self.mdlPersos.rowCount()): - if self.mdlPersos.ID(i) == index.data(): - item = self.mdlPersos.index(i, Perso.name.value) + for i in range(self.mdlCharacter.rowCount()): + if self.mdlCharacter.ID(i) == index.data(): + item = self.mdlCharacter.index(i, Character.name.value) opt = QStyleOptionViewItem(option) self.initStyleOption(opt, item) @@ -169,7 +169,7 @@ class outlinePersoDelegate(QStyledItemDelegate): qApp.style().drawControl(QStyle.CE_ItemViewItem, opt, painter) # if index.isValid() and index.internalPointer().data(Outline.POV.value) not in ["", None]: - if index.isValid() and self.mdlPersos.data(index) not in ["", None]: + if index.isValid() and self.mdlCharacter.data(index) not in ["", None]: opt = QStyleOptionComboBox() opt.rect = option.rect r = qApp.style().subControlRect(QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxArrow) diff --git a/manuskript/ui/views/outlineView.py b/manuskript/ui/views/outlineView.py index 848a8246..670894f2 100644 --- a/manuskript/ui/views/outlineView.py +++ b/manuskript/ui/views/outlineView.py @@ -11,20 +11,20 @@ from manuskript.ui.views.outlineDelegates import outlineTitleDelegate, outlinePe class outlineView(QTreeView, dndView, outlineBasics): - def __init__(self, parent=None, modelPersos=None, modelLabels=None, modelStatus=None): + def __init__(self, parent=None, modelCharacters=None, modelLabels=None, modelStatus=None): QTreeView.__init__(self, parent) dndView.__init__(self) outlineBasics.__init__(self, parent) - self.modelPersos = modelPersos + self.modelCharacters = modelCharacters self.modelLabels = modelLabels self.modelStatus = modelStatus self.header().setStretchLastSection(False) - def setModelPersos(self, model): + def setModelCharacters(self, model): # This is used by outlinePersoDelegate to select character - self.modelPersos = model + self.modelCharacters = model def setModelLabels(self, model): # This is used by outlineLabelDelegate to display labels @@ -41,7 +41,7 @@ class outlineView(QTreeView, dndView, outlineBasics): self.outlineTitleDelegate = outlineTitleDelegate(self) # self.outlineTitleDelegate.setView(self) self.setItemDelegateForColumn(Outline.title.value, self.outlineTitleDelegate) - self.outlinePersoDelegate = outlinePersoDelegate(self.modelPersos) + self.outlinePersoDelegate = outlinePersoDelegate(self.modelCharacters) self.setItemDelegateForColumn(Outline.POV.value, self.outlinePersoDelegate) self.outlineCompileDelegate = outlineCompileDelegate() self.setItemDelegateForColumn(Outline.compile.value, self.outlineCompileDelegate) diff --git a/manuskript/ui/views/propertiesView.py b/manuskript/ui/views/propertiesView.py index 8cdedda3..ce18cf0c 100644 --- a/manuskript/ui/views/propertiesView.py +++ b/manuskript/ui/views/propertiesView.py @@ -12,8 +12,8 @@ class propertiesView(QWidget, Ui_propertiesView): self.setupUi(self) self.txtGoal.setColumn(Outline.setGoal.value) - def setModels(self, mdlOutline, mdlPersos, mdlLabels, mdlStatus): - self.cmbPOV.setModels(mdlPersos, mdlOutline) + def setModels(self, mdlOutline, mdlCharacter, mdlLabels, mdlStatus): + self.cmbPOV.setModels(mdlCharacter, mdlOutline) self.cmbLabel.setModels(mdlLabels, mdlOutline) self.cmbStatus.setModels(mdlStatus, mdlOutline) self.cmbType.setModel(mdlOutline) diff --git a/manuskript/ui/views/storylineView.py b/manuskript/ui/views/storylineView.py index a5f81362..f5f40840 100644 --- a/manuskript/ui/views/storylineView.py +++ b/manuskript/ui/views/storylineView.py @@ -46,7 +46,7 @@ class storylineView(QWidget, Ui_storylineView): self.btnSettings.setMenu(m) - def setModels(self, mdlOutline, mdlPersos, mdlPlots): + def setModels(self, mdlOutline, mdlCharacter, mdlPlots): self._mdlPlots = mdlPlots # self._mdlPlots.dataChanged.connect(self.refresh) # self._mdlPlots.rowsInserted.connect(self.refresh) @@ -54,8 +54,8 @@ class storylineView(QWidget, Ui_storylineView): self._mdlOutline = mdlOutline self._mdlOutline.dataChanged.connect(self.reloadTimer.start) - self._mdlPersos = mdlPersos - self._mdlPersos.dataChanged.connect(self.reloadTimer.start) + self._mdlCharacter = mdlCharacter + self._mdlCharacter.dataChanged.connect(self.reloadTimer.start) def plotReferences(self): "Returns a list of plot references" @@ -73,10 +73,10 @@ class storylineView(QWidget, Ui_storylineView): def persosReferences(self): "Returns a list of character references" - if not self._mdlPersos: + if not self._mdlCharacter: pass - IDs = self._mdlPersos.getPersosByImportance() + IDs = self._mdlCharacter.getPersosByImportance() r = [] for importance in IDs: for ID in importance: @@ -86,7 +86,7 @@ class storylineView(QWidget, Ui_storylineView): return r def refresh(self): - if not self._mdlPlots or not self._mdlOutline or not self._mdlPersos: + if not self._mdlPlots or not self._mdlOutline or not self._mdlCharacter: pass LINE_HEIGHT = 18 @@ -222,7 +222,7 @@ class storylineView(QWidget, Ui_storylineView): for ref in trackedItems: if references.type(ref) == references.PersoLetter: - color = QColor(self._mdlPersos.getPersoColorByID(references.ID(ref))) + color = QColor(self._mdlCharacter.getPersoColorByID(references.ID(ref))) else: color = randomColor() diff --git a/manuskript/ui/welcome.py b/manuskript/ui/welcome.py index 137ceb22..e1248dd1 100644 --- a/manuskript/ui/welcome.py +++ b/manuskript/ui/welcome.py @@ -12,9 +12,9 @@ from PyQt5.QtWidgets import QWidget, QAction, QFileDialog, QSpinBox, QLineEdit, from manuskript import settings from manuskript.enums import Outline from manuskript.functions import mainWindow, iconFromColor, appPath +from manuskript.models.characterModel import characterModel from manuskript.models.outlineModel import outlineItem from manuskript.models.outlineModel import outlineModel -from manuskript.models.persosModel import persosModel from manuskript.models.plotModel import plotModel from manuskript.models.worldModel import worldModel from manuskript.ui.welcome_ui import Ui_welcome @@ -345,7 +345,7 @@ class welcome(QWidget, Ui_welcome): # Persos # self.mw.mdlPersos = QStandardItemModel(0, 0, self.mw) - self.mw.mdlPersos = persosModel(self.mw) + self.mw.mdlCharacter = characterModel(self.mw) # self.mdlPersosProxy = None # persosProxyModel() # None # self.mw.mdlPersosProxy = persosProxyModel(self.mw) From a17745a89a20b1da93a46f8113e6089636e6e714 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Thu, 3 Mar 2016 17:18:30 +0100 Subject: [PATCH 02/33] Corrects references, cheatsheet, storyline, ... --- manuskript/models/characterModel.py | 9 +++- manuskript/models/references.py | 62 ++++++++++++----------- manuskript/ui/cheatSheet.py | 4 +- manuskript/ui/editors/basicHighlighter.py | 2 +- manuskript/ui/views/characterTreeView.py | 7 +++ manuskript/ui/views/storylineView.py | 18 +++---- 6 files changed, 61 insertions(+), 41 deletions(-) diff --git a/manuskript/models/characterModel.py b/manuskript/models/characterModel.py index 72f525bf..85745344 100644 --- a/manuskript/models/characterModel.py +++ b/manuskript/models/characterModel.py @@ -39,7 +39,7 @@ class characterModel(QAbstractItemModel): if index.column() in c._data: return c._data[index.column()] else: - return QVariant() + return "" elif type(c) == CharacterInfo: if role == Qt.DisplayRole or role == Qt.EditRole: @@ -141,6 +141,7 @@ class characterModel(QAbstractItemModel): return r def getCharacterByID(self, ID): + ID = int(ID) for c in self.characters: if c.ID() == ID: return c @@ -274,6 +275,12 @@ class Character(): self._data[C.ID.value] = k + def listInfos(self): + r = [] + for i in self.infos: + r.append((i.description, i.value)) + return r + class CharacterInfo(): def __init__(self, character, description="", value=""): self.description = description diff --git a/manuskript/models/references.py b/manuskript/models/references.py index 6aeeb374..16663199 100644 --- a/manuskript/models/references.py +++ b/manuskript/models/references.py @@ -22,7 +22,7 @@ RegExNonCapturing = r"{\w:\d+:?.*?}" # The basic format of the references EmptyRef = "{{{}:{}:{}}}" EmptyRefSearchable = "{{{}:{}:" -PersoLetter = "C" +CharacterLetter = "C" TextLetter = "T" PlotLetter = "P" WorldLetter = "W" @@ -37,13 +37,13 @@ def plotReference(ID, searchable=False): return EmptyRefSearchable.format(PlotLetter, ID, "") -def persoReference(ID, searchable=False): +def characterReference(ID, searchable=False): """Takes the ID of a character and returns a reference for that character. @searchable: returns a stripped version that allows simple text search.""" if not searchable: - return EmptyRef.format(PersoLetter, ID, "") + return EmptyRef.format(CharacterLetter, ID, "") else: - return EmptyRefSearchable.format(PersoLetter, ID, "") + return EmptyRefSearchable.format(CharacterLetter, ID, "") def textReference(ID, searchable=False): @@ -103,7 +103,7 @@ def infos(ref): POV = "" if item.POV(): POV = "{text}".format( - ref=persoReference(item.POV()), + ref=characterReference(item.POV()), text=mainWindow().mdlCharacter.getCharacterByID(item.POV()).name()) # The status of the scene @@ -174,10 +174,12 @@ def infos(ref): return text # A character - elif _type == PersoLetter: + elif _type == CharacterLetter: m = mainWindow().mdlCharacter - index = m.getIndexFromID(_ref) - name = m.name(index.row()) + c = m.getCharacterByID(int(_ref)) + index = c.index() + + name = c.name() # Titles basicTitle = qApp.translate("references", "Basic infos") @@ -191,14 +193,16 @@ def infos(ref): # basic infos basic = [] for i in [ - (Perso.motivation, qApp.translate("references", "Motivation"), False), - (Perso.goal, qApp.translate("references", "Goal"), False), - (Perso.conflict, qApp.translate("references", "Conflict"), False), - (Perso.epiphany, qApp.translate("references", "Epiphany"), False), - (Perso.summarySentence, qApp.translate("references", "Short summary"), True), - (Perso.summaryPara, qApp.translate("references", "Longer summary"), True), + (Character.motivation, qApp.translate("references", "Motivation"), False), + (Character.goal, qApp.translate("references", "Goal"), False), + (Character.conflict, qApp.translate("references", "Conflict"), False), + (Character.epiphany, qApp.translate("references", "Epiphany"), False), + (Character.summarySentence, qApp.translate("references", "Short summary"), True), + (Character.summaryPara, qApp.translate("references", "Longer summary"), True), ]: + val = m.data(index.sibling(index.row(), i[0].value)) + if val: basic.append("{title}:{n}{val}".format( title=i[1], @@ -208,7 +212,7 @@ def infos(ref): # detailed infos detailed = [] - for _name, _val in m.listPersoInfos(index): + for _name, _val in c.listInfos(): detailed.append("{}: {}".format( _name, _val)) @@ -279,7 +283,7 @@ def infos(ref): for r in range(item.rowCount()): ID = item.child(r, 0).text() characters += "
  • {text}".format( - link=persoReference(ID), + link=characterReference(ID), text=pM.getPersoNameByID(ID)) # Resolution steps @@ -408,15 +412,16 @@ def shortInfos(ref): infos["path"] = item.path() return infos - elif _type == PersoLetter: + elif _type == CharacterLetter: - infos["type"] = PersoLetter + infos["type"] = CharacterLetter m = mainWindow().mdlCharacter - item = m.item(int(_ref), Perso.name.value) - if item: - infos["title"] = item.text() - infos["name"] = item.text() + c = m.getCharacterByID(int(_ref)) + + if c: + infos["title"] = c.name() + infos["name"] = c.name() return infos elif _type == PlotLetter: @@ -482,7 +487,7 @@ def tooltip(ref): tt += "
    {}".format(infos["path"]) return tt - elif infos["type"] == PersoLetter: + elif infos["type"] == CharacterLetter: return qApp.translate("references", "Character: {}").format(infos["title"]) elif infos["type"] == PlotLetter: @@ -515,9 +520,9 @@ def refToLink(ref): item = idx.internalPointer() text = item.title() - elif _type == PersoLetter: + elif _type == CharacterLetter: m = mainWindow().mdlCharacter - text = m.item(int(_ref), Perso.name.value).text() + text = m.getCharacterByID(int(_ref)).name() elif _type == PlotLetter: m = mainWindow().mdlPlots @@ -618,17 +623,16 @@ def open(ref): _type = match.group(1) _ref = match.group(2) - if _type == PersoLetter: + if _type == CharacterLetter: mw = mainWindow() - # FIXME - item = mw.lstPersos.getItemByID(_ref) + item = mw.lstCharacters.getItemByID(int(_ref)) if item: mw.tabMain.setCurrentIndex(mw.TabPersos) mw.lstCharacters.setCurrentItem(item) return True - print("Ref not found") + print("Error: Ref {} not found".format(ref)) return False elif _type == TextLetter: diff --git a/manuskript/ui/cheatSheet.py b/manuskript/ui/cheatSheet.py index 1ab6c2b0..2e3a4401 100644 --- a/manuskript/ui/cheatSheet.py +++ b/manuskript/ui/cheatSheet.py @@ -59,6 +59,8 @@ class cheatSheet(QWidget, Ui_cheatSheet): self.outlineModel.dataChanged.connect(self.populateTimer.start) self.characterModel.dataChanged.connect(self.populateTimer.start) + self.characterModel.rowsInserted.connect(self.populateTimer.start) + self.characterModel.rowsRemoved.connect(self.populateTimer.start) self.plotModel.dataChanged.connect(self.populateTimer.start) self.worldModel.dataChanged.connect(self.populateTimer.start) @@ -82,7 +84,7 @@ class cheatSheet(QWidget, Ui_cheatSheet): imp = [self.tr("Minor"), self.tr("Secondary"), self.tr("Main")][int(c.importance())] d.append((c.name(), c.ID(), imp)) - self.data[(self.tr("Characters"), Ref.PersoLetter)] = d + self.data[(self.tr("Characters"), Ref.CharacterLetter)] = d if self.outlineModel: d = [] diff --git a/manuskript/ui/editors/basicHighlighter.py b/manuskript/ui/editors/basicHighlighter.py index b5055060..f0d5050b 100644 --- a/manuskript/ui/editors/basicHighlighter.py +++ b/manuskript/ui/editors/basicHighlighter.py @@ -61,7 +61,7 @@ class basicHighlighter(QSyntaxHighlighter): fmt.setFontWeight(QFont.DemiBold) if txt.group(1) == Ref.TextLetter: fmt.setBackground(QBrush(QColor(Qt.blue).lighter(190))) - elif txt.group(1) == Ref.PersoLetter: + elif txt.group(1) == Ref.CharacterLetter: fmt.setBackground(QBrush(QColor(Qt.yellow).lighter(170))) elif txt.group(1) == Ref.PlotLetter: fmt.setBackground(QBrush(QColor(Qt.red).lighter(170))) diff --git a/manuskript/ui/views/characterTreeView.py b/manuskript/ui/views/characterTreeView.py index 71e354a1..2bfbe55d 100644 --- a/manuskript/ui/views/characterTreeView.py +++ b/manuskript/ui/views/characterTreeView.py @@ -161,6 +161,13 @@ class characterTreeView(QTreeWidget): ID = self.currentCharacterID() return self._model.getCharacterByID(ID) + def getItemByID(self, ID): + for t in range(self.topLevelItemCount()): + for i in range(self.topLevelItem(t).childCount()): + item = self.topLevelItem(t).child(i) + if item.data(0, Qt.UserRole) == ID: + return item + def mouseDoubleClickEvent(self, event): item = self.currentItem() # Catching double clicks to forbid collapsing of toplevel items diff --git a/manuskript/ui/views/storylineView.py b/manuskript/ui/views/storylineView.py index f5f40840..e10fc81e 100644 --- a/manuskript/ui/views/storylineView.py +++ b/manuskript/ui/views/storylineView.py @@ -71,16 +71,16 @@ class storylineView(QWidget, Ui_storylineView): return r - def persosReferences(self): + def charactersReferences(self): "Returns a list of character references" if not self._mdlCharacter: pass - IDs = self._mdlCharacter.getPersosByImportance() + chars = self._mdlCharacter.getCharactersByImportance() r = [] - for importance in IDs: - for ID in importance: - ref = references.persoReference(ID) + for importance in chars: + for c in importance: + ref = references.characterReference(c.ID()) r.append(ref) return r @@ -118,7 +118,7 @@ class storylineView(QWidget, Ui_storylineView): trackedItems += self.plotReferences() if self.actCharacters.isChecked(): - trackedItems += self.persosReferences() + trackedItems += self.charactersReferences() ROWS_HEIGHT = len(trackedItems) * (LINE_HEIGHT + SPACING ) @@ -185,7 +185,7 @@ class storylineView(QWidget, Ui_storylineView): # Tests if POV scenePOV = False # Will hold true of character is POV of the current text, not containing folder - if references.type(ref) == references.PersoLetter: + if references.type(ref) == references.CharacterLetter: ID = references.ID(ref) c = child while c: @@ -221,8 +221,8 @@ class storylineView(QWidget, Ui_storylineView): itemsRect.setPos(0, MAX_LEVEL * LEVEL_HEIGHT + SPACING) for ref in trackedItems: - if references.type(ref) == references.PersoLetter: - color = QColor(self._mdlCharacter.getPersoColorByID(references.ID(ref))) + if references.type(ref) == references.CharacterLetter: + color = self._mdlCharacter.getCharacterByID(references.ID(ref)).color() else: color = randomColor() From 7e05e2227540cae57e6aa828c429b317a70d1479 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Thu, 3 Mar 2016 17:41:19 +0100 Subject: [PATCH 03/33] Corrects a few things in combo and other places --- manuskript/models/characterModel.py | 13 +++++++------ manuskript/models/outlineModel.py | 2 +- manuskript/models/references.py | 2 +- manuskript/ui/views/basicItemView_ui.py | 8 ++++---- manuskript/ui/views/basicItemView_ui.ui | 6 +++--- ...PersoChoser.py => cmbOutlineCharacterChoser.py} | 5 ++++- manuskript/ui/views/propertiesView_ui.py | 14 +++++++------- manuskript/ui/views/propertiesView_ui.ui | 8 ++++---- 8 files changed, 31 insertions(+), 27 deletions(-) rename manuskript/ui/views/{cmbOutlinePersoChoser.py => cmbOutlineCharacterChoser.py} (96%) diff --git a/manuskript/models/characterModel.py b/manuskript/models/characterModel.py index 85745344..a9881f94 100644 --- a/manuskript/models/characterModel.py +++ b/manuskript/models/characterModel.py @@ -141,10 +141,11 @@ class characterModel(QAbstractItemModel): return r def getCharacterByID(self, ID): - ID = int(ID) - for c in self.characters: - if c.ID() == ID: - return c + if ID is not None: + ID = str(ID) + for c in self.characters: + if c.ID() == ID: + return c return None ############################################################################### @@ -267,13 +268,13 @@ class Character(): """Assigns an unused character ID.""" vals = [] for c in self._model.characters: - vals.append(c.ID()) + vals.append(int(c.ID())) k = 0 while k in vals: k += 1 - self._data[C.ID.value] = k + self._data[C.ID.value] = str(k) def listInfos(self): r = [] diff --git a/manuskript/models/outlineModel.py b/manuskript/models/outlineModel.py index 6fc547df..9f2c1fe0 100644 --- a/manuskript/models/outlineModel.py +++ b/manuskript/models/outlineModel.py @@ -855,7 +855,7 @@ class outlineItem(): for c in columns: if c == Outline.POV.value: - searchIn = mainWindow.mdlCharacter.getPersoNameByID(self.POV()) + searchIn = mainWindow.mdlCharacter.getCharacterByID(self.POV()).name() elif c == Outline.status.value: searchIn = mainWindow.mdlStatus.item(toInt(self.status()), 0).text() diff --git a/manuskript/models/references.py b/manuskript/models/references.py index 16663199..64261d7e 100644 --- a/manuskript/models/references.py +++ b/manuskript/models/references.py @@ -417,7 +417,7 @@ def shortInfos(ref): infos["type"] = CharacterLetter m = mainWindow().mdlCharacter - c = m.getCharacterByID(int(_ref)) + c = m.getCharacterByID(_ref) if c: infos["title"] = c.name() diff --git a/manuskript/ui/views/basicItemView_ui.py b/manuskript/ui/views/basicItemView_ui.py index 6be70e81..f0594fad 100644 --- a/manuskript/ui/views/basicItemView_ui.py +++ b/manuskript/ui/views/basicItemView_ui.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'manuskript/ui/views/basicItemView_ui.ui' # -# Created: Wed Mar 2 00:33:34 2016 +# Created: Thu Mar 3 17:26:11 2016 # by: PyQt5 UI code generator 5.2.1 # # WARNING! All changes made in this file will be lost! @@ -24,7 +24,7 @@ class Ui_basicItemView(object): self.lblPlanPOV.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.lblPlanPOV.setObjectName("lblPlanPOV") self.horizontalLayout_11.addWidget(self.lblPlanPOV) - self.cmbPOV = cmbOutlinePersoChoser(basicItemView) + self.cmbPOV = cmbOutlineCharacterChoser(basicItemView) self.cmbPOV.setFrame(False) self.cmbPOV.setObjectName("cmbPOV") self.horizontalLayout_11.addWidget(self.cmbPOV) @@ -67,6 +67,6 @@ class Ui_basicItemView(object): self.txtSummarySentence.setPlaceholderText(_translate("basicItemView", "One line summary")) self.label_9.setText(_translate("basicItemView", "Few sentences summary:")) -from manuskript.ui.views.cmbOutlinePersoChoser import cmbOutlinePersoChoser -from manuskript.ui.views.textEditView import textEditView +from manuskript.ui.views.cmbOutlineCharacterChoser import cmbOutlineCharacterChoser from manuskript.ui.views.lineEditView import lineEditView +from manuskript.ui.views.textEditView import textEditView diff --git a/manuskript/ui/views/basicItemView_ui.ui b/manuskript/ui/views/basicItemView_ui.ui index 64552858..6466cc73 100644 --- a/manuskript/ui/views/basicItemView_ui.ui +++ b/manuskript/ui/views/basicItemView_ui.ui @@ -43,7 +43,7 @@ - + false @@ -112,9 +112,9 @@
    manuskript.ui.views.textEditView.h
    - cmbOutlinePersoChoser + cmbOutlineCharacterChoser QComboBox -
    manuskript.ui.views.cmbOutlinePersoChoser.h
    +
    manuskript.ui.views.cmbOutlineCharacterChoser.h
    lineEditView diff --git a/manuskript/ui/views/cmbOutlinePersoChoser.py b/manuskript/ui/views/cmbOutlineCharacterChoser.py similarity index 96% rename from manuskript/ui/views/cmbOutlinePersoChoser.py rename to manuskript/ui/views/cmbOutlineCharacterChoser.py index dfb9449d..4185aca9 100644 --- a/manuskript/ui/views/cmbOutlinePersoChoser.py +++ b/manuskript/ui/views/cmbOutlineCharacterChoser.py @@ -8,7 +8,7 @@ from manuskript.enums import Outline from manuskript.functions import toInt -class cmbOutlinePersoChoser(QComboBox): +class cmbOutlineCharacterChoser(QComboBox): def __init__(self, parent=None): QComboBox.__init__(self, parent) self.activated[int].connect(self.submit) @@ -21,6 +21,9 @@ class cmbOutlinePersoChoser(QComboBox): def setModels(self, mdlCharacter, mdlOutline): self.mdlCharacters = mdlCharacter self.mdlCharacters.dataChanged.connect(self.updateItems) + self.mdlCharacters.rowsInserted.connect(self.updateItems) + self.mdlCharacters.rowsRemoved.connect(self.updateItems) + self.mdlOutline = mdlOutline self.mdlOutline.dataChanged.connect(self.update) self.updateItems() diff --git a/manuskript/ui/views/propertiesView_ui.py b/manuskript/ui/views/propertiesView_ui.py index 0d9a56e2..217b3ab5 100644 --- a/manuskript/ui/views/propertiesView_ui.py +++ b/manuskript/ui/views/propertiesView_ui.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'manuskript/ui/views/propertiesView_ui.ui' # -# Created: Wed Mar 2 00:30:18 2016 +# Created: Thu Mar 3 17:26:11 2016 # by: PyQt5 UI code generator 5.2.1 # # WARNING! All changes made in this file will be lost! @@ -37,7 +37,7 @@ class Ui_propertiesView(object): self.lblPOV = QtWidgets.QLabel(self.page) self.lblPOV.setObjectName("lblPOV") self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.lblPOV) - self.cmbPOV = cmbOutlinePersoChoser(self.page) + self.cmbPOV = cmbOutlineCharacterChoser(self.page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -116,7 +116,7 @@ class Ui_propertiesView(object): self.lblPOV_2 = QtWidgets.QLabel(self.page_2) self.lblPOV_2.setObjectName("lblPOV_2") self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.lblPOV_2) - self.cmbPOVMulti = cmbOutlinePersoChoser(self.page_2) + self.cmbPOVMulti = cmbOutlineCharacterChoser(self.page_2) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -195,9 +195,9 @@ class Ui_propertiesView(object): self.label_36.setText(_translate("propertiesView", "Goal")) self.txtGoalMulti.setPlaceholderText(_translate("propertiesView", "Word count")) -from manuskript.ui.views.cmbOutlineStatusChoser import cmbOutlineStatusChoser from manuskript.ui.views.lineEditView import lineEditView -from manuskript.ui.views.chkOutlineCompile import chkOutlineCompile -from manuskript.ui.views.cmbOutlineLabelChoser import cmbOutlineLabelChoser +from manuskript.ui.views.cmbOutlineCharacterChoser import cmbOutlineCharacterChoser from manuskript.ui.views.cmbOutlineTypeChoser import cmbOutlineTypeChoser -from manuskript.ui.views.cmbOutlinePersoChoser import cmbOutlinePersoChoser +from manuskript.ui.views.chkOutlineCompile import chkOutlineCompile +from manuskript.ui.views.cmbOutlineStatusChoser import cmbOutlineStatusChoser +from manuskript.ui.views.cmbOutlineLabelChoser import cmbOutlineLabelChoser diff --git a/manuskript/ui/views/propertiesView_ui.ui b/manuskript/ui/views/propertiesView_ui.ui index 0c342b84..d76b439d 100644 --- a/manuskript/ui/views/propertiesView_ui.ui +++ b/manuskript/ui/views/propertiesView_ui.ui @@ -53,7 +53,7 @@
    - + 0 @@ -190,7 +190,7 @@ - + 0 @@ -300,9 +300,9 @@
    manuskript.ui.views.lineEditView.h
    - cmbOutlinePersoChoser + cmbOutlineCharacterChoser QComboBox -
    manuskript.ui.views.cmbOutlinePersoChoser.h
    +
    manuskript.ui.views.cmbOutlineCharacterChoser.h
    cmbOutlineStatusChoser From f57e8d2ab1d6c9990227224b27964409892b6381 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Thu, 3 Mar 2016 18:48:45 +0100 Subject: [PATCH 04/33] Bug --- manuskript/mainWindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index 4d5959bc..5c6511cc 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -740,7 +740,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.tblDebugPersos.selectionModel().currentChanged.connect( lambda: self.tblDebugPersosInfos.setRootIndex(self.mdlCharacter.index( self.tblDebugPersos.selectionModel().currentIndex().row(), - Perso.name.value)), AUC) + Character.name.value)), AUC) self.tblDebugPlots.setModel(self.mdlPlots) self.tblDebugPlotsPersos.setModel(self.mdlPlots) From cec3d2563c51a00add85e3f35922c5339ca4523f Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Thu, 3 Mar 2016 18:48:53 +0100 Subject: [PATCH 05/33] Clean up --- manuskript/loadSave.py | 89 +++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/manuskript/loadSave.py b/manuskript/loadSave.py index af5dfc6d..38c45bb9 100644 --- a/manuskript/loadSave.py +++ b/manuskript/loadSave.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -#--!-- coding: utf8 --!-- +# --!-- coding: utf8 --!-- import zipfile @@ -11,22 +11,25 @@ from lxml import etree as ET from manuskript.functions import iconColor, iconFromColorString try: - import zlib # Used with zipfile for compression + import zlib # Used with zipfile for compression + compression = zipfile.ZIP_DEFLATED except: compression = zipfile.ZIP_STORED + def saveFilesToZip(files, zipname): """Saves given files to zipname. files is actually a list of (content, filename).""" - + zf = zipfile.ZipFile(zipname, mode="w") - + for content, filename in files: zf.writestr(filename, content, compress_type=compression) - + zf.close() - + + def loadFilesFromZip(zipname): """Returns the content of zipfile as a dict of filename:content.""" print(zipname) @@ -35,14 +38,15 @@ def loadFilesFromZip(zipname): for f in zf.namelist(): files[f] = zf.read(f) return files - + + def saveStandardItemModelXML(mdl, xml=None): """Saves the given QStandardItemModel to XML. If xml (filename) is given, saves to xml. Otherwise returns as string.""" - + root = ET.Element("model") root.attrib["version"] = qApp.applicationVersion() - + # Header header = ET.SubElement(root, "header") vHeader = ET.SubElement(header, "vertical") @@ -50,28 +54,29 @@ def saveStandardItemModelXML(mdl, xml=None): vH = ET.SubElement(vHeader, "label") vH.attrib["row"] = str(x) vH.attrib["text"] = str(mdl.headerData(x, Qt.Vertical)) - + hHeader = ET.SubElement(header, "horizontal") for y in range(mdl.columnCount()): hH = ET.SubElement(hHeader, "label") hH.attrib["row"] = str(y) hH.attrib["text"] = str(mdl.headerData(y, Qt.Horizontal)) - + # Data data = ET.SubElement(root, "data") saveItem(data, mdl) - - #print(qApp.tr("Saving to {}.").format(xml)) + + # print(qApp.tr("Saving to {}.").format(xml)) if xml: ET.ElementTree(root).write(xml, encoding="UTF-8", xml_declaration=True, pretty_print=True) else: return ET.tostring(root, encoding="UTF-8", xml_declaration=True, pretty_print=True) - + + def saveItem(root, mdl, parent=QModelIndex()): for x in range(mdl.rowCount(parent)): row = ET.SubElement(root, "row") row.attrib["row"] = str(x) - + for y in range(mdl.columnCount(parent)): col = ET.SubElement(row, "col") col.attrib["col"] = str(y) @@ -82,13 +87,14 @@ def saveItem(root, mdl, parent=QModelIndex()): col.text = mdl.data(mdl.index(x, y, parent)) if mdl.hasChildren(mdl.index(x, y, parent)): saveItem(col, mdl, mdl.index(x, y, parent)) - + + def loadStandardItemModelXML(mdl, xml, fromString=False): """Load data to a QStandardItemModel mdl from xml. By default xml is a filename. If fromString=True, xml is a string containg the data.""" - - #print(qApp.tr("Loading {}... ").format(xml), end="") - + + # print(qApp.tr("Loading {}... ").format(xml), end="") + if not fromString: try: tree = ET.parse(xml) @@ -97,35 +103,36 @@ def loadStandardItemModelXML(mdl, xml, fromString=False): return else: root = ET.fromstring(xml) - - #root = tree.getroot() - - #Header + + # root = tree.getroot() + + # Header hLabels = [] vLabels = [] for l in root.find("header").find("horizontal").findall("label"): hLabels.append(l.attrib["text"]) for l in root.find("header").find("vertical").findall("label"): vLabels.append(l.attrib["text"]) - - #print(root.find("header").find("vertical").text) - - #mdl.setVerticalHeaderLabels(vLabels) - #mdl.setHorizontalHeaderLabels(hLabels) - + + # print(root.find("header").find("vertical").text) + + # mdl.setVerticalHeaderLabels(vLabels) + # mdl.setHorizontalHeaderLabels(hLabels) + # Populates with empty items for i in enumerate(vLabels): row = [] for r in enumerate(hLabels): row.append(QStandardItem()) mdl.appendRow(row) - - #Data + + # Data data = root.find("data") loadItem(data, mdl) - + return True - + + def loadItem(root, mdl, parent=QModelIndex()): for row in root: r = int(row.attrib["row"]) @@ -135,15 +142,15 @@ def loadItem(root, mdl, parent=QModelIndex()): if not item: item = QStandardItem() mdl.itemFromIndex(parent).setChild(r, c, item) - - if col.text: - #mdl.setData(mdl.index(r, c, parent), col.text) + + if col.text: + # mdl.setData(mdl.index(r, c, parent), col.text) item.setText(col.text) - + if "color" in col.attrib: - #mdl.itemFromIndex(mdl.index(r, c, parent)).setIcon(iconFromColorString(col.attrib["color"])) + # mdl.itemFromIndex(mdl.index(r, c, parent)).setIcon(iconFromColorString(col.attrib["color"])) item.setIcon(iconFromColorString(col.attrib["color"])) - + if len(col) != 0: - #loadItem(col, mdl, mdl.index(r, c, parent)) - loadItem(col, mdl, mdl.indexFromItem(item)) \ No newline at end of file + # loadItem(col, mdl, mdl.index(r, c, parent)) + loadItem(col, mdl, mdl.indexFromItem(item)) From aece6ca87f15a40d889acd373cd7c97e5ef4a27b Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Thu, 3 Mar 2016 18:55:57 +0100 Subject: [PATCH 06/33] Moves sldImportance to views folder, where it belongs --- manuskript/ui/mainWindow.py | 22 +++++++++---------- manuskript/ui/mainWindow.ui | 2 +- manuskript/ui/{ => views}/sldImportance.py | 2 +- manuskript/ui/{ => views}/sldImportance_ui.py | 7 +++--- manuskript/ui/{ => views}/sldImportance_ui.ui | 0 5 files changed, 17 insertions(+), 16 deletions(-) rename manuskript/ui/{ => views}/sldImportance.py (97%) rename manuskript/ui/{ => views}/sldImportance_ui.py (89%) rename manuskript/ui/{ => views}/sldImportance_ui.ui (100%) diff --git a/manuskript/ui/mainWindow.py b/manuskript/ui/mainWindow.py index 9d8762b7..ed9ab078 100644 --- a/manuskript/ui/mainWindow.py +++ b/manuskript/ui/mainWindow.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'manuskript/ui/mainWindow.ui' # -# Created: Thu Mar 3 13:40:20 2016 +# Created: Thu Mar 3 18:52:22 2016 # by: PyQt5 UI code generator 5.2.1 # # WARNING! All changes made in this file will be lost! @@ -1303,18 +1303,18 @@ class Ui_MainWindow(object): self.actCompile.setShortcut(_translate("MainWindow", "F6")) self.actToolFrequency.setText(_translate("MainWindow", "&Frequency Analyzer")) -from manuskript.ui.search import search +from manuskript.ui.views.outlineView import outlineView +from manuskript.ui.views.textEditView import textEditView +from manuskript.ui.views.basicItemView import basicItemView +from manuskript.ui.views.plotTreeView import plotTreeView from manuskript.ui.cheatSheet import cheatSheet -from manuskript.ui.views.textEditCompleter import textEditCompleter +from manuskript.ui.views.sldImportance import sldImportance +from manuskript.ui.views.metadataView import metadataView +from manuskript.ui.views.characterTreeView import characterTreeView +from manuskript.ui.editors.mainEditor import mainEditor +from manuskript.ui.search import search from manuskript.ui.views.lineEditView import lineEditView from manuskript.ui.welcome import welcome -from manuskript.ui.views.characterTreeView import characterTreeView -from manuskript.ui.sldImportance import sldImportance -from manuskript.ui.views.plotTreeView import plotTreeView -from manuskript.ui.views.basicItemView import basicItemView -from manuskript.ui.views.outlineView import outlineView -from manuskript.ui.views.metadataView import metadataView from manuskript.ui.views.treeView import treeView -from manuskript.ui.editors.mainEditor import mainEditor +from manuskript.ui.views.textEditCompleter import textEditCompleter from manuskript.ui.views.storylineView import storylineView -from manuskript.ui.views.textEditView import textEditView diff --git a/manuskript/ui/mainWindow.ui b/manuskript/ui/mainWindow.ui index 7e34ea70..9f9e7c6e 100644 --- a/manuskript/ui/mainWindow.ui +++ b/manuskript/ui/mainWindow.ui @@ -2358,7 +2358,7 @@ QListView::item:hover { sldImportance QWidget -
    manuskript.ui.sldImportance.h
    +
    manuskript.ui.views.sldImportance.h
    1
    diff --git a/manuskript/ui/sldImportance.py b/manuskript/ui/views/sldImportance.py similarity index 97% rename from manuskript/ui/sldImportance.py rename to manuskript/ui/views/sldImportance.py index ad239a8d..bd303d1e 100644 --- a/manuskript/ui/sldImportance.py +++ b/manuskript/ui/views/sldImportance.py @@ -2,9 +2,9 @@ # --!-- coding: utf8 --!-- from PyQt5.QtCore import pyqtSignal, pyqtProperty from PyQt5.QtWidgets import QWidget +from manuskript.ui.views.sldImportance_ui import Ui_sldImportance from manuskript.functions import toInt -from manuskript.ui.sldImportance_ui import Ui_sldImportance class sldImportance(QWidget, Ui_sldImportance): diff --git a/manuskript/ui/sldImportance_ui.py b/manuskript/ui/views/sldImportance_ui.py similarity index 89% rename from manuskript/ui/sldImportance_ui.py rename to manuskript/ui/views/sldImportance_ui.py index f7e3c0b2..639bbdc7 100644 --- a/manuskript/ui/sldImportance_ui.py +++ b/manuskript/ui/views/sldImportance_ui.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'manuskript/ui/sldImportance_ui.ui' +# Form implementation generated from reading ui file 'manuskript/ui/views/sldImportance_ui.ui' # -# Created by: PyQt5 UI code generator 5.4.1 +# Created: Thu Mar 3 18:52:22 2016 +# by: PyQt5 UI code generator 5.2.1 # # WARNING! All changes made in this file will be lost! -from PyQt5 import QtCore, QtWidgets +from PyQt5 import QtCore, QtGui, QtWidgets class Ui_sldImportance(object): def setupUi(self, sldImportance): diff --git a/manuskript/ui/sldImportance_ui.ui b/manuskript/ui/views/sldImportance_ui.ui similarity index 100% rename from manuskript/ui/sldImportance_ui.ui rename to manuskript/ui/views/sldImportance_ui.ui From 1f85ee617196ff45a31bf8d2c60c866cbc3d82c8 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Fri, 4 Mar 2016 21:57:38 +0100 Subject: [PATCH 07/33] Working on a more flexible loading/saving system --- manuskript/functions.py | 4 + manuskript/loadSave.py | 159 ++----------------- manuskript/load_save/__init__.py | 0 manuskript/load_save/version_0.py | 249 ++++++++++++++++++++++++++++++ manuskript/load_save/version_1.py | 81 ++++++++++ manuskript/mainWindow.py | 76 +-------- 6 files changed, 352 insertions(+), 217 deletions(-) create mode 100644 manuskript/load_save/__init__.py create mode 100644 manuskript/load_save/version_0.py create mode 100644 manuskript/load_save/version_1.py diff --git a/manuskript/functions.py b/manuskript/functions.py index 69799f79..fa8cffd8 100644 --- a/manuskript/functions.py +++ b/manuskript/functions.py @@ -179,6 +179,10 @@ def allPaths(suffix=None): return paths def lightBlue(): + """ + A light blue used in several places in manuskript. + @return: QColor + """ return QColor(Qt.blue).lighter(190) def totalObjects(): diff --git a/manuskript/loadSave.py b/manuskript/loadSave.py index 38c45bb9..1476b7df 100644 --- a/manuskript/loadSave.py +++ b/manuskript/loadSave.py @@ -1,156 +1,27 @@ #!/usr/bin/env python # --!-- coding: utf8 --!-- -import zipfile +# The loadSave file calls the propper functions to load and save file +# trying to detect the proper file format if it comes from an older version -from PyQt5.QtCore import QModelIndex, Qt -from PyQt5.QtGui import QColor, QStandardItem -from PyQt5.QtWidgets import qApp -from lxml import etree as ET +import manuskript.load_save.version_0 as v0 +import manuskript.load_save.version_1 as v1 -from manuskript.functions import iconColor, iconFromColorString +def saveProject(version=None): -try: - import zlib # Used with zipfile for compression - - compression = zipfile.ZIP_DEFLATED -except: - compression = zipfile.ZIP_STORED - - -def saveFilesToZip(files, zipname): - """Saves given files to zipname. - files is actually a list of (content, filename).""" - - zf = zipfile.ZipFile(zipname, mode="w") - - for content, filename in files: - zf.writestr(filename, content, compress_type=compression) - - zf.close() - - -def loadFilesFromZip(zipname): - """Returns the content of zipfile as a dict of filename:content.""" - print(zipname) - zf = zipfile.ZipFile(zipname) - files = {} - for f in zf.namelist(): - files[f] = zf.read(f) - return files - - -def saveStandardItemModelXML(mdl, xml=None): - """Saves the given QStandardItemModel to XML. - If xml (filename) is given, saves to xml. Otherwise returns as string.""" - - root = ET.Element("model") - root.attrib["version"] = qApp.applicationVersion() - - # Header - header = ET.SubElement(root, "header") - vHeader = ET.SubElement(header, "vertical") - for x in range(mdl.rowCount()): - vH = ET.SubElement(vHeader, "label") - vH.attrib["row"] = str(x) - vH.attrib["text"] = str(mdl.headerData(x, Qt.Vertical)) - - hHeader = ET.SubElement(header, "horizontal") - for y in range(mdl.columnCount()): - hH = ET.SubElement(hHeader, "label") - hH.attrib["row"] = str(y) - hH.attrib["text"] = str(mdl.headerData(y, Qt.Horizontal)) - - # Data - data = ET.SubElement(root, "data") - saveItem(data, mdl) - - # print(qApp.tr("Saving to {}.").format(xml)) - if xml: - ET.ElementTree(root).write(xml, encoding="UTF-8", xml_declaration=True, pretty_print=True) + if version == 0: + v0.saveProject() else: - return ET.tostring(root, encoding="UTF-8", xml_declaration=True, pretty_print=True) + v1.saveProject() -def saveItem(root, mdl, parent=QModelIndex()): - for x in range(mdl.rowCount(parent)): - row = ET.SubElement(root, "row") - row.attrib["row"] = str(x) +def loadProject(project): - for y in range(mdl.columnCount(parent)): - col = ET.SubElement(row, "col") - col.attrib["col"] = str(y) - if mdl.data(mdl.index(x, y, parent), Qt.DecorationRole) != None: - color = iconColor(mdl.data(mdl.index(x, y, parent), Qt.DecorationRole)).name(QColor.HexArgb) - col.attrib["color"] = color if color != "#ff000000" else "#00000000" - if mdl.data(mdl.index(x, y, parent)) != "": - col.text = mdl.data(mdl.index(x, y, parent)) - if mdl.hasChildren(mdl.index(x, y, parent)): - saveItem(col, mdl, mdl.index(x, y, parent)) + # Detect version + # FIXME + version = 0 - -def loadStandardItemModelXML(mdl, xml, fromString=False): - """Load data to a QStandardItemModel mdl from xml. - By default xml is a filename. If fromString=True, xml is a string containg the data.""" - - # print(qApp.tr("Loading {}... ").format(xml), end="") - - if not fromString: - try: - tree = ET.parse(xml) - except: - print("Failed.") - return + if version == 0: + v0.loadProject(project) else: - root = ET.fromstring(xml) - - # root = tree.getroot() - - # Header - hLabels = [] - vLabels = [] - for l in root.find("header").find("horizontal").findall("label"): - hLabels.append(l.attrib["text"]) - for l in root.find("header").find("vertical").findall("label"): - vLabels.append(l.attrib["text"]) - - # print(root.find("header").find("vertical").text) - - # mdl.setVerticalHeaderLabels(vLabels) - # mdl.setHorizontalHeaderLabels(hLabels) - - # Populates with empty items - for i in enumerate(vLabels): - row = [] - for r in enumerate(hLabels): - row.append(QStandardItem()) - mdl.appendRow(row) - - # Data - data = root.find("data") - loadItem(data, mdl) - - return True - - -def loadItem(root, mdl, parent=QModelIndex()): - for row in root: - r = int(row.attrib["row"]) - for col in row: - c = int(col.attrib["col"]) - item = mdl.itemFromIndex(mdl.index(r, c, parent)) - if not item: - item = QStandardItem() - mdl.itemFromIndex(parent).setChild(r, c, item) - - if col.text: - # mdl.setData(mdl.index(r, c, parent), col.text) - item.setText(col.text) - - if "color" in col.attrib: - # mdl.itemFromIndex(mdl.index(r, c, parent)).setIcon(iconFromColorString(col.attrib["color"])) - item.setIcon(iconFromColorString(col.attrib["color"])) - - if len(col) != 0: - # loadItem(col, mdl, mdl.index(r, c, parent)) - loadItem(col, mdl, mdl.indexFromItem(item)) + v1.loadProject(project) diff --git a/manuskript/load_save/__init__.py b/manuskript/load_save/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manuskript/load_save/version_0.py b/manuskript/load_save/version_0.py new file mode 100644 index 00000000..4023539e --- /dev/null +++ b/manuskript/load_save/version_0.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +# Version 0 of file saving format. +# Was used at the begining and up util version XXX when +# it was superseded by Version 1, which is more open and flexible + +import zipfile + +from PyQt5.QtCore import QModelIndex, Qt +from PyQt5.QtGui import QColor, QStandardItem +from PyQt5.QtWidgets import qApp +from lxml import etree as ET + +from manuskript import settings +from manuskript.functions import iconColor, iconFromColorString, mainWindow + +try: + import zlib # Used with zipfile for compression + + compression = zipfile.ZIP_DEFLATED +except: + compression = zipfile.ZIP_STORED + +########################################################################################### +# SAVE +########################################################################################### + +def saveProject(): + """ + Saves the whole project. Call this function to save the project in Version 0 format. + """ + + files = [] + mw = mainWindow() + + files.append((saveStandardItemModelXML(mw.mdlFlatData), + "flatModel.xml")) + files.append((saveStandardItemModelXML(mw.mdlCharacter), + "perso.xml")) + files.append((saveStandardItemModelXML(mw.mdlWorld), + "world.xml")) + files.append((saveStandardItemModelXML(mw.mdlLabels), + "labels.xml")) + files.append((saveStandardItemModelXML(mw.mdlStatus), + "status.xml")) + files.append((saveStandardItemModelXML(mw.mdlPlots), + "plots.xml")) + files.append((mw.mdlOutline.saveToXML(), + "outline.xml")) + files.append((settings.save(), + "settings.pickle")) + + saveFilesToZip(files, mw.currentProject) + +def saveFilesToZip(files, zipname): + """Saves given files to zipname. + files is actually a list of (content, filename).""" + + zf = zipfile.ZipFile(zipname, mode="w") + + for content, filename in files: + zf.writestr(filename, content, compress_type=compression) + + zf.close() + +def saveStandardItemModelXML(mdl, xml=None): + """Saves the given QStandardItemModel to XML. + If xml (filename) is given, saves to xml. Otherwise returns as string.""" + + root = ET.Element("model") + root.attrib["version"] = qApp.applicationVersion() + + # Header + header = ET.SubElement(root, "header") + vHeader = ET.SubElement(header, "vertical") + for x in range(mdl.rowCount()): + vH = ET.SubElement(vHeader, "label") + vH.attrib["row"] = str(x) + vH.attrib["text"] = str(mdl.headerData(x, Qt.Vertical)) + + hHeader = ET.SubElement(header, "horizontal") + for y in range(mdl.columnCount()): + hH = ET.SubElement(hHeader, "label") + hH.attrib["row"] = str(y) + hH.attrib["text"] = str(mdl.headerData(y, Qt.Horizontal)) + + # Data + data = ET.SubElement(root, "data") + saveItem(data, mdl) + + # print(qApp.tr("Saving to {}.").format(xml)) + if xml: + ET.ElementTree(root).write(xml, encoding="UTF-8", xml_declaration=True, pretty_print=True) + else: + return ET.tostring(root, encoding="UTF-8", xml_declaration=True, pretty_print=True) + + +def saveItem(root, mdl, parent=QModelIndex()): + for x in range(mdl.rowCount(parent)): + row = ET.SubElement(root, "row") + row.attrib["row"] = str(x) + + for y in range(mdl.columnCount(parent)): + col = ET.SubElement(row, "col") + col.attrib["col"] = str(y) + if mdl.data(mdl.index(x, y, parent), Qt.DecorationRole) != None: + color = iconColor(mdl.data(mdl.index(x, y, parent), Qt.DecorationRole)).name(QColor.HexArgb) + col.attrib["color"] = color if color != "#ff000000" else "#00000000" + if mdl.data(mdl.index(x, y, parent)) != "": + col.text = mdl.data(mdl.index(x, y, parent)) + if mdl.hasChildren(mdl.index(x, y, parent)): + saveItem(col, mdl, mdl.index(x, y, parent)) + +########################################################################################### +# LOAD +########################################################################################### + +def loadProject(project): + + files = loadFilesFromZip(project) + mw = mainWindow() + + errors = [] + + if "flatModel.xml" in files: + loadStandardItemModelXML(mw.mdlFlatData, + files["flatModel.xml"], fromString=True) + else: + errors.append("flatModel.xml") + + if "perso.xml" in files: + loadStandardItemModelXML(mw.mdlCharacter, + files["perso.xml"], fromString=True) + else: + errors.append("perso.xml") + + if "world.xml" in files: + loadStandardItemModelXML(mw.mdlWorld, + files["world.xml"], fromString=True) + else: + errors.append("world.xml") + + if "labels.xml" in files: + loadStandardItemModelXML(mw.mdlLabels, + files["labels.xml"], fromString=True) + else: + errors.append("perso.xml") + + if "status.xml" in files: + loadStandardItemModelXML(mw.mdlStatus, + files["status.xml"], fromString=True) + else: + errors.append("perso.xml") + + if "plots.xml" in files: + loadStandardItemModelXML(mw.mdlPlots, + files["plots.xml"], fromString=True) + else: + errors.append("perso.xml") + + if "outline.xml" in files: + mw.mdlOutline.loadFromXML(files["outline.xml"], fromString=True) + else: + errors.append("perso.xml") + + if "settings.pickle" in files: + settings.load(files["settings.pickle"], fromString=True) + else: + errors.append("perso.xml") + + return errors + + +def loadFilesFromZip(zipname): + """Returns the content of zipfile as a dict of filename:content.""" + print(zipname) + zf = zipfile.ZipFile(zipname) + files = {} + for f in zf.namelist(): + files[f] = zf.read(f) + return files + + +def loadStandardItemModelXML(mdl, xml, fromString=False): + """Load data to a QStandardItemModel mdl from xml. + By default xml is a filename. If fromString=True, xml is a string containg the data.""" + + # print(qApp.tr("Loading {}... ").format(xml), end="") + + if not fromString: + try: + tree = ET.parse(xml) + except: + print("Failed.") + return + else: + root = ET.fromstring(xml) + + # root = tree.getroot() + + # Header + hLabels = [] + vLabels = [] + for l in root.find("header").find("horizontal").findall("label"): + hLabels.append(l.attrib["text"]) + for l in root.find("header").find("vertical").findall("label"): + vLabels.append(l.attrib["text"]) + + # print(root.find("header").find("vertical").text) + + # mdl.setVerticalHeaderLabels(vLabels) + # mdl.setHorizontalHeaderLabels(hLabels) + + # Populates with empty items + for i in enumerate(vLabels): + row = [] + for r in enumerate(hLabels): + row.append(QStandardItem()) + mdl.appendRow(row) + + # Data + data = root.find("data") + loadItem(data, mdl) + + return True + + +def loadItem(root, mdl, parent=QModelIndex()): + for row in root: + r = int(row.attrib["row"]) + for col in row: + c = int(col.attrib["col"]) + item = mdl.itemFromIndex(mdl.index(r, c, parent)) + if not item: + item = QStandardItem() + mdl.itemFromIndex(parent).setChild(r, c, item) + + if col.text: + # mdl.setData(mdl.index(r, c, parent), col.text) + item.setText(col.text) + + if "color" in col.attrib: + # mdl.itemFromIndex(mdl.index(r, c, parent)).setIcon(iconFromColorString(col.attrib["color"])) + item.setIcon(iconFromColorString(col.attrib["color"])) + + if len(col) != 0: + # loadItem(col, mdl, mdl.index(r, c, parent)) + loadItem(col, mdl, mdl.indexFromItem(item)) \ No newline at end of file diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py new file mode 100644 index 00000000..2760ea82 --- /dev/null +++ b/manuskript/load_save/version_1.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +# Version 1 of file saving format. +# Aims at providing a plain-text way of saving a project +# (except for some elements), allowing collaborative work +# versioning and third-partty editing. +import os +import zipfile + +from manuskript import settings +from manuskript.functions import mainWindow + +try: + import zlib # Used with zipfile for compression + + compression = zipfile.ZIP_DEFLATED +except: + compression = zipfile.ZIP_STORED + + +def saveProject(zip=None): + """ + Saves the project. If zip is False, the project is saved as a multitude of plain-text files for the most parts + and some XML or zip? for settings and stuff. + If zip is True, everything is saved as a single zipped file. Easier to carry around, but does not allow + collaborative work, versionning, or third-party editing. + @param zip: if True, saves as a single file. If False, saves as plain-text. If None, tries to determine based on + settings. + @return: Nothing + """ + if zip is None: + zip = False + # Fixme + + + files = [] + mw = mainWindow() + + # files.append((saveStandardItemModelXML(mw.mdlFlatData), + # "flatModel.xml")) + # # files.append((saveStandardItemModelXML(self.mdlCharacter), + # # "perso.xml")) + # files.append((saveStandardItemModelXML(mw.mdlWorld), + # "world.xml")) + # files.append((saveStandardItemModelXML(mw.mdlLabels), + # "labels.xml")) + # files.append((saveStandardItemModelXML(mw.mdlStatus), + # "status.xml")) + # files.append((saveStandardItemModelXML(mw.mdlPlots), + # "plots.xml")) + # files.append((mw.mdlOutline.saveToXML(), + # "outline.xml")) + # files.append((settings.save(), + # "settings.pickle")) + + files.append(("blabla", "test/machin.txt")) + files.append(("youpi", "encore/truc.txt")) + + project = mw.currentProject + + project = os.path.join( + os.path.dirname(project), + "_" + os.path.basename(project) + ) + + zf = zipfile.ZipFile(project, mode="w") + + for content, filename in files: + zf.writestr(filename, content, compress_type=compression) + + zf.close() + + +def loadProject(project): + """ + Loads a project. + @param project: the filename of the project to open. + @return: an array of errors, empty if None. + """ + pass \ No newline at end of file diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index 5c6511cc..b7fea969 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -11,9 +11,7 @@ from PyQt5.QtWidgets import QMainWindow, QHeaderView, qApp, QMenu, QActionGroup, from manuskript import settings from manuskript.enums import Character, Subplot, Plot, World from manuskript.functions import AUC, wordCount, appPath -from manuskript.loadSave import loadStandardItemModelXML, loadFilesFromZip -from manuskript.loadSave import saveFilesToZip -from manuskript.loadSave import saveStandardItemModelXML +from manuskript.loadSave import saveProject, loadProject from manuskript.models.characterModel import characterModel from manuskript.models.outlineModel import outlineModel from manuskript.models.plotModel import plotModel @@ -462,27 +460,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.currentProject = projectName QSettings().setValue("lastProject", projectName) - # Saving - files = [] - - files.append((saveStandardItemModelXML(self.mdlFlatData), - "flatModel.xml")) - # files.append((saveStandardItemModelXML(self.mdlCharacter), - # "perso.xml")) - files.append((saveStandardItemModelXML(self.mdlWorld), - "world.xml")) - files.append((saveStandardItemModelXML(self.mdlLabels), - "labels.xml")) - files.append((saveStandardItemModelXML(self.mdlStatus), - "status.xml")) - files.append((saveStandardItemModelXML(self.mdlPlots), - "plots.xml")) - files.append((self.mdlOutline.saveToXML(), - "outline.xml")) - files.append((settings.save(), - "settings.pickle")) - - saveFilesToZip(files, self.currentProject) + saveProject(version=0) # Giving some feedback print(self.tr("Project {} saved.").format(self.currentProject)) @@ -501,56 +479,8 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.mdlWorld = worldModel(self) def loadDatas(self, project): - # Loading - files = loadFilesFromZip(project) - errors = [] - - if "flatModel.xml" in files: - loadStandardItemModelXML(self.mdlFlatData, - files["flatModel.xml"], fromString=True) - else: - errors.append("flatModel.xml") - - if "perso.xml" in files: - loadStandardItemModelXML(self.mdlCharacter, - files["perso.xml"], fromString=True) - else: - errors.append("perso.xml") - - if "world.xml" in files: - loadStandardItemModelXML(self.mdlWorld, - files["world.xml"], fromString=True) - else: - errors.append("world.xml") - - if "labels.xml" in files: - loadStandardItemModelXML(self.mdlLabels, - files["labels.xml"], fromString=True) - else: - errors.append("perso.xml") - - if "status.xml" in files: - loadStandardItemModelXML(self.mdlStatus, - files["status.xml"], fromString=True) - else: - errors.append("perso.xml") - - if "plots.xml" in files: - loadStandardItemModelXML(self.mdlPlots, - files["plots.xml"], fromString=True) - else: - errors.append("perso.xml") - - if "outline.xml" in files: - self.mdlOutline.loadFromXML(files["outline.xml"], fromString=True) - else: - errors.append("perso.xml") - - if "settings.pickle" in files: - settings.load(files["settings.pickle"], fromString=True) - else: - errors.append("perso.xml") + errors = loadProject(project) # Giving some feedback if not errors: From 240700ca17bc9db7a445cbe85d7474d21db9e4dc Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Sat, 5 Mar 2016 00:34:50 +0100 Subject: [PATCH 08/33] Updates .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2e6ffc7e..abbe884a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ ExportTest icons/Numix .idea dist -build \ No newline at end of file +build +test-projects \ No newline at end of file From af089e8a6a94a40c3cee4e55f8e3fc404248c623 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Sat, 5 Mar 2016 00:35:14 +0100 Subject: [PATCH 09/33] Plain text file format in progress --- manuskript/load_save/version_1.py | 172 +++++++++++++++++++++++++----- manuskript/mainWindow.py | 2 +- manuskript/settings.py | 22 +++- 3 files changed, 162 insertions(+), 34 deletions(-) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index 2760ea82..ca6c986c 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -8,8 +8,11 @@ import os import zipfile +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QColor + from manuskript import settings -from manuskript.functions import mainWindow +from manuskript.functions import mainWindow, iconColor try: import zlib # Used with zipfile for compression @@ -19,6 +22,8 @@ except: compression = zipfile.ZIP_STORED +cache = {} + def saveProject(zip=None): """ Saves the project. If zip is False, the project is saved as a multitude of plain-text files for the most parts @@ -30,46 +35,153 @@ def saveProject(zip=None): @return: Nothing """ if zip is None: - zip = False + zip = True # Fixme files = [] mw = mainWindow() - # files.append((saveStandardItemModelXML(mw.mdlFlatData), - # "flatModel.xml")) - # # files.append((saveStandardItemModelXML(self.mdlCharacter), - # # "perso.xml")) - # files.append((saveStandardItemModelXML(mw.mdlWorld), - # "world.xml")) - # files.append((saveStandardItemModelXML(mw.mdlLabels), - # "labels.xml")) - # files.append((saveStandardItemModelXML(mw.mdlStatus), - # "status.xml")) - # files.append((saveStandardItemModelXML(mw.mdlPlots), - # "plots.xml")) - # files.append((mw.mdlOutline.saveToXML(), - # "outline.xml")) - # files.append((settings.save(), - # "settings.pickle")) + # General infos (book and author) + # Saved in plain text, in infos.txt - files.append(("blabla", "test/machin.txt")) - files.append(("youpi", "encore/truc.txt")) + path = "infos.txt" + content = "" + for name, col in [ + ("Title", 0), + ("Subtitle", 1), + ("Serie", 2), + ("Volume", 3), + ("Genre", 4), + ("License", 5), + ("Author", 6), + ("Email", 7), + ]: + val = mw.mdlFlatData.item(0, col).text().strip() + if val: + content += "{name}:{spaces}{value}\n".format( + name=name, + spaces=" " * (15 - len(name)), + value=val + ) + files.append((path, content)) + + # Summary + # In plain text, in summary.txt + + path = "summary.txt" + content = "" + for name, col in [ + ("Situation", 0), + ("Sentence", 1), + ("Paragraph", 2), + ("Page", 3), + ("Full", 4), + ]: + val = mw.mdlFlatData.item(1, col).text().strip() + val = "\n".join([" " * 13 + l for l in val.split("\n")])[13:] + if val: + content += "{name}:{spaces}{value}\n".format( + name=name, + spaces=" " * (12 - len(name)), + value=val + ) + files.append((path, content)) + + # Label & Status + # In plain text + + for mdl, path in [ + (mw.mdlStatus, "status.txt"), + (mw.mdlLabels, "labels.txt") + ]: + + content = "" + + # We skip the first row, which is empty and transparent + for i in range(1, mdl.rowCount()): + color = "" + if mdl.data(mdl.index(i, 0), Qt.DecorationRole) is not None: + color = iconColor(mdl.data(mdl.index(i, 0), Qt.DecorationRole)).name(QColor.HexRgb) + color = color if color != "#ff000000" else "#00000000" + + text = mdl.data(mdl.index(i, 0)) + + if text: + content += "{name}{color}\n".format( + name=text, + color= "" if color == "" else ":" + " " * (20 - len(text)) + color + ) + + files.append((path, content)) + + # Characters (self.mdlCharacter) + # In a character folder + + # TODO + + # Texts + # In an outline folder + + # TODO + + # World (mw.mdlWorld) + # Either in an XML file, or in lots of plain texts? + # More probably text, since there might be writing done in third-party. + + # TODO + + # Plots (mw.mdlPlots) + # Either in XML or lots of plain texts? + # More probably XML since there is not really a lot if writing to do (third-party) + + # TODO + + # Settings + # Saved in readable text (json) for easier versionning. But they mustn't be shared, it seems. + # Maybe include them only if zipped? + # Well, for now, we keep them here... + files.append(("settings.txt", settings.save(protocol=0))) project = mw.currentProject - project = os.path.join( - os.path.dirname(project), - "_" + os.path.basename(project) - ) + # Save to zip + if zip: + project = os.path.join( + os.path.dirname(project), + "_" + os.path.basename(project) + ) - zf = zipfile.ZipFile(project, mode="w") + zf = zipfile.ZipFile(project, mode="w") - for content, filename in files: - zf.writestr(filename, content, compress_type=compression) + for filename, content in files: + zf.writestr(filename, content, compress_type=compression) - zf.close() + zf.close() + + # Save to plain text + else: + dir = os.path.dirname(project) + folder = os.path.splitext(os.path.basename(project))[0] + print("Saving to folder", folder) + + for path, content in files: + filename = os.path.join(dir, folder, path) + os.makedirs(os.path.dirname(filename), exist_ok=True) + print("* Saving file", filename) + + # TODO: the first time it saves, it will overwrite everything, since it's not yet in cache. + # Or we have to cache while loading. + + if not path in cache or cache[path] != content: + print(" Not in cache or changed: we write") + mode = "w"+ ("b" if type(content) == bytes else "") + with open(filename, mode) as f: + f.write(content) + cache[path] = content + + else: + print(" In cache, and identical. Do nothing.") def loadProject(project): @@ -78,4 +190,8 @@ def loadProject(project): @param project: the filename of the project to open. @return: an array of errors, empty if None. """ + + # Don't forget to cache everything that is loaded + # In order to save only what has changed. + pass \ No newline at end of file diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index b7fea969..fb1ca49a 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -460,7 +460,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.currentProject = projectName QSettings().setValue("lastProject", projectName) - saveProject(version=0) + saveProject() # version=0 # Giving some feedback print(self.tr("Project {} saved.").format(self.currentProject)) diff --git a/manuskript/settings.py b/manuskript/settings.py index 4f342f3a..d03f6dec 100644 --- a/manuskript/settings.py +++ b/manuskript/settings.py @@ -1,12 +1,16 @@ # -*- coding: utf-8 -*- import collections +import json import pickle from PyQt5.QtWidgets import qApp from manuskript.enums import Outline +# TODO: move some/all of those settings to application settings and not project settings +# in order to allow a shared project between several writers + viewSettings = { "Tree": { "Icon": "Nothing", @@ -28,7 +32,8 @@ viewSettings = { "Background": "Nothing", }, } - + +# Application spellcheck = False dict = None corkSizeFactor = 100 @@ -81,7 +86,7 @@ frequencyAnalyzer = { "phraseMax": 5 } -def save(filename=None): +def save(filename=None, protocol=None): global spellcheck, dict, corkSliderFactor, viewSettings, corkSizeFactor, folderView, lastTab, openIndexes, \ autoSave, autoSaveDelay, saveOnQuit, autoSaveNoChanges, autoSaveNoChangesDelay, outlineViewColumns, \ @@ -107,7 +112,7 @@ def save(filename=None): "textEditor":textEditor, "revisions":revisions, "frequencyAnalyzer": frequencyAnalyzer - } + } #pp=pprint.PrettyPrinter(indent=4, compact=False) #print("Saving:") @@ -117,8 +122,15 @@ def save(filename=None): f = open(filename, "wb") pickle.dump(allSettings, f) else: - return pickle.dumps(allSettings) - + if protocol == 0: + # This looks stupid + # But a simple json.dumps with sort_keys will throw a TypeError + # because of unorderable types. + return json.dumps(json.loads(json.dumps(allSettings)), indent=4, sort_keys=True) + else: + return pickle.dumps(allSettings) + + def load(string, fromString=False): """Load settings from 'string'. 'string' is the filename of the pickle dump. If fromString=True, string is the data of the pickle dumps.""" From 3fd446dd1eb5dc3ce345d38ab1dae65e40b68b95 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Sat, 5 Mar 2016 12:55:56 +0100 Subject: [PATCH 10/33] Working on backward compatibility --- manuskript/load_save/version_0.py | 24 ++++++++++++-- manuskript/load_save/version_1.py | 51 +++++++++++++++++++++++++++-- manuskript/models/characterModel.py | 2 +- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/manuskript/load_save/version_0.py b/manuskript/load_save/version_0.py index 4023539e..1874b050 100644 --- a/manuskript/load_save/version_0.py +++ b/manuskript/load_save/version_0.py @@ -14,6 +14,7 @@ from lxml import etree as ET from manuskript import settings from manuskript.functions import iconColor, iconFromColorString, mainWindow +from manuskript.models.characterModel import Character try: import zlib # Used with zipfile for compression @@ -130,8 +131,7 @@ def loadProject(project): errors.append("flatModel.xml") if "perso.xml" in files: - loadStandardItemModelXML(mw.mdlCharacter, - files["perso.xml"], fromString=True) + loadStandardItemModelXMLForCharacters(mw.mdlCharacter, files["perso.xml"]) else: errors.append("perso.xml") @@ -246,4 +246,22 @@ def loadItem(root, mdl, parent=QModelIndex()): if len(col) != 0: # loadItem(col, mdl, mdl.index(r, c, parent)) - loadItem(col, mdl, mdl.indexFromItem(item)) \ No newline at end of file + loadItem(col, mdl, mdl.indexFromItem(item)) + + +def loadStandardItemModelXMLForCharacters(mdl, xml): + mdl = mainWindow().mdlCharacter + root = ET.fromstring(xml) + data = root.find("data") + for row in data: + char = Character(mdl) + for col in row: + c = int(col.attrib["col"]) + # Value + if col.text: + char._data[c] = col.text + # Color + if "color" in col.attrib: + char.setColor(QColor(col.attrib["color"])) + # TODO: infos + mdl.characters.append(char) \ No newline at end of file diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index ca6c986c..ede22ddb 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -12,6 +12,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor from manuskript import settings +from manuskript.enums import Character from manuskript.functions import mainWindow, iconColor try: @@ -24,6 +25,17 @@ except: cache = {} + +def formatMetaData(name, value, tabLength=10): + + # TODO: escape ":" in name + return "{name}:{spaces}{value}\n".format( + name=name, + spaces=" " * (tabLength - len(name)), + value=value + ) + + def saveProject(zip=None): """ Saves the project. If zip is False, the project is saved as a multitude of plain-text files for the most parts @@ -35,7 +47,7 @@ def saveProject(zip=None): @return: Nothing """ if zip is None: - zip = True + zip = False # Fixme @@ -118,7 +130,42 @@ def saveProject(zip=None): # Characters (self.mdlCharacter) # In a character folder - # TODO + path = os.path.join("characters", "{name}.txt") # Not sure wheter os.path allows this + _map = [ + (Character.name, "Name"), + (Character.ID, "ID"), + (Character.importance, "Importance"), + (Character.motivation, "Motivation"), + (Character.goal, "Goal"), + (Character.conflict, "Conflict"), + (Character.summarySentence, "Phrase Summary"), + (Character.summaryPara, "Paragraph Summary"), + (Character.summaryFull, "Full Summary"), + (Character.notes, "Notes"), + ] + mdl = mw.mdlCharacter + for c in mdl.characters: + content = "" + LENGTH = 20 + for m, name in _map: + val = mdl.data(c.index(m.value)).strip() + if val: + # Multiline formatting + if len(val.split("\n")) > 1: + val = "\n".join([" " * (LENGTH + 1) + l for l in val.split("\n")])[LENGTH + 1:] + + content += formatMetaData(name, val, LENGTH) + + for info in c.infos: + content += formatMetaData(info.description, info.value, LENGTH) + + name = "{ID}-{slugName}".format( + ID=c.ID(), + slugName="FIXME" + ) + files.append(( + path.format(name=name), + content)) # Texts # In an outline folder diff --git a/manuskript/models/characterModel.py b/manuskript/models/characterModel.py index a9881f94..0a922bd7 100644 --- a/manuskript/models/characterModel.py +++ b/manuskript/models/characterModel.py @@ -213,7 +213,7 @@ class characterModel(QAbstractItemModel): ############################################################################### class Character(): - def __init__(self, model, name): + def __init__(self, model, name="No name"): self._model = model self._data = {} From b45a32cfde41cee860cba96dcc8015a0d2fee7e5 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Sat, 5 Mar 2016 17:46:02 +0100 Subject: [PATCH 11/33] Saves World Model. Still work in progress, but a step closer. --- manuskript/load_save/version_0.py | 26 ++++++- manuskript/load_save/version_1.py | 114 ++++++++++++++++++++++------ manuskript/models/characterModel.py | 1 + 3 files changed, 117 insertions(+), 24 deletions(-) diff --git a/manuskript/load_save/version_0.py b/manuskript/load_save/version_0.py index 1874b050..8bc15fcf 100644 --- a/manuskript/load_save/version_0.py +++ b/manuskript/load_save/version_0.py @@ -14,7 +14,7 @@ from lxml import etree as ET from manuskript import settings from manuskript.functions import iconColor, iconFromColorString, mainWindow -from manuskript.models.characterModel import Character +from manuskript.models.characterModel import Character, CharacterInfo try: import zlib # Used with zipfile for compression @@ -250,18 +250,40 @@ def loadItem(root, mdl, parent=QModelIndex()): def loadStandardItemModelXMLForCharacters(mdl, xml): + """ + Loads a standardItemModel saved to XML by version 0, but for the new characterModel. + @param mdl: characterModel + @param xml: the content of the xml + @return: nothing + """ mdl = mainWindow().mdlCharacter root = ET.fromstring(xml) data = root.find("data") + for row in data: char = Character(mdl) + for col in row: c = int(col.attrib["col"]) + # Value if col.text: char._data[c] = col.text + # Color if "color" in col.attrib: char.setColor(QColor(col.attrib["color"])) - # TODO: infos + + # Infos + if len(col) != 0: + for rrow in col: + info = CharacterInfo(char) + for ccol in rrow: + cc = int(ccol.attrib["col"]) + if cc == 11 and ccol.text: + info.description = ccol.text + if cc == 12 and ccol.text: + info.value = ccol.text + char.infos.append(info) + mdl.characters.append(char) \ No newline at end of file diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index ede22ddb..095d5ae3 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -6,14 +6,17 @@ # (except for some elements), allowing collaborative work # versioning and third-partty editing. import os +import string import zipfile -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QModelIndex from PyQt5.QtGui import QColor from manuskript import settings -from manuskript.enums import Character +from manuskript.enums import Character, World from manuskript.functions import mainWindow, iconColor +from lxml import etree as ET + try: import zlib # Used with zipfile for compression @@ -28,7 +31,17 @@ cache = {} def formatMetaData(name, value, tabLength=10): - # TODO: escape ":" in name + # Multiline formatting + if len(value.split("\n")) > 1: + value = "\n".join([" " * (tabLength + 1) + l for l in value.split("\n")])[tabLength + 1:] + + # Avoid empty description (don't know how much MMD loves that) + if name == "": + name = "None" + + # Escapes ":" in name + name = name.replace(":", "_.._") + return "{name}:{spaces}{value}\n".format( name=name, spaces=" " * (tabLength - len(name)), @@ -36,6 +49,23 @@ def formatMetaData(name, value, tabLength=10): ) +def slugify(name): + """ + A basic slug function, that escapes all spaces to "_" and all non letters/digits to "-". + @param name: name to slugify (str) + @return: str + """ + valid = string.ascii_letters + string.digits + newName = "" + for c in name: + if c in valid: + newName += c + elif c in string.whitespace: + newName += "_" + else: + newName += "-" + return newName + def saveProject(zip=None): """ Saves the project. If zip is False, the project is saved as a multitude of plain-text files for the most parts @@ -50,8 +80,11 @@ def saveProject(zip=None): zip = False # Fixme - + # List of files to be written files = [] + # List of files to be removed + removes = [] + mw = mainWindow() # General infos (book and author) @@ -91,13 +124,9 @@ def saveProject(zip=None): ("Full", 4), ]: val = mw.mdlFlatData.item(1, col).text().strip() - val = "\n".join([" " * 13 + l for l in val.split("\n")])[13:] if val: - content += "{name}:{spaces}{value}\n".format( - name=name, - spaces=" " * (12 - len(name)), - value=val - ) + content += formatMetaData(name, val, 12) + files.append((path, content)) # Label & Status @@ -146,25 +175,23 @@ def saveProject(zip=None): mdl = mw.mdlCharacter for c in mdl.characters: content = "" - LENGTH = 20 for m, name in _map: val = mdl.data(c.index(m.value)).strip() if val: - # Multiline formatting - if len(val.split("\n")) > 1: - val = "\n".join([" " * (LENGTH + 1) + l for l in val.split("\n")])[LENGTH + 1:] - - content += formatMetaData(name, val, LENGTH) + content += formatMetaData(name, val, 20) for info in c.infos: - content += formatMetaData(info.description, info.value, LENGTH) + content += formatMetaData(info.description, info.value, 20) - name = "{ID}-{slugName}".format( + cpath = path.format(name="{ID}-{slugName}".format( ID=c.ID(), - slugName="FIXME" - ) + slugName=slugify(c.name()) + )) + if c.lastPath and cpath != c.lastPath: + removes.append(c.lastPath) + c.lastPath = cpath files.append(( - path.format(name=name), + cpath, content)) # Texts @@ -176,7 +203,15 @@ def saveProject(zip=None): # Either in an XML file, or in lots of plain texts? # More probably text, since there might be writing done in third-party. - # TODO + path = "world.opml" + mdl = mw.mdlWorld + + root = ET.Element("opml") + root.attrib["version"] = "1.0" + body = ET.SubElement(root, "body") + addWorldItem(body, mdl) + content = ET.tostring(root, encoding="UTF-8", xml_declaration=True, pretty_print=True) + files.append((path, content)) # Plots (mw.mdlPlots) # Either in XML or lots of plain texts? @@ -212,6 +247,13 @@ def saveProject(zip=None): folder = os.path.splitext(os.path.basename(project))[0] print("Saving to folder", folder) + for path in removes: + if path not in [p for p,c in files]: + filename = os.path.join(dir, folder, path) + print("* Removing", filename) + os.remove(filename) + cache.pop(path) + for path, content in files: filename = os.path.join(dir, folder, path) os.makedirs(os.path.dirname(filename), exist_ok=True) @@ -230,6 +272,34 @@ def saveProject(zip=None): else: print(" In cache, and identical. Do nothing.") +def addWorldItem(root, mdl, parent=QModelIndex()): + """ + Lists elements in a world model and create an OPML xml file. + @param root: an Etree element + @param mdl: a worldModel + @param parent: the parent index in the world model + @return: root, to which sub element have been added + """ + # List every row (every world item) + for x in range(mdl.rowCount(parent)): + + # For each row, create an outline item. + outline = ET.SubElement(root, "outline") + for y in range(mdl.columnCount(parent)): + + val = mdl.data(mdl.index(x, y, parent)) + + if not val: + continue + + for w in World: + if y == w.value: + outline.attrib[w.name] = val + + if mdl.hasChildren(mdl.index(x, y, parent)): + addWorldItem(outline, mdl, mdl.index(x, y, parent)) + + return root def loadProject(project): """ diff --git a/manuskript/models/characterModel.py b/manuskript/models/characterModel.py index 0a922bd7..84133d96 100644 --- a/manuskript/models/characterModel.py +++ b/manuskript/models/characterModel.py @@ -215,6 +215,7 @@ class characterModel(QAbstractItemModel): class Character(): def __init__(self, model, name="No name"): self._model = model + self.lastPath = "" self._data = {} self._data[C.name.value] = name From fca0e9679f15b26c539b027fb4ddfc128a99520a Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Sat, 5 Mar 2016 17:53:25 +0100 Subject: [PATCH 12/33] Corrects a potential segfault --- manuskript/models/characterModel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manuskript/models/characterModel.py b/manuskript/models/characterModel.py index 84133d96..fce95f4b 100644 --- a/manuskript/models/characterModel.py +++ b/manuskript/models/characterModel.py @@ -33,6 +33,9 @@ class characterModel(QAbstractItemModel): return 1 def data(self, index, role=Qt.DisplayRole): + if not index.isValid(): + return None + c = index.internalPointer() if type(c) == Character: if role == Qt.DisplayRole: From 65579dbd6e725bdd82a595f964db233b2987d644 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Sat, 5 Mar 2016 18:20:39 +0100 Subject: [PATCH 13/33] Updates makefile for gdb --- makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makefile b/makefile index f003a312..46e7e6e3 100644 --- a/makefile +++ b/makefile @@ -10,7 +10,7 @@ run: $(UIs) bin/manuskript debug: $(UIs) - gdb --args python3 manuskript/main.py + gdb --args python3 bin/manuskript lineprof: kernprof -l -v manuskript/main.py From e824f7c9256aab2e18dce0c9538e53c8dd392003 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Sat, 5 Mar 2016 18:28:29 +0100 Subject: [PATCH 14/33] =?UTF-8?q?Refactoring=20outlinePersoDelegate=20?= =?UTF-8?q?=E2=86=92=20outlineCharacterDelegate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- i18n/manuskript_fr.ts | 2 +- manuskript/mainWindow.py | 4 ++-- manuskript/ui/views/outlineDelegates.py | 2 +- manuskript/ui/views/outlineView.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/i18n/manuskript_fr.ts b/i18n/manuskript_fr.ts index d4e27417..6a308d39 100644 --- a/i18n/manuskript_fr.ts +++ b/i18n/manuskript_fr.ts @@ -1669,7 +1669,7 @@ des lignes: - outlinePersoDelegate + outlineCharacterDelegate None diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index fb1ca49a..143794cb 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -22,7 +22,7 @@ from manuskript.ui.compileDialog import compileDialog from manuskript.ui.helpLabel import helpLabel from manuskript.ui.mainWindow import Ui_MainWindow from manuskript.ui.tools.frequencyAnalyzer import frequencyAnalyzer -from manuskript.ui.views.outlineDelegates import outlinePersoDelegate +from manuskript.ui.views.outlineDelegates import outlineCharacterDelegate from manuskript.ui.views.plotDelegate import plotDelegate # Spellcheck support @@ -605,7 +605,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.mdlCharacter.dataChanged.connect(self.mdlPlots.updatePlotPersoButton) self.lstOutlinePlots.setPlotModel(self.mdlPlots) self.lstOutlinePlots.setShowSubPlot(True) - self.plotPersoDelegate = outlinePersoDelegate(self.mdlCharacter, self) + self.plotPersoDelegate = outlineCharacterDelegate(self.mdlCharacter, self) self.lstPlotPerso.setItemDelegate(self.plotPersoDelegate) self.plotDelegate = plotDelegate(self) self.lstSubPlots.setItemDelegateForColumn(Subplot.meta.value, self.plotDelegate) diff --git a/manuskript/ui/views/outlineDelegates.py b/manuskript/ui/views/outlineDelegates.py index 1172f766..383d2cf1 100644 --- a/manuskript/ui/views/outlineDelegates.py +++ b/manuskript/ui/views/outlineDelegates.py @@ -92,7 +92,7 @@ class outlineTitleDelegate(QStyledItemDelegate): # QStyledItemDelegate.paint(self, painter, option, index) -class outlinePersoDelegate(QStyledItemDelegate): +class outlineCharacterDelegate(QStyledItemDelegate): def __init__(self, mdlCharacter, parent=None): QStyledItemDelegate.__init__(self, parent) self.mdlCharacter = mdlCharacter diff --git a/manuskript/ui/views/outlineView.py b/manuskript/ui/views/outlineView.py index 670894f2..6bb55a27 100644 --- a/manuskript/ui/views/outlineView.py +++ b/manuskript/ui/views/outlineView.py @@ -6,7 +6,7 @@ from manuskript import settings from manuskript.enums import Outline from manuskript.ui.views.dndView import dndView from manuskript.ui.views.outlineBasics import outlineBasics -from manuskript.ui.views.outlineDelegates import outlineTitleDelegate, outlinePersoDelegate, outlineCompileDelegate, \ +from manuskript.ui.views.outlineDelegates import outlineTitleDelegate, outlineCharacterDelegate, outlineCompileDelegate, \ outlineStatusDelegate, outlineGoalPercentageDelegate, outlineLabelDelegate @@ -23,7 +23,7 @@ class outlineView(QTreeView, dndView, outlineBasics): self.header().setStretchLastSection(False) def setModelCharacters(self, model): - # This is used by outlinePersoDelegate to select character + # This is used by outlineCharacterDelegate to select character self.modelCharacters = model def setModelLabels(self, model): @@ -41,7 +41,7 @@ class outlineView(QTreeView, dndView, outlineBasics): self.outlineTitleDelegate = outlineTitleDelegate(self) # self.outlineTitleDelegate.setView(self) self.setItemDelegateForColumn(Outline.title.value, self.outlineTitleDelegate) - self.outlinePersoDelegate = outlinePersoDelegate(self.modelCharacters) + self.outlinePersoDelegate = outlineCharacterDelegate(self.modelCharacters) self.setItemDelegateForColumn(Outline.POV.value, self.outlinePersoDelegate) self.outlineCompileDelegate = outlineCompileDelegate() self.setItemDelegateForColumn(Outline.compile.value, self.outlineCompileDelegate) From e6913849de235a66c9bb1b68fd0cc53ef84d5f2a Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Sun, 6 Mar 2016 08:47:51 +0100 Subject: [PATCH 15/33] Eradicating 'persos' --- manuskript/ui/views/outlineView.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manuskript/ui/views/outlineView.py b/manuskript/ui/views/outlineView.py index 6bb55a27..50e44ecc 100644 --- a/manuskript/ui/views/outlineView.py +++ b/manuskript/ui/views/outlineView.py @@ -41,8 +41,8 @@ class outlineView(QTreeView, dndView, outlineBasics): self.outlineTitleDelegate = outlineTitleDelegate(self) # self.outlineTitleDelegate.setView(self) self.setItemDelegateForColumn(Outline.title.value, self.outlineTitleDelegate) - self.outlinePersoDelegate = outlineCharacterDelegate(self.modelCharacters) - self.setItemDelegateForColumn(Outline.POV.value, self.outlinePersoDelegate) + self.outlineCharacterDelegate = outlineCharacterDelegate(self.modelCharacters) + self.setItemDelegateForColumn(Outline.POV.value, self.outlineCharacterDelegate) self.outlineCompileDelegate = outlineCompileDelegate() self.setItemDelegateForColumn(Outline.compile.value, self.outlineCompileDelegate) self.outlineStatusDelegate = outlineStatusDelegate(self.modelStatus) From 50dc7f37395cd84469af1e3a11b71147f8f4d9c4 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Sun, 6 Mar 2016 09:21:10 +0100 Subject: [PATCH 16/33] Tracking segfault, hopefuly done now --- manuskript/mainWindow.py | 4 ++-- manuskript/models/characterModel.py | 5 +---- manuskript/ui/views/outlineDelegates.py | 21 ++++++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index 143794cb..630d6856 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -605,8 +605,8 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.mdlCharacter.dataChanged.connect(self.mdlPlots.updatePlotPersoButton) self.lstOutlinePlots.setPlotModel(self.mdlPlots) self.lstOutlinePlots.setShowSubPlot(True) - self.plotPersoDelegate = outlineCharacterDelegate(self.mdlCharacter, self) - self.lstPlotPerso.setItemDelegate(self.plotPersoDelegate) + self.plotCharacterDelegate = outlineCharacterDelegate(self.mdlCharacter, self) + self.lstPlotPerso.setItemDelegate(self.plotCharacterDelegate) self.plotDelegate = plotDelegate(self) self.lstSubPlots.setItemDelegateForColumn(Subplot.meta.value, self.plotDelegate) diff --git a/manuskript/models/characterModel.py b/manuskript/models/characterModel.py index fce95f4b..e1a4e684 100644 --- a/manuskript/models/characterModel.py +++ b/manuskript/models/characterModel.py @@ -30,12 +30,9 @@ class characterModel(QAbstractItemModel): # Returns characters infos return 2 else: - return 1 + return len(C) def data(self, index, role=Qt.DisplayRole): - if not index.isValid(): - return None - c = index.internalPointer() if type(c) == Character: if role == Qt.DisplayRole: diff --git a/manuskript/ui/views/outlineDelegates.py b/manuskript/ui/views/outlineDelegates.py index 383d2cf1..d091070f 100644 --- a/manuskript/ui/views/outlineDelegates.py +++ b/manuskript/ui/views/outlineDelegates.py @@ -101,9 +101,9 @@ class outlineCharacterDelegate(QStyledItemDelegate): # s = QStyledItemDelegate.sizeHint(self, option, index) item = QModelIndex() - for i in range(self.mdlCharacter.rowCount()): - if self.mdlCharacter.ID(i) == index.data(): - item = self.mdlCharacter.index(i, Character.name.value) + character = self.mdlCharacter.getCharacterByID(index.data()) + if character: + item = character.index(Character.name.value) opt = QStyleOptionViewItem(option) self.initStyleOption(opt, item) @@ -158,18 +158,21 @@ class outlineCharacterDelegate(QStyledItemDelegate): # QStyledItemDelegate.paint(self, painter, option, index) ##option.rect.setWidth(option.rect.width() + 18) - item = QModelIndex() - for i in range(self.mdlCharacter.rowCount()): - if self.mdlCharacter.ID(i) == index.data(): - item = self.mdlCharacter.index(i, Character.name.value) + itemIndex = QModelIndex() + character = self.mdlCharacter.getCharacterByID(index.data()) + if character: + itemIndex = character.index(Character.name.value) + else: + # Character ID not found in character model. + return opt = QStyleOptionViewItem(option) - self.initStyleOption(opt, item) + self.initStyleOption(opt, itemIndex) qApp.style().drawControl(QStyle.CE_ItemViewItem, opt, painter) # if index.isValid() and index.internalPointer().data(Outline.POV.value) not in ["", None]: - if index.isValid() and self.mdlCharacter.data(index) not in ["", None]: + if itemIndex.isValid() and self.mdlCharacter.data(itemIndex) not in ["", None]: opt = QStyleOptionComboBox() opt.rect = option.rect r = qApp.style().subControlRect(QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxArrow) From 599a60ecff74372929a3c297a7ebaf8606f30687 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Sun, 6 Mar 2016 09:26:59 +0100 Subject: [PATCH 17/33] Changes some more 'persos' to 'characters' --- manuskript/enums.py | 3 +- manuskript/load_save/version_1.py | 58 +++++++++++++++++++++++++++++-- manuskript/mainWindow.py | 4 +-- manuskript/models/plotModel.py | 6 ++-- manuskript/models/references.py | 2 +- 5 files changed, 63 insertions(+), 10 deletions(-) diff --git a/manuskript/enums.py b/manuskript/enums.py index 591812e0..1368dfdc 100644 --- a/manuskript/enums.py +++ b/manuskript/enums.py @@ -26,7 +26,7 @@ class Plot(Enum): name = 0 ID = 1 importance = 2 - persos = 3 + characters = 3 description = 4 result = 5 subplots = 6 @@ -64,4 +64,3 @@ class Outline(Enum): # (sum of all sub-items' goals) textFormat = 15 revisions = 16 - diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index 095d5ae3..19b7e627 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -13,7 +13,7 @@ from PyQt5.QtCore import Qt, QModelIndex from PyQt5.QtGui import QColor from manuskript import settings -from manuskript.enums import Character, World +from manuskript.enums import Character, World, Plot from manuskript.functions import mainWindow, iconColor from lxml import etree as ET @@ -217,7 +217,15 @@ def saveProject(zip=None): # Either in XML or lots of plain texts? # More probably XML since there is not really a lot if writing to do (third-party) - # TODO + path = "plots.opml" + mdl = mw.mdlPlots + + root = ET.Element("opml") + root.attrib["version"] = "1.0" + body = ET.SubElement(root, "body") + addPlotItem(body, mdl) + content = ET.tostring(root, encoding="UTF-8", xml_declaration=True, pretty_print=True) + files.append((path, content)) # Settings # Saved in readable text (json) for easier versionning. But they mustn't be shared, it seems. @@ -301,6 +309,52 @@ def addWorldItem(root, mdl, parent=QModelIndex()): return root + +def addPlotItem(root, mdl, parent=QModelIndex()): + """ + Lists elements in a plot model and create an OPML xml file. + @param root: an Etree element + @param mdl: a plotModel + @param parent: the parent index in the plot model + @return: root, to which sub element have been added + """ + + # List every row (every plot item) + for x in range(mdl.rowCount(parent)): + + # For each row, create an outline item. + outline = ET.SubElement(root, "outline") + for y in range(mdl.columnCount(parent)): + + index = mdl.index(x, y, parent) + val = mdl.data(index) + + if not val: + continue + + for w in Plot: + if y == w.value: + outline.attrib[w.name] = val + + if y == Plot.characters.value: + if mdl.hasChildren(index): + characters = [] + for cX in range(mdl.rowCount(index)): + for cY in range(mdl.columnCount(index)): + cIndex = mdl.index(cX, cY, index) + characters.append(mdl.data(cIndex)) + outline.attrib[Plot.characters.name] = ",".join(characters) + else: + outline.attrib.pop(Plot.characters.name) + + elif y == Plot.subplots.value and mdl.hasChildren(index): + outline.attrib.pop(Plot.subplots.name) + + + # addWorldItem(outline, mdl, index) + + return root + def loadProject(project): """ Loads a project. diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index 630d6856..b01edd22 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -205,7 +205,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.txtPlotResult.setCurrentModelIndex(index) self.sldPlotImportance.setCurrentModelIndex(index) self.lstPlotPerso.setRootIndex(index.sibling(index.row(), - Plot.persos.value)) + Plot.characters.value)) subplotindex = index.sibling(index.row(), Plot.subplots.value) self.lstSubPlots.setRootIndex(subplotindex) if self.mdlPlots.rowCount(subplotindex): @@ -678,7 +678,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.tblDebugPlots.selectionModel().currentChanged.connect( lambda: self.tblDebugPlotsPersos.setRootIndex(self.mdlPlots.index( self.tblDebugPlots.selectionModel().currentIndex().row(), - Plot.persos.value)), AUC) + Plot.characters.value)), AUC) self.tblDebugPlots.selectionModel().currentChanged.connect( lambda: self.tblDebugSubPlots.setRootIndex(self.mdlPlots.index( self.tblDebugPlots.selectionModel().currentIndex().row(), diff --git a/manuskript/models/plotModel.py b/manuskript/models/plotModel.py index b7f4949f..6b272af8 100644 --- a/manuskript/models/plotModel.py +++ b/manuskript/models/plotModel.py @@ -182,10 +182,10 @@ class plotModel(QStandardItemModel): def addPlotPerso(self, v): index = self.mw.lstPlots.currentPlotIndex() if index.isValid(): - if not self.item(index.row(), Plot.persos.value): - self.setItem(index.row(), Plot.persos.value, QStandardItem()) + if not self.item(index.row(), Plot.characters.value): + self.setItem(index.row(), Plot.characters.value, QStandardItem()) - item = self.item(index.row(), Plot.persos.value) + item = self.item(index.row(), Plot.characters.value) # We check that the PersoID is not in the list yet for i in range(item.rowCount()): diff --git a/manuskript/models/references.py b/manuskript/models/references.py index 64261d7e..8c6b86a1 100644 --- a/manuskript/models/references.py +++ b/manuskript/models/references.py @@ -277,7 +277,7 @@ def infos(ref): # Characters pM = mainWindow().mdlCharacter - item = m.item(index.row(), Plot.persos.value) + item = m.item(index.row(), Plot.characters.value) characters = "" if item: for r in range(item.rowCount()): From 8949d7b8e31c3cab679b2ce224aa26fc85e4f5f0 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Sun, 6 Mar 2016 16:10:25 +0100 Subject: [PATCH 18/33] Changes subplots to steps --- manuskript/enums.py | 4 ++-- manuskript/load_save/version_1.py | 4 ++-- manuskript/mainWindow.py | 20 ++++++++++---------- manuskript/models/plotModel.py | 22 +++++++++++----------- manuskript/models/references.py | 10 +++++----- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/manuskript/enums.py b/manuskript/enums.py index 1368dfdc..661915f0 100644 --- a/manuskript/enums.py +++ b/manuskript/enums.py @@ -29,10 +29,10 @@ class Plot(Enum): characters = 3 description = 4 result = 5 - subplots = 6 + steps = 6 summary = 7 -class Subplot(Enum): +class PlotStep(Enum): name = 0 ID = 1 meta = 2 diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index 19b7e627..3dbddc21 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -347,8 +347,8 @@ def addPlotItem(root, mdl, parent=QModelIndex()): else: outline.attrib.pop(Plot.characters.name) - elif y == Plot.subplots.value and mdl.hasChildren(index): - outline.attrib.pop(Plot.subplots.name) + elif y == Plot.steps.value and mdl.hasChildren(index): + outline.attrib.pop(Plot.steps.name) # addWorldItem(outline, mdl, index) diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index b01edd22..401b10cc 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -9,7 +9,7 @@ from PyQt5.QtWidgets import QMainWindow, QHeaderView, qApp, QMenu, QActionGroup, QLabel from manuskript import settings -from manuskript.enums import Character, Subplot, Plot, World +from manuskript.enums import Character, PlotStep, Plot, World from manuskript.functions import AUC, wordCount, appPath from manuskript.loadSave import saveProject, loadProject from manuskript.models.characterModel import characterModel @@ -206,7 +206,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.sldPlotImportance.setCurrentModelIndex(index) self.lstPlotPerso.setRootIndex(index.sibling(index.row(), Plot.characters.value)) - subplotindex = index.sibling(index.row(), Plot.subplots.value) + subplotindex = index.sibling(index.row(), Plot.steps.value) self.lstSubPlots.setRootIndex(subplotindex) if self.mdlPlots.rowCount(subplotindex): self.updateSubPlotView() @@ -222,18 +222,18 @@ class MainWindow(QMainWindow, Ui_MainWindow): # Hide columns for i in range(self.mdlPlots.columnCount()): self.lstSubPlots.hideColumn(i) - self.lstSubPlots.showColumn(Subplot.name.value) - self.lstSubPlots.showColumn(Subplot.meta.value) + self.lstSubPlots.showColumn(PlotStep.name.value) + self.lstSubPlots.showColumn(PlotStep.meta.value) self.lstSubPlots.horizontalHeader().setSectionResizeMode( - Subplot.name.value, QHeaderView.Stretch) + PlotStep.name.value, QHeaderView.Stretch) self.lstSubPlots.horizontalHeader().setSectionResizeMode( - Subplot.meta.value, QHeaderView.ResizeToContents) + PlotStep.meta.value, QHeaderView.ResizeToContents) self.lstSubPlots.verticalHeader().hide() def changeCurrentSubPlot(self, index): # Got segfaults when using textEditView model system, so ad hoc stuff. - index = index.sibling(index.row(), Subplot.summary.value) + index = index.sibling(index.row(), PlotStep.summary.value) item = self.mdlPlots.itemFromIndex(index) if not item: self.txtSubPlotSummary.setEnabled(False) @@ -251,7 +251,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): index = self.lstSubPlots.currentIndex() if not index.isValid(): return - index = index.sibling(index.row(), Subplot.summary.value) + index = index.sibling(index.row(), PlotStep.summary.value) item = self.mdlPlots.itemFromIndex(index) self._updatingSubPlot = True @@ -608,7 +608,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.plotCharacterDelegate = outlineCharacterDelegate(self.mdlCharacter, self) self.lstPlotPerso.setItemDelegate(self.plotCharacterDelegate) self.plotDelegate = plotDelegate(self) - self.lstSubPlots.setItemDelegateForColumn(Subplot.meta.value, self.plotDelegate) + self.lstSubPlots.setItemDelegateForColumn(PlotStep.meta.value, self.plotDelegate) # World self.treeWorld.setModel(self.mdlWorld) @@ -682,7 +682,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.tblDebugPlots.selectionModel().currentChanged.connect( lambda: self.tblDebugSubPlots.setRootIndex(self.mdlPlots.index( self.tblDebugPlots.selectionModel().currentIndex().row(), - Plot.subplots.value)), AUC) + Plot.steps.value)), AUC) self.treeDebugWorld.setModel(self.mdlWorld) self.treeDebugOutline.setModel(self.mdlOutline) self.lstDebugLabels.setModel(self.mdlLabels) diff --git a/manuskript/models/plotModel.py b/manuskript/models/plotModel.py index 6b272af8..5ac706f5 100644 --- a/manuskript/models/plotModel.py +++ b/manuskript/models/plotModel.py @@ -9,7 +9,7 @@ from PyQt5.QtGui import QStandardItemModel from PyQt5.QtWidgets import QAction, QMenu from manuskript.enums import Plot -from manuskript.enums import Subplot +from manuskript.enums import PlotStep from manuskript.functions import toInt, mainWindow @@ -37,7 +37,7 @@ class plotModel(QStandardItemModel): index = self.getIndexFromID(ID) if not index.isValid(): return - index = index.sibling(index.row(), Plot.subplots.value) + index = index.sibling(index.row(), Plot.steps.value) item = self.itemFromIndex(index) lst = [] for i in range(item.rowCount()): @@ -86,8 +86,8 @@ class plotModel(QStandardItemModel): p = QStandardItem(self.tr("New plot")) _id = QStandardItem(self.getUniqueID()) importance = QStandardItem(str(0)) - self.appendRow([p, _id, importance, QStandardItem("Persos"), - QStandardItem(), QStandardItem(), QStandardItem("Subplots")]) + self.appendRow([p, _id, importance, QStandardItem("Characters"), + QStandardItem(), QStandardItem(), QStandardItem("Resolution steps")]) def getUniqueID(self, parent=QModelIndex()): """Returns an unused ID""" @@ -114,9 +114,9 @@ class plotModel(QStandardItemModel): def headerData(self, section, orientation, role=Qt.DisplayRole): if role == Qt.DisplayRole: if orientation == Qt.Horizontal: - if section == Subplot.name.value: + if section == PlotStep.name.value: return self.tr("Name") - elif section == Subplot.meta.value: + elif section == PlotStep.meta.value: return self.tr("Meta") else: return "" @@ -127,8 +127,8 @@ class plotModel(QStandardItemModel): def data(self, index, role=Qt.DisplayRole): if index.parent().isValid() and \ - index.parent().column() == Plot.subplots.value and \ - index.column() == Subplot.meta.value: + index.parent().column() == Plot.steps.value and \ + index.column() == PlotStep.meta.value: if role == Qt.TextAlignmentRole: return Qt.AlignRight | Qt.AlignVCenter elif role == Qt.ForegroundRole: @@ -144,13 +144,13 @@ class plotModel(QStandardItemModel): if not index.isValid(): return - parent = index.sibling(index.row(), Plot.subplots.value) - parentItem = self.item(index.row(), Plot.subplots.value) + parent = index.sibling(index.row(), Plot.steps.value) + parentItem = self.item(index.row(), Plot.steps.value) if not parentItem: return - p = QStandardItem(self.tr("New subplot")) + p = QStandardItem(self.tr("New step")) _id = QStandardItem(self.getUniqueID(parent)) summary = QStandardItem() diff --git a/manuskript/models/references.py b/manuskript/models/references.py index 8c6b86a1..a8a86cb7 100644 --- a/manuskript/models/references.py +++ b/manuskript/models/references.py @@ -13,7 +13,7 @@ from PyQt5.QtWidgets import qApp from manuskript.enums import Outline from manuskript.enums import Character from manuskript.enums import Plot -from manuskript.enums import Subplot +from manuskript.enums import PlotStep from manuskript.functions import mainWindow RegEx = r"{(\w):(\d+):?.*?}" @@ -288,12 +288,12 @@ def infos(ref): # Resolution steps steps = "" - item = m.item(index.row(), Plot.subplots.value) + item = m.item(index.row(), Plot.steps.value) if item: for r in range(item.rowCount()): - title = item.child(r, Subplot.name.value).text() - summary = item.child(r, Subplot.summary.value).text() - meta = item.child(r, Subplot.meta.value).text() + title = item.child(r, PlotStep.name.value).text() + summary = item.child(r, PlotStep.summary.value).text() + meta = item.child(r, PlotStep.meta.value).text() if meta: meta = " ({})".format(meta) steps += "
  • {title}{summary}{meta}
  • ".format( From c31681a724f64038a0d4d1aa1638c30eebe80758 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Sun, 6 Mar 2016 16:27:03 +0100 Subject: [PATCH 19/33] Saves plots, correctly. --- manuskript/load_save/version_1.py | 42 +++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index 3dbddc21..6e582914 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -13,7 +13,7 @@ from PyQt5.QtCore import Qt, QModelIndex from PyQt5.QtGui import QColor from manuskript import settings -from manuskript.enums import Character, World, Plot +from manuskript.enums import Character, World, Plot, PlotStep from manuskript.functions import mainWindow, iconColor from lxml import etree as ET @@ -25,7 +25,6 @@ try: except: compression = zipfile.ZIP_STORED - cache = {} @@ -66,6 +65,7 @@ def slugify(name): newName += "-" return newName + def saveProject(zip=None): """ Saves the project. If zip is False, the project is saved as a multitude of plain-text files for the most parts @@ -87,6 +87,10 @@ def saveProject(zip=None): mw = mainWindow() + if zip: + # File format version + files.append(("VERSION", "1")) + # General infos (book and author) # Saved in plain text, in infos.txt @@ -187,6 +191,8 @@ def saveProject(zip=None): ID=c.ID(), slugName=slugify(c.name()) )) + # Has the character been renamed? + # If so, we remove the old file (if not zipped) if c.lastPath and cpath != c.lastPath: removes.append(c.lastPath) c.lastPath = cpath @@ -217,13 +223,11 @@ def saveProject(zip=None): # Either in XML or lots of plain texts? # More probably XML since there is not really a lot if writing to do (third-party) - path = "plots.opml" + path = "plots.xml" mdl = mw.mdlPlots - root = ET.Element("opml") - root.attrib["version"] = "1.0" - body = ET.SubElement(root, "body") - addPlotItem(body, mdl) + root = ET.Element("root") + addPlotItem(root, mdl) content = ET.tostring(root, encoding="UTF-8", xml_declaration=True, pretty_print=True) files.append((path, content)) @@ -280,6 +284,7 @@ def saveProject(zip=None): else: print(" In cache, and identical. Do nothing.") + def addWorldItem(root, mdl, parent=QModelIndex()): """ Lists elements in a world model and create an OPML xml file. @@ -312,7 +317,7 @@ def addWorldItem(root, mdl, parent=QModelIndex()): def addPlotItem(root, mdl, parent=QModelIndex()): """ - Lists elements in a plot model and create an OPML xml file. + Lists elements in a plot model and create an xml file. @param root: an Etree element @param mdl: a plotModel @param parent: the parent index in the plot model @@ -323,7 +328,7 @@ def addPlotItem(root, mdl, parent=QModelIndex()): for x in range(mdl.rowCount(parent)): # For each row, create an outline item. - outline = ET.SubElement(root, "outline") + outline = ET.SubElement(root, "plot") for y in range(mdl.columnCount(parent)): index = mdl.index(x, y, parent) @@ -336,6 +341,7 @@ def addPlotItem(root, mdl, parent=QModelIndex()): if y == w.value: outline.attrib[w.name] = val + # List characters as attrib if y == Plot.characters.value: if mdl.hasChildren(index): characters = [] @@ -347,14 +353,24 @@ def addPlotItem(root, mdl, parent=QModelIndex()): else: outline.attrib.pop(Plot.characters.name) - elif y == Plot.steps.value and mdl.hasChildren(index): + # List resolution steps as sub items + elif y == Plot.steps.value: + if mdl.hasChildren(index): + for cX in range(mdl.rowCount(index)): + step = ET.SubElement(outline, "step") + for cY in range(mdl.columnCount(index)): + cIndex = mdl.index(cX, cY, index) + val = mdl.data(cIndex) + + for w in PlotStep: + if cY == w.value: + step.attrib[w.name] = val + outline.attrib.pop(Plot.steps.name) - - # addWorldItem(outline, mdl, index) - return root + def loadProject(project): """ Loads a project. From 05715a26e048294f2122e7f0ebb8ecc8fcf1c09f Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Tue, 8 Mar 2016 09:21:44 +0100 Subject: [PATCH 20/33] Writes outline --- manuskript/loadSave.py | 26 +++++++++- manuskript/load_save/version_0.py | 18 +++---- manuskript/load_save/version_1.py | 84 +++++++++++++++++++++++++++++-- manuskript/models/outlineModel.py | 9 +++- 4 files changed, 122 insertions(+), 15 deletions(-) diff --git a/manuskript/loadSave.py b/manuskript/loadSave.py index 1476b7df..0433d264 100644 --- a/manuskript/loadSave.py +++ b/manuskript/loadSave.py @@ -3,10 +3,12 @@ # The loadSave file calls the propper functions to load and save file # trying to detect the proper file format if it comes from an older version +import zipfile import manuskript.load_save.version_0 as v0 import manuskript.load_save.version_1 as v1 + def saveProject(version=None): if version == 0: @@ -18,9 +20,31 @@ def saveProject(version=None): def loadProject(project): # Detect version - # FIXME + # Is it a zip? + isZip = False version = 0 + try: + zf = zipfile.ZipFile(project) + isZip = True + except zipfile.BadZipFile: + isZip = False + + # Does it have a VERSION in zip root? + if isZip and "VERSION" in zf.namelist(): + version = int(zf.read("VERSION")) + + # Zip but no VERSION: oldest file format + elif isZip: + version = 0 + + # Not a zip + else: + # FIXME + pass + + print("Detected file format version:", version) + if version == 0: v0.loadProject(project) else: diff --git a/manuskript/load_save/version_0.py b/manuskript/load_save/version_0.py index 8bc15fcf..76cd886d 100644 --- a/manuskript/load_save/version_0.py +++ b/manuskript/load_save/version_0.py @@ -37,8 +37,9 @@ def saveProject(): files.append((saveStandardItemModelXML(mw.mdlFlatData), "flatModel.xml")) - files.append((saveStandardItemModelXML(mw.mdlCharacter), - "perso.xml")) + print("ERROR: file format 0 does not save characters !") + # files.append((saveStandardItemModelXML(mw.mdlCharacter), + # "perso.xml")) files.append((saveStandardItemModelXML(mw.mdlWorld), "world.xml")) files.append((saveStandardItemModelXML(mw.mdlLabels), @@ -145,36 +146,35 @@ def loadProject(project): loadStandardItemModelXML(mw.mdlLabels, files["labels.xml"], fromString=True) else: - errors.append("perso.xml") + errors.append("labels.xml") if "status.xml" in files: loadStandardItemModelXML(mw.mdlStatus, files["status.xml"], fromString=True) else: - errors.append("perso.xml") + errors.append("status.xml") if "plots.xml" in files: loadStandardItemModelXML(mw.mdlPlots, files["plots.xml"], fromString=True) else: - errors.append("perso.xml") + errors.append("plots.xml") if "outline.xml" in files: mw.mdlOutline.loadFromXML(files["outline.xml"], fromString=True) else: - errors.append("perso.xml") + errors.append("outline.xml") if "settings.pickle" in files: settings.load(files["settings.pickle"], fromString=True) else: - errors.append("perso.xml") + errors.append("settings.pickle") return errors def loadFilesFromZip(zipname): """Returns the content of zipfile as a dict of filename:content.""" - print(zipname) zf = zipfile.ZipFile(zipname) files = {} for f in zf.namelist(): @@ -286,4 +286,4 @@ def loadStandardItemModelXMLForCharacters(mdl, xml): info.value = ccol.text char.infos.append(info) - mdl.characters.append(char) \ No newline at end of file + mdl.characters.append(char) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index 6e582914..8b113d1a 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -13,7 +13,7 @@ from PyQt5.QtCore import Qt, QModelIndex from PyQt5.QtGui import QColor from manuskript import settings -from manuskript.enums import Character, World, Plot, PlotStep +from manuskript.enums import Character, World, Plot, PlotStep, Outline from manuskript.functions import mainWindow, iconColor from lxml import etree as ET @@ -163,7 +163,7 @@ def saveProject(zip=None): # Characters (self.mdlCharacter) # In a character folder - path = os.path.join("characters", "{name}.txt") # Not sure wheter os.path allows this + path = os.path.join("characters", "{name}.txt") _map = [ (Character.name, "Name"), (Character.ID, "ID"), @@ -203,7 +203,9 @@ def saveProject(zip=None): # Texts # In an outline folder - # TODO + mdl = mw.mdlOutline + for filename, content in exportOutlineItem(mdl.rootItem): + files.append((filename, content)) # World (mw.mdlWorld) # Either in an XML file, or in lots of plain texts? @@ -371,6 +373,82 @@ def addPlotItem(root, mdl, parent=QModelIndex()): return root +def exportOutlineItem(root): + """ + Takes an outline item, and returns an array of (`filename`, `content`) sets, representing the whole tree + of items converted to mmd. + + @param root: OutlineItem + @return: (str, str) + """ + r = [] + path = "outline" + + k=0 + for child in root.children(): + spath = os.path.join(path, *outlineItemPath(child)) + k += 1 + + if child.type() == "folder": + fpath = os.path.join(spath, "folder.txt") + content = outlineToMMD(child) + r.append((fpath, content)) + + elif child.type() in ["txt", "t2t"]: + content = outlineToMMD(child) + r.append((spath, content)) + + elif child.type() in ["html"]: + # Convert first + pass + + else: + print("Unknown type") + + r += exportOutlineItem(child) + + return r + +def outlineItemPath(item): + # Root item + if not item.parent(): + return [] + else: + name = "{ID}-{name}{ext}".format( + ID=item.row(), + name=slugify(item.title()), + ext="" if item.type() == "folder" else ".{}".format(item.type()) + ) + return outlineItemPath(item.parent()) + [name] + +def outlineToMMD(item): + content = "" + + # We don't want to write some datas (computed) + exclude = [Outline.wordCount, Outline.goal, Outline.goalPercentage, Outline.revisions, Outline.text] + # We want to force some data even if they're empty + force = [Outline.compile] + + for attrib in Outline: + if attrib in exclude: continue + val = item.data(attrib.value) + if val or attrib in force: + content += formatMetaData(attrib.name, str(val), 15) + + content += "\n\n" + content += item.data(Outline.text.value) + + # Saving revisions + # TODO + # rev = item.revisions() + # for r in rev: + # revItem = ET.Element("revision") + # revItem.set("timestamp", str(r[0])) + # revItem.set("text", r[1]) + # item.append(revItem) + + return content + def loadProject(project): """ Loads a project. diff --git a/manuskript/models/outlineModel.py b/manuskript/models/outlineModel.py index 9f2c1fe0..dffbff76 100644 --- a/manuskript/models/outlineModel.py +++ b/manuskript/models/outlineModel.py @@ -854,8 +854,13 @@ class outlineItem(): text = text.lower() if not caseSensitive else text for c in columns: - if c == Outline.POV.value: - searchIn = mainWindow.mdlCharacter.getCharacterByID(self.POV()).name() + if c == Outline.POV.value and self.POV(): + c = mainWindow.mdlCharacter.getCharacterByID(self.POV()) + if c: + searchIn = c.name() + else: + searchIn = "" + print("Character POV not found:", self.POV()) elif c == Outline.status.value: searchIn = mainWindow.mdlStatus.item(toInt(self.status()), 0).text() From 518d4c7201474eed620ae95e1c737abb637d9e22 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Wed, 9 Mar 2016 13:19:03 +0100 Subject: [PATCH 21/33] Stores open IDs and not indexes in settings --- manuskript/mainWindow.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index 401b10cc..cd1156fa 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -308,7 +308,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): # Load settings for i in settings.openIndexes: - idx = self.mdlOutline.indexFromPath(i) + idx = self.mdlOutline.getIndexByID(i) self.mainEditor.setCurrentModelIndex(idx, newTab=True) self.generateViewMenu() self.mainEditor.sldCorkSizeFactor.setValue(settings.corkSizeFactor) @@ -432,10 +432,10 @@ class MainWindow(QMainWindow, Ui_MainWindow): settings.lastTab = self.tabMain.currentIndex() if self.currentProject: - # Remembering the current items + # Remembering the current items (stores outlineItem's ID) sel = [] for i in range(self.mainEditor.tab.count()): - sel.append(self.mdlOutline.pathToIndex(self.mainEditor.tab.widget(i).currentIndex)) + sel.append(self.mdlOutline.ID(self.mainEditor.tab.widget(i).currentIndex)) settings.openIndexes = sel # Save data from models @@ -461,6 +461,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): QSettings().setValue("lastProject", projectName) saveProject() # version=0 + self.saveTimerNoChanges.stop() # Giving some feedback print(self.tr("Project {} saved.").format(self.currentProject)) From 97903b2781c245a8f43d8699d08362b20d1590e0 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Wed, 9 Mar 2016 13:20:52 +0100 Subject: [PATCH 22/33] Never thought it would be so boring to code that part --- manuskript/load_save/version_1.py | 67 +++++++++++++++++++++++-------- manuskript/models/outlineModel.py | 21 ++++------ 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index 8b113d1a..5d084036 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -5,7 +5,7 @@ # Aims at providing a plain-text way of saving a project # (except for some elements), allowing collaborative work # versioning and third-partty editing. -import os +import os, shutil import string import zipfile @@ -80,6 +80,8 @@ def saveProject(zip=None): zip = False # Fixme + print("\n\n", "Saving to:", "zip" if zip else "folder") + # List of files to be written files = [] # List of files to be removed @@ -204,8 +206,9 @@ def saveProject(zip=None): # In an outline folder mdl = mw.mdlOutline - for filename, content in exportOutlineItem(mdl.rootItem): - files.append((filename, content)) + f, r = exportOutlineItem(mdl.rootItem) + files += f + removes += r # World (mw.mdlWorld) # Either in an XML file, or in lots of plain texts? @@ -259,24 +262,36 @@ def saveProject(zip=None): else: dir = os.path.dirname(project) folder = os.path.splitext(os.path.basename(project))[0] - print("Saving to folder", folder) + print("\nSaving to folder", folder) for path in removes: if path not in [p for p,c in files]: filename = os.path.join(dir, folder, path) print("* Removing", filename) - os.remove(filename) - cache.pop(path) + if os.path.isdir(filename): + shutil.rmtree(filename) + # FIXME: when deleting a folder, there are still files in "removes". + + # FIXME: if user copied custom files in the directory, they will be lost. + # need to find a way to rename instead of remove. + + # FIXME: items removed have to be removed (not just the renamed) + + else: # elif os.path.exists(filename) + os.remove(filename) + + cache.pop(path, 0) for path, content in files: filename = os.path.join(dir, folder, path) os.makedirs(os.path.dirname(filename), exist_ok=True) - print("* Saving file", filename) + # print("* Saving file", filename) # TODO: the first time it saves, it will overwrite everything, since it's not yet in cache. # Or we have to cache while loading. if not path in cache or cache[path] != content: + print("* Saving file", filename) print(" Not in cache or changed: we write") mode = "w"+ ("b" if type(content) == bytes else "") with open(filename, mode) as f: @@ -284,7 +299,8 @@ def saveProject(zip=None): cache[path] = content else: - print(" In cache, and identical. Do nothing.") + pass + # print(" In cache, and identical. Do nothing.") def addWorldItem(root, mdl, parent=QModelIndex()): @@ -375,13 +391,17 @@ def addPlotItem(root, mdl, parent=QModelIndex()): def exportOutlineItem(root): """ - Takes an outline item, and returns an array of (`filename`, `content`) sets, representing the whole tree - of items converted to mmd. + Takes an outline item, and returns two lists: + 1. of (`filename`, `content`), representing the whole tree of files to be written, in multimarkdown. + 2. of `filename`, representing files to be removed. @param root: OutlineItem - @return: (str, str) + @return: [(str, str)], [str] """ - r = [] + + files = [] + removes = [] + path = "outline" k=0 @@ -389,25 +409,38 @@ def exportOutlineItem(root): spath = os.path.join(path, *outlineItemPath(child)) k += 1 + # Has the item been renamed? + # If so, we mark the old file for removal + if "Herod_dies" in spath: + print(child.title(), spath, "<==", child.lastPath) + if child.lastPath and spath != child.lastPath: + removes.append(child.lastPath) + print(child.title(), "has been renamed (", child.lastPath, " → ", spath, ")") + print(" → We remove:", child.lastPath) + + child.lastPath = spath + if child.type() == "folder": fpath = os.path.join(spath, "folder.txt") content = outlineToMMD(child) - r.append((fpath, content)) + files.append((fpath, content)) elif child.type() in ["txt", "t2t"]: content = outlineToMMD(child) - r.append((spath, content)) + files.append((spath, content)) elif child.type() in ["html"]: - # Convert first + # FIXME: Convert first pass else: print("Unknown type") - r += exportOutlineItem(child) + f, r = exportOutlineItem(child) + files += f + removes += r - return r + return files, removes def outlineItemPath(item): # Root item diff --git a/manuskript/models/outlineModel.py b/manuskript/models/outlineModel.py index dffbff76..3bedf3ab 100644 --- a/manuskript/models/outlineModel.py +++ b/manuskript/models/outlineModel.py @@ -400,19 +400,6 @@ class outlineModel(QAbstractItemModel): self.rootItem = outlineItem(model=self, xml=ET.tostring(root), ID="0") self.rootItem.checkIDs() - def pathToIndex(self, index, path=""): - # FIXME: Use item's ID instead of rows - if not index.isValid(): - return "" - if index.parent().isValid(): - path = self.pathToIndex(index.parent()) - if path: - path = "{},{}".format(path, str(index.row())) - else: - path = str(index.row()) - - return path - def indexFromPath(self, path): path = path.split(",") item = self.rootItem @@ -431,6 +418,8 @@ class outlineItem(): self._model = model self.defaultTextType = None self.IDs = [] # used by root item to store unique IDs + self.lastPath = "" # used by loadSave version_1 to remember which files the items comes from, + # in case it is renamed / removed if title: self._data[Outline.title] = title @@ -758,6 +747,9 @@ class outlineItem(): revItem.set("text", r[1]) item.append(revItem) + # Saving lastPath + item.set("lastPath", self.lastPath) + for i in self.childItems: item.append(ET.XML(i.toXML())) @@ -773,6 +765,9 @@ class outlineItem(): # else: self.setData(Outline.__members__[k].value, str(root.attrib[k])) + if "lastPath" in root.attrib: + self.lastPath = root.attrib["lastPath"] + for child in root: if child.tag == "outlineItem": item = outlineItem(self._model, xml=ET.tostring(child), parent=self) From da5bfb8951ec61d4d7eb278aebe923f3f1e774f5 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Wed, 9 Mar 2016 14:10:22 +0100 Subject: [PATCH 23/33] Removes deleted character's files when saving --- manuskript/load_save/version_1.py | 126 ++++++++++++++++++++-------- manuskript/models/characterModel.py | 6 ++ 2 files changed, 97 insertions(+), 35 deletions(-) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index 5d084036..9d4d24fd 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -66,6 +66,10 @@ def slugify(name): return newName +def log(*args): + print(" ".join(str(a) for a in args)) + + def saveProject(zip=None): """ Saves the project. If zip is False, the project is saved as a multitude of plain-text files for the most parts @@ -80,12 +84,14 @@ def saveProject(zip=None): zip = False # Fixme - print("\n\n", "Saving to:", "zip" if zip else "folder") + log("\n\nSaving to:", "zip" if zip else "folder") # List of files to be written files = [] # List of files to be removed removes = [] + # List of files to be moved + moves = [] mw = mainWindow() @@ -117,6 +123,7 @@ def saveProject(zip=None): ) files.append((path, content)) + #################################################################################################################### # Summary # In plain text, in summary.txt @@ -135,6 +142,7 @@ def saveProject(zip=None): files.append((path, content)) + #################################################################################################################### # Label & Status # In plain text @@ -162,7 +170,8 @@ def saveProject(zip=None): files.append((path, content)) - # Characters (self.mdlCharacter) + #################################################################################################################### + # Characters # In a character folder path = os.path.join("characters", "{name}.txt") @@ -179,7 +188,11 @@ def saveProject(zip=None): (Character.notes, "Notes"), ] mdl = mw.mdlCharacter + + # Review characters for c in mdl.characters: + + # Generates file's content content = "" for m, name in _map: val = mdl.data(c.index(m.value)).strip() @@ -189,28 +202,46 @@ def saveProject(zip=None): for info in c.infos: content += formatMetaData(info.description, info.value, 20) + # generate file's path cpath = path.format(name="{ID}-{slugName}".format( ID=c.ID(), slugName=slugify(c.name()) )) - # Has the character been renamed? - # If so, we remove the old file (if not zipped) - if c.lastPath and cpath != c.lastPath: - removes.append(c.lastPath) - c.lastPath = cpath - files.append(( - cpath, - content)) + # Has the character been renamed? + if c.lastPath and cpath != c.lastPath: + moves.append((c.lastPath, cpath)) + + # Update character's path + c.lastPath = cpath + + files.append((cpath, content)) + + # List removed characters + for c in mdl.removed: + # generate file's path + cpath = path.format(name="{ID}-{slugName}".format( + ID=c.ID(), + slugName=slugify(c.name()) + )) + + # Mark for removal + removes.append(cpath) + + mdl.removed.clear() + + #################################################################################################################### # Texts # In an outline folder mdl = mw.mdlOutline - f, r = exportOutlineItem(mdl.rootItem) + f, m, r = exportOutlineItem(mdl.rootItem) files += f + moves += m removes += r - # World (mw.mdlWorld) + #################################################################################################################### + # World # Either in an XML file, or in lots of plain texts? # More probably text, since there might be writing done in third-party. @@ -224,6 +255,7 @@ def saveProject(zip=None): content = ET.tostring(root, encoding="UTF-8", xml_declaration=True, pretty_print=True) files.append((path, content)) + #################################################################################################################### # Plots (mw.mdlPlots) # Either in XML or lots of plain texts? # More probably XML since there is not really a lot if writing to do (third-party) @@ -236,15 +268,19 @@ def saveProject(zip=None): content = ET.tostring(root, encoding="UTF-8", xml_declaration=True, pretty_print=True) files.append((path, content)) + #################################################################################################################### # Settings # Saved in readable text (json) for easier versionning. But they mustn't be shared, it seems. # Maybe include them only if zipped? # Well, for now, we keep them here... + files.append(("settings.txt", settings.save(protocol=0))) project = mw.currentProject + #################################################################################################################### # Save to zip + if zip: project = os.path.join( os.path.dirname(project), @@ -258,41 +294,58 @@ def saveProject(zip=None): zf.close() + #################################################################################################################### # Save to plain text + else: + + # Project path dir = os.path.dirname(project) + + # Folder containing file: name of the project file (without .msk extension) folder = os.path.splitext(os.path.basename(project))[0] - print("\nSaving to folder", folder) + + # Debug + log("\nSaving to folder", folder) + + # Moving files that have been renamed + for old, new in moves: + + # Get full path + oldPath = os.path.join(dir, folder, old) + newPath = os.path.join(dir, folder, new) + + # Move the old file to the new place + os.replace(oldPath, newPath) + log("* Renaming {} to {}".format(old, new)) + + # Update cache + if old in cache: + cache[new] = cache.pop(old) for path in removes: if path not in [p for p,c in files]: filename = os.path.join(dir, folder, path) - print("* Removing", filename) + log("* Removing", path) + if os.path.isdir(filename): shutil.rmtree(filename) - # FIXME: when deleting a folder, there are still files in "removes". - # FIXME: if user copied custom files in the directory, they will be lost. - # need to find a way to rename instead of remove. - - # FIXME: items removed have to be removed (not just the renamed) - else: # elif os.path.exists(filename) os.remove(filename) + # Clear cache cache.pop(path, 0) for path, content in files: filename = os.path.join(dir, folder, path) os.makedirs(os.path.dirname(filename), exist_ok=True) - # print("* Saving file", filename) # TODO: the first time it saves, it will overwrite everything, since it's not yet in cache. # Or we have to cache while loading. if not path in cache or cache[path] != content: - print("* Saving file", filename) - print(" Not in cache or changed: we write") + log("* Writing file", path) mode = "w"+ ("b" if type(content) == bytes else "") with open(filename, mode) as f: f.write(content) @@ -300,7 +353,7 @@ def saveProject(zip=None): else: pass - # print(" In cache, and identical. Do nothing.") + # log(" In cache, and identical. Do nothing.") def addWorldItem(root, mdl, parent=QModelIndex()): @@ -393,6 +446,7 @@ def exportOutlineItem(root): """ Takes an outline item, and returns two lists: 1. of (`filename`, `content`), representing the whole tree of files to be written, in multimarkdown. + 3. of (`filename`, `filename`) listing files to be moved 2. of `filename`, representing files to be removed. @param root: OutlineItem @@ -400,6 +454,7 @@ def exportOutlineItem(root): """ files = [] + moves = [] removes = [] path = "outline" @@ -410,16 +465,15 @@ def exportOutlineItem(root): k += 1 # Has the item been renamed? - # If so, we mark the old file for removal - if "Herod_dies" in spath: - print(child.title(), spath, "<==", child.lastPath) if child.lastPath and spath != child.lastPath: - removes.append(child.lastPath) - print(child.title(), "has been renamed (", child.lastPath, " → ", spath, ")") - print(" → We remove:", child.lastPath) + moves.append((child.lastPath, spath)) + log(child.title(), "has been renamed (", child.lastPath, " → ", spath, ")") + log(" → We mark for moving:", child.lastPath) + # Updates item last's path child.lastPath = spath + # Generating content if child.type() == "folder": fpath = os.path.join(spath, "folder.txt") content = outlineToMMD(child) @@ -431,16 +485,18 @@ def exportOutlineItem(root): elif child.type() in ["html"]: # FIXME: Convert first - pass + content = outlineToMMD(child) + files.append((spath, content)) else: - print("Unknown type") + log("Unknown type") - f, r = exportOutlineItem(child) + f, m, r = exportOutlineItem(child) files += f + moves += m removes += r - return files, removes + return files, moves, removes def outlineItemPath(item): # Root item @@ -472,7 +528,7 @@ def outlineToMMD(item): content += item.data(Outline.text.value) # Saving revisions - # TODO + # TODO: saving revisions? # rev = item.revisions() # for r in rev: # revItem = ET.Element("revision") diff --git a/manuskript/models/characterModel.py b/manuskript/models/characterModel.py index e1a4e684..fad34f1d 100644 --- a/manuskript/models/characterModel.py +++ b/manuskript/models/characterModel.py @@ -12,7 +12,11 @@ class characterModel(QAbstractItemModel): def __init__(self, parent): QAbstractItemModel.__init__(self, parent) + # CharacterItems are stored in this list self.characters = [] + # We keep track of removed character, so that when we save in multiple files, we can remove old character's + # files. + self.removed = [] ############################################################################### # QAbstractItemModel subclassed @@ -170,7 +174,9 @@ class characterModel(QAbstractItemModel): """ c = self.getCharacterByID(ID) self.beginRemoveRows(QModelIndex(), self.characters.index(c), self.characters.index(c)) + self.removed.append(c) self.characters.remove(c) + self.endRemoveRows() ############################################################################### # CHARACTER INFOS From dc1b7577700cbd572f5d1945798e25581b524377 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Wed, 9 Mar 2016 15:48:59 +0100 Subject: [PATCH 24/33] Saving seems to work kind of smoothly --- manuskript/load_save/version_1.py | 116 ++++++++++++++++++++---------- manuskript/models/outlineModel.py | 24 +++++-- 2 files changed, 94 insertions(+), 46 deletions(-) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index 9d4d24fd..253bcdd0 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -217,16 +217,16 @@ def saveProject(zip=None): files.append((cpath, content)) - # List removed characters - for c in mdl.removed: - # generate file's path - cpath = path.format(name="{ID}-{slugName}".format( - ID=c.ID(), - slugName=slugify(c.name()) - )) - - # Mark for removal - removes.append(cpath) + # # List removed characters + # for c in mdl.removed: + # # generate file's path + # cpath = path.format(name="{ID}-{slugName}".format( + # ID=c.ID(), + # slugName=slugify(c.name()) + # )) + # + # # Mark for removal + # removes.append(cpath) mdl.removed.clear() @@ -235,11 +235,19 @@ def saveProject(zip=None): # In an outline folder mdl = mw.mdlOutline + + # Go through the tree f, m, r = exportOutlineItem(mdl.rootItem) files += f moves += m removes += r + # List removed items + # for item in mdl.removed: + # path = outlineItemPath(item) + # log("* Marking for removal:", path) + + #################################################################################################################### # World # Either in an XML file, or in lots of plain texts? @@ -299,6 +307,8 @@ def saveProject(zip=None): else: + global cache + # Project path dir = os.path.dirname(project) @@ -316,27 +326,23 @@ def saveProject(zip=None): newPath = os.path.join(dir, folder, new) # Move the old file to the new place - os.replace(oldPath, newPath) - log("* Renaming {} to {}".format(old, new)) + try: + os.replace(oldPath, newPath) + log("* Renaming/moving {} to {}".format(old, new)) + except FileNotFoundError: + # Maybe parent folder has been renamed + pass # Update cache - if old in cache: - cache[new] = cache.pop(old) - - for path in removes: - if path not in [p for p,c in files]: - filename = os.path.join(dir, folder, path) - log("* Removing", path) - - if os.path.isdir(filename): - shutil.rmtree(filename) - - else: # elif os.path.exists(filename) - os.remove(filename) - - # Clear cache - cache.pop(path, 0) + cache2 = {} + for f in cache: + f2 = f.replace(old, new) + if f2 != f: + log(" * Updating cache:", f, f2) + cache2[f2] = cache[f] + cache = cache2 + # Writing files for path, content in files: filename = os.path.join(dir, folder, path) os.makedirs(os.path.dirname(filename), exist_ok=True) @@ -355,6 +361,31 @@ def saveProject(zip=None): pass # log(" In cache, and identical. Do nothing.") + # Removing phantoms + for path in [p for p in cache if p not in [p for p,c in files]]: + filename = os.path.join(dir, folder, path) + log("* Removing", path) + + if os.path.isdir(filename): + shutil.rmtree(filename) + + else: # elif os.path.exists(filename) + os.remove(filename) + + # Clear cache + cache.pop(path, 0) + + # Removing empty directories + for root, dirs, files in os.walk(os.path.join(dir, folder, "outline")): + for dir in dirs: + newDir = os.path.join(root, dir) + try: + os.removedirs(newDir) + log("* Removing empty directory:", newDir) + except: + # Directory not empty, we don't remove. + pass + def addWorldItem(root, mdl, parent=QModelIndex()): """ @@ -457,21 +488,22 @@ def exportOutlineItem(root): moves = [] removes = [] - path = "outline" - k=0 for child in root.children(): - spath = os.path.join(path, *outlineItemPath(child)) + itemPath = outlineItemPath(child) + spath = os.path.join(*itemPath) + k += 1 # Has the item been renamed? - if child.lastPath and spath != child.lastPath: - moves.append((child.lastPath, spath)) - log(child.title(), "has been renamed (", child.lastPath, " → ", spath, ")") - log(" → We mark for moving:", child.lastPath) + lp = child._lastPath + if lp and spath != lp: + moves.append((lp, spath)) + log(child.title(), "has been renamed (", lp, " → ", spath, ")") + log(" → We mark for moving:", lp) # Updates item last's path - child.lastPath = spath + child._lastPath = spath # itemPath[-1] # Generating content if child.type() == "folder": @@ -484,7 +516,7 @@ def exportOutlineItem(root): files.append((spath, content)) elif child.type() in ["html"]: - # FIXME: Convert first + # Save as html. Not the most beautiful, but hey. content = outlineToMMD(child) files.append((spath, content)) @@ -499,14 +531,20 @@ def exportOutlineItem(root): return files, moves, removes def outlineItemPath(item): + """ + Returns the outlineItem file path (like the path where it will be written on the disk). As a list of folder's + name. To be joined by os.path.join. + @param item: outlineItem + @return: list of folder's names + """ # Root item if not item.parent(): - return [] + return ["outline"] else: name = "{ID}-{name}{ext}".format( ID=item.row(), name=slugify(item.title()), - ext="" if item.type() == "folder" else ".{}".format(item.type()) + ext="" if item.type() == "folder" else ".md" # ".{}".format(item.type()) # To have .txt, .t2t, .html, ... ) return outlineItemPath(item.parent()) + [name] diff --git a/manuskript/models/outlineModel.py b/manuskript/models/outlineModel.py index 3bedf3ab..55d676ca 100644 --- a/manuskript/models/outlineModel.py +++ b/manuskript/models/outlineModel.py @@ -18,7 +18,7 @@ from manuskript.enums import Outline from manuskript.functions import mainWindow, toInt, wordCount locale.setlocale(locale.LC_ALL, '') -import time +import time, os class outlineModel(QAbstractItemModel): @@ -27,6 +27,9 @@ class outlineModel(QAbstractItemModel): self.rootItem = outlineItem(self, title="root", ID="0") + # Stores removed item, in order to remove them on disk when saving, depending on the file format. + self.removed = [] + def index(self, row, column, parent): if not self.hasIndex(row, column, parent): @@ -363,7 +366,8 @@ class outlineModel(QAbstractItemModel): self.beginRemoveRows(parent, row, row + count - 1) for i in range(count): - parentItem.removeChild(row) + item = parentItem.removeChild(row) + self.removed.append(item) self.endRemoveRows() return True @@ -418,7 +422,7 @@ class outlineItem(): self._model = model self.defaultTextType = None self.IDs = [] # used by root item to store unique IDs - self.lastPath = "" # used by loadSave version_1 to remember which files the items comes from, + self._lastPath = "" # used by loadSave version_1 to remember which files the items comes from, # in case it is renamed / removed if title: @@ -592,7 +596,7 @@ class outlineItem(): self.parent().updateWordCount(emit) def row(self): - if self.parent: + if self.parent(): return self.parent().childItems.index(self) def appendChild(self, child): @@ -634,9 +638,15 @@ class outlineItem(): c.emitDataChanged(cols, recursive=True) def removeChild(self, row): - self.childItems.pop(row) + """ + Removes child at position `row` and returns it. + @param row: index (int) of the child to remove. + @return: the removed outlineItem + """ + r = self.childItems.pop(row) # Might be causing segfault when updateWordCount emits dataChanged self.updateWordCount(emit=False) + return r def parent(self): return self._parent @@ -748,7 +758,7 @@ class outlineItem(): item.append(revItem) # Saving lastPath - item.set("lastPath", self.lastPath) + item.set("lastPath", self._lastPath) for i in self.childItems: item.append(ET.XML(i.toXML())) @@ -766,7 +776,7 @@ class outlineItem(): self.setData(Outline.__members__[k].value, str(root.attrib[k])) if "lastPath" in root.attrib: - self.lastPath = root.attrib["lastPath"] + self._lastPath = root.attrib["lastPath"] for child in root: if child.tag == "outlineItem": From b29fbebd25401b3845a90f446fa6d9893a1b8faa Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Wed, 9 Mar 2016 15:54:01 +0100 Subject: [PATCH 25/33] PEP8 cleaning --- manuskript/load_save/version_1.py | 56 +++++++++++++++++-------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index 253bcdd0..ec6ab5f4 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -5,7 +5,9 @@ # Aims at providing a plain-text way of saving a project # (except for some elements), allowing collaborative work # versioning and third-partty editing. -import os, shutil + +import os +import shutil import string import zipfile @@ -105,15 +107,15 @@ def saveProject(zip=None): path = "infos.txt" content = "" for name, col in [ - ("Title", 0), - ("Subtitle", 1), - ("Serie", 2), - ("Volume", 3), - ("Genre", 4), - ("License", 5), - ("Author", 6), - ("Email", 7), - ]: + ("Title", 0), + ("Subtitle", 1), + ("Serie", 2), + ("Volume", 3), + ("Genre", 4), + ("License", 5), + ("Author", 6), + ("Email", 7), + ]: val = mw.mdlFlatData.item(0, col).text().strip() if val: content += "{name}:{spaces}{value}\n".format( @@ -130,12 +132,12 @@ def saveProject(zip=None): path = "summary.txt" content = "" for name, col in [ - ("Situation", 0), - ("Sentence", 1), - ("Paragraph", 2), - ("Page", 3), - ("Full", 4), - ]: + ("Situation", 0), + ("Sentence", 1), + ("Paragraph", 2), + ("Page", 3), + ("Full", 4), + ]: val = mw.mdlFlatData.item(1, col).text().strip() if val: content += formatMetaData(name, val, 12) @@ -165,7 +167,7 @@ def saveProject(zip=None): if text: content += "{name}{color}\n".format( name=text, - color= "" if color == "" else ":" + " " * (20 - len(text)) + color + color="" if color == "" else ":" + " " * (20 - len(text)) + color ) files.append((path, content)) @@ -247,7 +249,6 @@ def saveProject(zip=None): # path = outlineItemPath(item) # log("* Marking for removal:", path) - #################################################################################################################### # World # Either in an XML file, or in lots of plain texts? @@ -350,9 +351,9 @@ def saveProject(zip=None): # TODO: the first time it saves, it will overwrite everything, since it's not yet in cache. # Or we have to cache while loading. - if not path in cache or cache[path] != content: + if path not in cache or cache[path] != content: log("* Writing file", path) - mode = "w"+ ("b" if type(content) == bytes else "") + mode = "w" + ("b" if type(content) == bytes else "") with open(filename, mode) as f: f.write(content) cache[path] = content @@ -362,7 +363,7 @@ def saveProject(zip=None): # log(" In cache, and identical. Do nothing.") # Removing phantoms - for path in [p for p in cache if p not in [p for p,c in files]]: + for path in [p for p in cache if p not in [p for p, c in files]]: filename = os.path.join(dir, folder, path) log("* Removing", path) @@ -488,10 +489,9 @@ def exportOutlineItem(root): moves = [] removes = [] - k=0 + k = 0 for child in root.children(): - itemPath = outlineItemPath(child) - spath = os.path.join(*itemPath) + spath = os.path.join(*outlineItemPath(child)) k += 1 @@ -503,7 +503,7 @@ def exportOutlineItem(root): log(" → We mark for moving:", lp) # Updates item last's path - child._lastPath = spath # itemPath[-1] + child._lastPath = spath # Generating content if child.type() == "folder": @@ -530,6 +530,7 @@ def exportOutlineItem(root): return files, moves, removes + def outlineItemPath(item): """ Returns the outlineItem file path (like the path where it will be written on the disk). As a list of folder's @@ -548,6 +549,7 @@ def outlineItemPath(item): ) return outlineItemPath(item.parent()) + [name] + def outlineToMMD(item): content = "" @@ -557,7 +559,8 @@ def outlineToMMD(item): force = [Outline.compile] for attrib in Outline: - if attrib in exclude: continue + if attrib in exclude: + continue val = item.data(attrib.value) if val or attrib in force: content += formatMetaData(attrib.name, str(val), 15) @@ -576,6 +579,7 @@ def outlineToMMD(item): return content + def loadProject(project): """ Loads a project. From fc89207ca88eb1077b172659b73c979836b71d16 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Wed, 9 Mar 2016 16:02:22 +0100 Subject: [PATCH 26/33] Saving seems to be done --- manuskript/loadSave.py | 16 +++++++++++++--- manuskript/load_save/version_1.py | 18 ------------------ manuskript/models/characterModel.py | 4 ---- 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/manuskript/loadSave.py b/manuskript/loadSave.py index 0433d264..dead6050 100644 --- a/manuskript/loadSave.py +++ b/manuskript/loadSave.py @@ -3,6 +3,7 @@ # The loadSave file calls the propper functions to load and save file # trying to detect the proper file format if it comes from an older version +import os import zipfile import manuskript.load_save.version_0 as v0 @@ -20,10 +21,10 @@ def saveProject(version=None): def loadProject(project): # Detect version - # Is it a zip? isZip = False version = 0 + # Is it a zip? try: zf = zipfile.ZipFile(project) isZip = True @@ -40,8 +41,17 @@ def loadProject(project): # Not a zip else: - # FIXME - pass + # Project path + dir = os.path.dirname(project) + + # Folder containing file: name of the project file (without .msk extension) + folder = os.path.splitext(os.path.basename(project))[0] + + # Reading VERSION file + path = os.path.join(dir, folder, "VERSION") + if os.path.exists(path): + with open(path, "r") as f: + version = int(f.read()) print("Detected file format version:", version) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index ec6ab5f4..af946453 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -219,19 +219,6 @@ def saveProject(zip=None): files.append((cpath, content)) - # # List removed characters - # for c in mdl.removed: - # # generate file's path - # cpath = path.format(name="{ID}-{slugName}".format( - # ID=c.ID(), - # slugName=slugify(c.name()) - # )) - # - # # Mark for removal - # removes.append(cpath) - - mdl.removed.clear() - #################################################################################################################### # Texts # In an outline folder @@ -244,11 +231,6 @@ def saveProject(zip=None): moves += m removes += r - # List removed items - # for item in mdl.removed: - # path = outlineItemPath(item) - # log("* Marking for removal:", path) - #################################################################################################################### # World # Either in an XML file, or in lots of plain texts? diff --git a/manuskript/models/characterModel.py b/manuskript/models/characterModel.py index fad34f1d..1ec975f9 100644 --- a/manuskript/models/characterModel.py +++ b/manuskript/models/characterModel.py @@ -14,9 +14,6 @@ class characterModel(QAbstractItemModel): # CharacterItems are stored in this list self.characters = [] - # We keep track of removed character, so that when we save in multiple files, we can remove old character's - # files. - self.removed = [] ############################################################################### # QAbstractItemModel subclassed @@ -174,7 +171,6 @@ class characterModel(QAbstractItemModel): """ c = self.getCharacterByID(ID) self.beginRemoveRows(QModelIndex(), self.characters.index(c), self.characters.index(c)) - self.removed.append(c) self.characters.remove(c) self.endRemoveRows() From ed7e5f69b503605e674575437d0c6e609beeb619 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Wed, 9 Mar 2016 17:20:43 +0100 Subject: [PATCH 27/33] Saves revision even in non-zip format --- manuskript/loadSave.py | 17 ++++------------- manuskript/load_save/version_1.py | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/manuskript/loadSave.py b/manuskript/loadSave.py index dead6050..7763d027 100644 --- a/manuskript/loadSave.py +++ b/manuskript/loadSave.py @@ -15,7 +15,7 @@ def saveProject(version=None): if version == 0: v0.saveProject() else: - v1.saveProject() + v1.saveProject(zip=True) def loadProject(project): @@ -41,21 +41,12 @@ def loadProject(project): # Not a zip else: - # Project path - dir = os.path.dirname(project) - - # Folder containing file: name of the project file (without .msk extension) - folder = os.path.splitext(os.path.basename(project))[0] - - # Reading VERSION file - path = os.path.join(dir, folder, "VERSION") - if os.path.exists(path): - with open(path, "r") as f: - version = int(f.read()) + with open(project, "r") as f: + version = int(f.read()) print("Detected file format version:", version) - if version == 0: + if version == 0 or True: v0.loadProject(project) else: v1.loadProject(project) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index af946453..b0ba4fe5 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -231,6 +231,10 @@ def saveProject(zip=None): moves += m removes += r + # Writes revisions (if asked for) + if settings.revisions["keep"]: + files.append(("revisions.xml", mdl.saveToXML())) + #################################################################################################################### # World # Either in an XML file, or in lots of plain texts? @@ -273,10 +277,10 @@ def saveProject(zip=None): # Save to zip if zip: - project = os.path.join( - os.path.dirname(project), - "_" + os.path.basename(project) - ) + # project = os.path.join( + # os.path.dirname(project), + # "_" + os.path.basename(project) + # ) zf = zipfile.ZipFile(project, mode="w") @@ -369,6 +373,10 @@ def saveProject(zip=None): # Directory not empty, we don't remove. pass + # Write the project file's content + with open(project, "w") as f: + f.write("1") # Format number + def addWorldItem(root, mdl, parent=QModelIndex()): """ From 22f2bd7899b7da28b1d360a0e2e4afe025377283 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Wed, 9 Mar 2016 17:38:12 +0100 Subject: [PATCH 28/33] Corrects bug when reopening file --- manuskript/mainWindow.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index cd1156fa..a60f5613 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -557,11 +557,14 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.tblPersoInfos.setModel(self.mdlCharacter) self.btnAddPerso.clicked.connect(self.mdlCharacter.addCharacter, AUC) - self.btnRmPerso.clicked.connect(self.lstCharacters.removeCharacter, AUC) - self.btnPersoColor.clicked.connect(self.lstCharacters.choseCharacterColor, AUC) - - self.btnPersoAddInfo.clicked.connect(self.lstCharacters.addCharacterInfo, AUC) - self.btnPersoRmInfo.clicked.connect(self.lstCharacters.removeCharacterInfo, AUC) + try: + self.btnRmPerso.clicked.connect(self.lstCharacters.removeCharacter, AUC) + self.btnPersoColor.clicked.connect(self.lstCharacters.choseCharacterColor, AUC) + self.btnPersoAddInfo.clicked.connect(self.lstCharacters.addCharacterInfo, AUC) + self.btnPersoRmInfo.clicked.connect(self.lstCharacters.removeCharacterInfo, AUC) + except TypeError: + # Connection has already been made + pass for w, c in [ (self.txtPersoName, Character.name.value), From 10fdb89eefc2bf0ed983aa47dfc6408e5bb45aa4 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Thu, 10 Mar 2016 11:01:35 +0100 Subject: [PATCH 29/33] Bug corrected --- manuskript/settings.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/manuskript/settings.py b/manuskript/settings.py index d03f6dec..fc6065f0 100644 --- a/manuskript/settings.py +++ b/manuskript/settings.py @@ -131,7 +131,7 @@ def save(filename=None, protocol=None): return pickle.dumps(allSettings) -def load(string, fromString=False): +def load(string, fromString=False, protocol=None): """Load settings from 'string'. 'string' is the filename of the pickle dump. If fromString=True, string is the data of the pickle dumps.""" global allSettings @@ -145,8 +145,11 @@ def load(string, fromString=False): print("{} doesn't exist, cannot load settings.".format(string)) return else: - allSettings = pickle.loads(string) - + if protocol == 0: + allSettings = json.loads(string) + else: + allSettings = pickle.loads(string) + #pp=pprint.PrettyPrinter(indent=4, compact=False) #print("Loading:") #pp.pprint(allSettings) @@ -223,6 +226,17 @@ def load(string, fromString=False): global revisions revisions = allSettings["revisions"] + # With JSON we had to convert int keys to str, and None to "null", so we roll back. + r = {} + for i in revisions["rules"]: + if i == "null": + r[None] = revisions["rules"]["null"] + continue + elif i == None: + continue + r[int(i)] = revisions["rules"][i] + revisions["rules"] = r + if "frequencyAnalyzer" in allSettings: global frequencyAnalyzer frequencyAnalyzer = allSettings["frequencyAnalyzer"] From b2a51e1a0957df0aa44abcc0a69176ea82474243 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Thu, 10 Mar 2016 11:45:40 +0100 Subject: [PATCH 30/33] Reads characters --- manuskript/loadSave.py | 12 +- manuskript/load_save/version_1.py | 365 ++++++++++++++++++++++++++-- manuskript/models/characterModel.py | 3 +- 3 files changed, 350 insertions(+), 30 deletions(-) diff --git a/manuskript/loadSave.py b/manuskript/loadSave.py index 7763d027..28eecb2f 100644 --- a/manuskript/loadSave.py +++ b/manuskript/loadSave.py @@ -12,10 +12,15 @@ import manuskript.load_save.version_1 as v1 def saveProject(version=None): + # While debugging, we don't save the project + return + if version == 0: v0.saveProject() else: - v1.saveProject(zip=True) + v1.saveProject() + + # FIXME: add settings to chose between saving as zip or not. def loadProject(project): @@ -44,9 +49,10 @@ def loadProject(project): with open(project, "r") as f: version = int(f.read()) + print("Loading:", project) print("Detected file format version:", version) - if version == 0 or True: + if version == 0: v0.loadProject(project) else: - v1.loadProject(project) + v1.loadProject(project, zip=isZip) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index b0ba4fe5..e38af597 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -7,18 +7,22 @@ # versioning and third-partty editing. import os +import re import shutil import string import zipfile +from collections import OrderedDict from PyQt5.QtCore import Qt, QModelIndex -from PyQt5.QtGui import QColor +from PyQt5.QtGui import QColor, QStandardItem from manuskript import settings from manuskript.enums import Character, World, Plot, PlotStep, Outline -from manuskript.functions import mainWindow, iconColor +from manuskript.functions import mainWindow, iconColor, iconFromColorString from lxml import etree as ET +from manuskript.load_save.version_0 import loadFilesFromZip +from manuskript.models.characterModel import CharacterInfo try: import zlib # Used with zipfile for compression @@ -30,6 +34,31 @@ except: cache = {} +characterMap = OrderedDict([ + (Character.name, "Name"), + (Character.ID, "ID"), + (Character.importance, "Importance"), + (Character.motivation, "Motivation"), + (Character.goal, "Goal"), + (Character.conflict, "Conflict"), + (Character.summarySentence, "Phrase Summary"), + (Character.summaryPara, "Paragraph Summary"), + (Character.summaryFull, "Full Summary"), + (Character.notes, "Notes"), +]) +# characterMap = { +# Character.name: "Name", +# Character.ID: "ID", +# Character.importance: "Importance", +# Character.motivation: "Motivation", +# Character.goal: "Goal", +# Character.conflict: "Conflict", +# Character.summarySentence: "Phrase Summary", +# Character.summaryPara: "Paragraph Summary", +# Character.summaryFull: "Full Summary", +# Character.notes: "Notes", +# } + def formatMetaData(name, value, tabLength=10): # Multiline formatting @@ -177,18 +206,6 @@ def saveProject(zip=None): # In a character folder path = os.path.join("characters", "{name}.txt") - _map = [ - (Character.name, "Name"), - (Character.ID, "ID"), - (Character.importance, "Importance"), - (Character.motivation, "Motivation"), - (Character.goal, "Goal"), - (Character.conflict, "Conflict"), - (Character.summarySentence, "Phrase Summary"), - (Character.summaryPara, "Paragraph Summary"), - (Character.summaryFull, "Full Summary"), - (Character.notes, "Notes"), - ] mdl = mw.mdlCharacter # Review characters @@ -196,11 +213,15 @@ def saveProject(zip=None): # Generates file's content content = "" - for m, name in _map: + for m in characterMap: val = mdl.data(c.index(m.value)).strip() if val: - content += formatMetaData(name, val, 20) + content += formatMetaData(characterMap[m], val, 20) + # Character's color: + content += formatMetaData("Color", c.color().name(QColor.HexRgb), 20) + + # Character's infos for info in c.infos: content += formatMetaData(info.description, info.value, 20) @@ -334,9 +355,7 @@ def saveProject(zip=None): filename = os.path.join(dir, folder, path) os.makedirs(os.path.dirname(filename), exist_ok=True) - # TODO: the first time it saves, it will overwrite everything, since it's not yet in cache. - # Or we have to cache while loading. - + # Check if content is in cache, and write if necessary if path not in cache or cache[path] != content: log("* Writing file", path) mode = "w" + ("b" if type(content) == bytes else "") @@ -344,10 +363,6 @@ def saveProject(zip=None): f.write(content) cache[path] = content - else: - pass - # log(" In cache, and identical. Do nothing.") - # Removing phantoms for path in [p for p in cache if p not in [p for p, c in files]]: filename = os.path.join(dir, folder, path) @@ -569,15 +584,313 @@ def outlineToMMD(item): return content +######################################################################################################################## +# LOAD +######################################################################################################################## -def loadProject(project): +def loadProject(project, zip=None): """ Loads a project. @param project: the filename of the project to open. + @param zip: whether the project is a zipped or not. @return: an array of errors, empty if None. """ - # Don't forget to cache everything that is loaded + # FIXME: Don't forget to cache everything that is loaded # In order to save only what has changed. - pass \ No newline at end of file + mw = mainWindow() + errors = [] + + #################################################################################################################### + # Read and store everything in a dict + + log("\nLoading {} ({})".format(project, "ZIP" if zip else "not zip")) + if zip: + files = loadFilesFromZip(project) + + # Decode files + for f in files: + if f[-4:] not in [".xml", "opml"]: + files[f] = files[f].decode("utf-8") + + else: + # Project path + dir = os.path.dirname(project) + + # Folder containing file: name of the project file (without .msk extension) + folder = os.path.splitext(os.path.basename(project))[0] + + # The full path towards the folder containing files + path = os.path.join(dir, folder, "") + + files = {} + for dirpath, dirnames, filenames in os.walk(path): + p = dirpath.replace(path, "") + for f in filenames: + mode = "r" + ("b" if f[-4:] in [".xml", "opml"] else "") + with open(os.path.join(dirpath, f), mode) as fo: + files[os.path.join(p, f)] = fo.read() + + #################################################################################################################### + # Settings + + if "settings.txt" in files: + settings.load(files["settings.txt"], fromString=True, protocol=0) + else: + errors.append("settings.txt") + + #################################################################################################################### + # Labels + + mdl = mw.mdlLabels + mdl.appendRow(QStandardItem("")) # Empty = No labels + if "labels.txt" in files: + log("\nReading labels:") + for s in files["labels.txt"].split("\n"): + if not s: + continue + + m = re.search(r"^(.*?):\s*(.*)$", s) + txt = m.group(1) + col = m.group(2) + log("* Add status: {} ({})".format(txt, col)) + icon = iconFromColorString(col) + mdl.appendRow(QStandardItem(icon, txt)) + + else: + errors.append("labels.txt") + + #################################################################################################################### + # Status + + mdl = mw.mdlStatus + mdl.appendRow(QStandardItem("")) # Empty = No status + if "status.txt" in files: + log("\nReading Status:") + for s in files["status.txt"].split("\n"): + if not s: + continue + log("* Add status:", s) + mdl.appendRow(QStandardItem(s)) + else: + errors.append("status.txt") + + #################################################################################################################### + # Infos + + mdl = mw.mdlFlatData + if "infos.txt" in files: + md, body = parseMMDFile(files["infos.txt"], asDict=True) + + row = [] + for name in ["Title", "Subtitle", "Serie", "Volume", "Genre", "License", "Author", "Email"]: + row.append(QStandardItem(md.get(name, ""))) + + mdl.appendRow(row) + + else: + errors.append("infos.txt") + + #################################################################################################################### + # Summary + + mdl = mw.mdlFlatData + if "summary.txt" in files: + md, body = parseMMDFile(files["summary.txt"], asDict=True) + + row = [] + for name in ["Situation", "Sentence", "Paragraph", "Page", "Full"]: + row.append(QStandardItem(md.get(name, ""))) + + mdl.appendRow(row) + + else: + errors.append("summary.txt") + + #################################################################################################################### + # Plots + + mdl = mw.mdlPlots + if "plots.xml" in files: + log("\nReading plots:") + # xml = bytearray(files["plots.xml"], "utf-8") + root = ET.fromstring(files["plots.xml"]) + + for plot in root: + # Create row + row = getStandardItemRowFromXMLEnum(plot, Plot) + + # Log + log("* Add plot: ", row[0].text()) + + # Characters + if row[Plot.characters.value].text(): + IDs = row[Plot.characters.value].text().split(",") + item = QStandardItem() + for ID in IDs: + item.appendRow(QStandardItem(ID.strip())) + row[Plot.characters.value] = item + + # Subplots + for step in plot: + row[Plot.steps.value].appendRow( + getStandardItemRowFromXMLEnum(step, PlotStep) + ) + + # Add row to the model + mdl.appendRow(row) + + else: + errors.append("plots.xml") + + #################################################################################################################### + # World + + mdl = mw.mdlWorld + if "world.opml" in files: + log("\nReading World:") + # xml = bytearray(files["plots.xml"], "utf-8") + root = ET.fromstring(files["world.opml"]) + body = root.find("body") + + for outline in body: + row = getOutlineItem(outline, World) + mdl.appendRow(row) + + else: + errors.append("world.opml") + + #################################################################################################################### + # Characters + + mdl = mw.mdlCharacter + log("\nReading Characters:") + for f in [f for f in files if "characters" in f]: + md, body = parseMMDFile(files[f]) + c = mdl.addCharacter() + + color = False + for desc, val in md: + + # Base infos + if desc in characterMap.values(): + key = [key for key, value in characterMap.items() if value == desc][0] + index = c.index(key.value) + mdl.setData(index, val) + + # Character color + elif desc == "Color" and not color: + c.setColor(QColor(val)) + # We remember the first time we found "Color": it is the icon color. + # If "Color" comes a second time, it is a Character's info. + color = True + + # Character's infos + else: + c.infos.append(CharacterInfo(c, desc, val)) + + log("* Adds {} ({})".format(c.name(), c.ID())) + + + # if "perso.xml" in files: + # loadStandardItemModelXMLForCharacters(mw.mdlCharacter, files["perso.xml"]) + # else: + # errors.append("perso.xml") + # + # + # if "outline.xml" in files: + # mw.mdlOutline.loadFromXML(files["outline.xml"], fromString=True) + # else: + # errors.append("outline.xml") + # + # return errors + +def getOutlineItem(item, enum): + row = getStandardItemRowFromXMLEnum(item, enum) + log("* Add worldItem:", row[0].text()) + for child in item: + sub = getOutlineItem(child, enum) + row[0].appendRow(sub) + + return row + +def getStandardItemRowFromXMLEnum(item, enum): + """ + Reads and etree item and creates a row of QStandardItems by cross-referencing an enum. + Returns a list of QStandardItems that can be added to a QStandardItemModel by appendRow. + @param item: the etree item + @param enum: the enum + @return: list of QStandardItems + """ + row = [] + for i in range(len(enum)): + row.append(QStandardItem("")) + + for name in item.attrib: + if name in enum.__members__: + row[enum[name].value] = QStandardItem(item.attrib[name]) + return row + +def parseMMDFile(text, asDict=False): + """ + Takes the content of a MultiMarkDown file (str) and returns: + 1. A list containing metadatas: (description, value) if asDict is False. + If asDict is True, returns metadatas as an OrderedDict. Be aware that if multiple metadatas have the same description + (which is stupid, but hey), they will be lost except the last one. + 2. The body of the file + @param text: the content of the file + @return: (list, str) or (OrderedDict, str) + """ + md = [] + mdd = OrderedDict() + body = [] + descr = "" + val = "" + inBody = False + for s in text.split("\n"): + if not inBody: + m = re.match(r"^(.*?):\s*(.*)$", s) + if m: + # Commit last metadata + if descr: + if descr == "None": + descr = "" + md.append((descr, val)) + mdd[descr] = val + descr = "" + val = "" + + # Store new values + descr = m.group(1) + val = m.group(2) + + elif s[:4] == " ": + val += "\n" + s.strip() + + elif s == "": + # End of metadatas + inBody = True + + # Commit last metadata + if descr: + if descr == "None": + descr = "" + md.append((descr, val)) + mdd[descr] = val + + else: + body.append[s] + + # We remove the second empty line (since we save with two empty lines) + if body and body[0] == "": + body = body[1:] + + body = "\n".join(body) + + if not asDict: + return md, body + else: + return mdd, body + + diff --git a/manuskript/models/characterModel.py b/manuskript/models/characterModel.py index 1ec975f9..d6f7016c 100644 --- a/manuskript/models/characterModel.py +++ b/manuskript/models/characterModel.py @@ -156,12 +156,13 @@ class characterModel(QAbstractItemModel): def addCharacter(self): """ Creates a new character - @return: nothing + @return: the character """ c = Character(model=self, name=self.tr("New character")) self.beginInsertRows(QModelIndex(), len(self.characters), len(self.characters)) self.characters.append(c) self.endInsertRows() + return c def removeCharacter(self, ID): """ From b26de717a9f1022b5aac9da4b1d82e5750a5bc4b Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Thu, 10 Mar 2016 13:10:31 +0100 Subject: [PATCH 31/33] Seems that loading works --- manuskript/load_save/version_1.py | 163 +++++++++++++++++++++++++----- manuskript/models/outlineModel.py | 9 +- 2 files changed, 144 insertions(+), 28 deletions(-) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index e38af597..6aecb198 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -23,6 +23,7 @@ from lxml import etree as ET from manuskript.load_save.version_0 import loadFilesFromZip from manuskript.models.characterModel import CharacterInfo +from manuskript.models.outlineModel import outlineItem try: import zlib # Used with zipfile for compression @@ -573,15 +574,6 @@ def outlineToMMD(item): content += "\n\n" content += item.data(Outline.text.value) - # Saving revisions - # TODO: saving revisions? - # rev = item.revisions() - # for r in rev: - # revItem = ET.Element("revision") - # revItem.set("timestamp", str(r[0])) - # revItem.set("text", r[1]) - # item.append(revItem) - return content ######################################################################################################################## @@ -596,9 +588,6 @@ def loadProject(project, zip=None): @return: an array of errors, empty if None. """ - # FIXME: Don't forget to cache everything that is loaded - # In order to save only what has changed. - mw = mainWindow() errors = [] @@ -632,6 +621,12 @@ def loadProject(project, zip=None): with open(os.path.join(dirpath, f), mode) as fo: files[os.path.join(p, f)] = fo.read() + # Sort files by keys + files = OrderedDict(sorted(files.items())) + + # Saves to cache + cache = files + #################################################################################################################### # Settings @@ -792,21 +787,138 @@ def loadProject(project, zip=None): log("* Adds {} ({})".format(c.name(), c.ID())) + #################################################################################################################### + # Texts + # We read outline form the outline folder. If revisions are saved, then there's also a revisions.xml which contains + # everything, but the outline folder takes precedence (in cases it's been edited outside of manuksript. + + mdl = mw.mdlOutline + log("\nReading outline:") + paths = [f for f in files if "outline" in f] + outline = OrderedDict() + + # We create a structure of imbricated OrderedDict to store the whole tree. + for f in paths: + split = f.split(os.path.sep)[1:] + # log("* ", split) + + last = "" + parent = outline + for i in split: + if last: + parent = parent[last] + last = i + + if not i in parent: + # If not last item, then it is folder + if i != split[-1]: + parent[i] = OrderedDict() + + # If file, we store it + else: + parent[i] = files[f] + + # We now just have to recursively add items. + addTextItems(mdl, outline) + + # Adds revisions + if "revisions.xml" in files: + root = ET.fromstring(files["revisions.xml"]) + appendRevisions(mdl, root) + + # Check IDS + mdl.rootItem.checkIDs() + + return errors + + +def addTextItems(mdl, odict, parent=None): + """ + Adds a text / outline items from an OrderedDict. + @param mdl: model to add to + @param odict: OrderedDict + @return: nothing + """ + if parent is None: + parent = mdl.rootItem + + for k in odict: + + # In case k is a folder: + if type(odict[k]) == OrderedDict and "folder.txt" in odict[k]: + + # Adds folder + log("{}* Adds {} to {} (folder)".format(" " * parent.level(), k, parent.title())) + item = outlineFromMMD(odict[k]["folder.txt"], parent=parent) + + # Read content + addTextItems(mdl, odict[k], parent=item) + + # In case it is not + elif k != "folder.txt": + log("{}* Adds {} to {} (file)".format(" " * parent.level(), k, parent.title())) + item = outlineFromMMD(odict[k], parent=parent) + + +def outlineFromMMD(text, parent): + """ + Creates outlineItem from multimarkdown file. + @param text: content of the file + @param parent: appends item to parent (outlineItem) + @return: outlineItem + """ + + item = outlineItem(parent=parent) + md, body = parseMMDFile(text, asDict=True) + + # Store metadata + for k in md: + if k in Outline.__members__: + item.setData(Outline.__members__[k].value, str(md[k])) + + # Store body + item.setData(Outline.text.value, str(body)) + + # FIXME: add lastpath + + return item + + +def appendRevisions(mdl, root): + """ + Parse etree item to find outlineItem's with revisions, and adds them to model `mdl`. + @param mdl: outlineModel + @param root: etree + @return: nothing + """ + for child in root: + # Recursively go through items + if child.tag == "outlineItem": + appendRevisions(mdl, child) + + # Revision found. + elif child.tag == "revision": + # Get root's ID + ID = root.attrib["ID"] + if not ID: + log("* Serious problem: no ID!") + return + + # Find outline item in model + item = mdl.getItemByID(ID) + + # Store revision + log("* Appends revision ({}) to {}".format(child.attrib["timestamp"], item.title())) + item.appendRevision(child.attrib["timestamp"], child.attrib["text"]) - # if "perso.xml" in files: - # loadStandardItemModelXMLForCharacters(mw.mdlCharacter, files["perso.xml"]) - # else: - # errors.append("perso.xml") - # - # - # if "outline.xml" in files: - # mw.mdlOutline.loadFromXML(files["outline.xml"], fromString=True) - # else: - # errors.append("outline.xml") - # - # return errors def getOutlineItem(item, enum): + """ + Reads outline items from an opml file. Returns a row of QStandardItem, easy to add to a QStandardItemModel. + @param item: etree item + @param enum: enum to read keys from + @return: [QStandardItem] + """ row = getStandardItemRowFromXMLEnum(item, enum) log("* Add worldItem:", row[0].text()) for child in item: @@ -815,6 +927,7 @@ def getOutlineItem(item, enum): return row + def getStandardItemRowFromXMLEnum(item, enum): """ Reads and etree item and creates a row of QStandardItems by cross-referencing an enum. @@ -880,7 +993,7 @@ def parseMMDFile(text, asDict=False): mdd[descr] = val else: - body.append[s] + body.append(s) # We remove the second empty line (since we save with two empty lines) if body and body[0] == "": diff --git a/manuskript/models/outlineModel.py b/manuskript/models/outlineModel.py index 55d676ca..564c818b 100644 --- a/manuskript/models/outlineModel.py +++ b/manuskript/models/outlineModel.py @@ -77,9 +77,7 @@ class outlineModel(QAbstractItemModel): in columns ``columns`` (being a list of int).""" return self.rootItem.findItemsContaining(text, columns, mainWindow(), caseSensitive) - def getIndexByID(self, ID): - "Returns the index of item whose ID is ``ID``. If none, returns QModelIndex()." - + def getItemByID(self, ID): def search(item): if item.ID() == ID: return item @@ -89,6 +87,11 @@ class outlineModel(QAbstractItemModel): return r item = search(self.rootItem) + return item + + def getIndexByID(self, ID): + "Returns the index of item whose ID is ``ID``. If none, returns QModelIndex()." + item = self.getItemByID(ID) if not item: return QModelIndex() else: From fa386896db4c8ab0b0ea45fe70c858c649ec30d2 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Thu, 10 Mar 2016 14:11:28 +0100 Subject: [PATCH 32/33] Saving 2.0 works. --- manuskript/loadSave.py | 2 +- manuskript/load_save/version_1.py | 68 +++++++++++++++++++------------ 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/manuskript/loadSave.py b/manuskript/loadSave.py index 28eecb2f..0e177e6f 100644 --- a/manuskript/loadSave.py +++ b/manuskript/loadSave.py @@ -13,7 +13,7 @@ import manuskript.load_save.version_1 as v1 def saveProject(version=None): # While debugging, we don't save the project - return + # return if version == 0: v0.saveProject() diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index 6aecb198..546fa008 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -47,18 +47,9 @@ characterMap = OrderedDict([ (Character.summaryFull, "Full Summary"), (Character.notes, "Notes"), ]) -# characterMap = { -# Character.name: "Name", -# Character.ID: "ID", -# Character.importance: "Importance", -# Character.motivation: "Motivation", -# Character.goal: "Goal", -# Character.conflict: "Conflict", -# Character.summarySentence: "Phrase Summary", -# Character.summaryPara: "Paragraph Summary", -# Character.summaryFull: "Full Summary", -# Character.notes: "Notes", -# } + +# If true, logs infos while saving and loading. +LOG = False def formatMetaData(name, value, tabLength=10): @@ -99,7 +90,8 @@ def slugify(name): def log(*args): - print(" ".join(str(a) for a in args)) + if LOG: + print(" ".join(str(a) for a in args)) def saveProject(zip=None): @@ -327,6 +319,10 @@ def saveProject(zip=None): # Debug log("\nSaving to folder", folder) + # If cache is empty (meaning we haven't loaded from disk), we wipe folder, just to be sure. + if not cache: + shutil.rmtree(os.path.join(dir, folder)) + # Moving files that have been renamed for old, new in moves: @@ -358,7 +354,7 @@ def saveProject(zip=None): # Check if content is in cache, and write if necessary if path not in cache or cache[path] != content: - log("* Writing file", path) + log("* Writing file {} ({})".format(path, "not in cache" if path not in cache else "different")) mode = "w" + ("b" if type(content) == bytes else "") with open(filename, mode) as f: f.write(content) @@ -372,6 +368,10 @@ def saveProject(zip=None): if os.path.isdir(filename): shutil.rmtree(filename) + elif path == "VERSION": + # If loading from zip, but saving to path, file VERSION is not needed. + continue + else: # elif os.path.exists(filename) os.remove(filename) @@ -442,12 +442,12 @@ def addPlotItem(root, mdl, parent=QModelIndex()): index = mdl.index(x, y, parent) val = mdl.data(index) - - if not val: - continue + # + # if not val: + # continue for w in Plot: - if y == w.value: + if y == w.value and val: outline.attrib[w.name] = val # List characters as attrib @@ -459,7 +459,8 @@ def addPlotItem(root, mdl, parent=QModelIndex()): cIndex = mdl.index(cX, cY, index) characters.append(mdl.data(cIndex)) outline.attrib[Plot.characters.name] = ",".join(characters) - else: + + elif Plot.characters.name in outline.attrib: outline.attrib.pop(Plot.characters.name) # List resolution steps as sub items @@ -475,7 +476,8 @@ def addPlotItem(root, mdl, parent=QModelIndex()): if cY == w.value: step.attrib[w.name] = val - outline.attrib.pop(Plot.steps.name) + elif Plot.steps.name in outline.attrib: + outline.attrib.pop(Plot.steps.name) return root @@ -621,12 +623,13 @@ def loadProject(project, zip=None): with open(os.path.join(dirpath, f), mode) as fo: files[os.path.join(p, f)] = fo.read() + # Saves to cache (only if we loaded from disk and not zip) + global cache + cache = files + # Sort files by keys files = OrderedDict(sorted(files.items())) - # Saves to cache - cache = files - #################################################################################################################### # Settings @@ -764,6 +767,7 @@ def loadProject(project, zip=None): for f in [f for f in files if "characters" in f]: md, body = parseMMDFile(files[f]) c = mdl.addCharacter() + c.lastPath = f color = False for desc, val in md: @@ -804,13 +808,15 @@ def loadProject(project, zip=None): last = "" parent = outline + parentLastPath = "outline" for i in split: if last: parent = parent[last] + parentLastPath = os.path.join(parentLastPath, last) last = i if not i in parent: - # If not last item, then it is folder + # If not last item, then it is a folder if i != split[-1]: parent[i] = OrderedDict() @@ -818,6 +824,11 @@ def loadProject(project, zip=None): else: parent[i] = files[f] + # We store f to add it later as lastPath + parent[i + ":lastPath"] = os.path.join(parentLastPath, i) + + + # We now just have to recursively add items. addTextItems(mdl, outline) @@ -850,14 +861,19 @@ def addTextItems(mdl, odict, parent=None): # Adds folder log("{}* Adds {} to {} (folder)".format(" " * parent.level(), k, parent.title())) item = outlineFromMMD(odict[k]["folder.txt"], parent=parent) + item._lastPath = odict[k + ":lastPath"] # Read content addTextItems(mdl, odict[k], parent=item) - # In case it is not - elif k != "folder.txt": + # k is not a folder + elif type(odict[k]) == str and k != "folder.txt" and not ":lastPath" in k: log("{}* Adds {} to {} (file)".format(" " * parent.level(), k, parent.title())) item = outlineFromMMD(odict[k], parent=parent) + item._lastPath = odict[k + ":lastPath"] + + elif not ":lastPath" in k and k != "folder.txt": + print("* Strange things in file {}".format(k)) def outlineFromMMD(text, parent): From 482641c7f278e5a3a36c076f8f00893c2b6bfeaa Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Thu, 10 Mar 2016 14:15:03 +0100 Subject: [PATCH 33/33] Adds a few FIXME as roadmap --- manuskript/loadSave.py | 2 +- manuskript/load_save/version_1.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/manuskript/loadSave.py b/manuskript/loadSave.py index 0e177e6f..69f2b229 100644 --- a/manuskript/loadSave.py +++ b/manuskript/loadSave.py @@ -50,7 +50,7 @@ def loadProject(project): version = int(f.read()) print("Loading:", project) - print("Detected file format version:", version) + print("Detected file format version: {}. Zip: {}.".format(version, isZip)) if version == 0: v0.loadProject(project) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index 546fa008..f0d23de1 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -106,7 +106,7 @@ def saveProject(zip=None): """ if zip is None: zip = False - # Fixme + # FIXME: use value from settings log("\n\nSaving to:", "zip" if zip else "folder") @@ -626,6 +626,8 @@ def loadProject(project, zip=None): # Saves to cache (only if we loaded from disk and not zip) global cache cache = files + + # FIXME: watch directory for changes # Sort files by keys files = OrderedDict(sorted(files.items()))