Checkpoint: refactoring

This commit is contained in:
Olivier Keshavjee 2017-11-17 17:38:06 +01:00
parent ad01de4cd4
commit 04fc6a5ae4
9 changed files with 255 additions and 192 deletions

View file

@ -353,3 +353,17 @@ def customIcons():
def statusMessage(message, duration=5000):
mainWindow().statusBar().showMessage(message, duration)
def inspect():
"""
Debugging tool. Call it to see a stack of calls up to that point.
"""
import inspect, os
print("-----------------------")
for s in inspect.stack()[1:]:
print(" * {}:{} // {}".format(
os.path.basename(s.filename),
s.lineno,
s.function))
print(" " + "".join(s.code_context))

View file

@ -1,8 +1,6 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
import locale
from PyQt5.QtCore import QAbstractItemModel, QMimeData
from PyQt5.QtCore import QModelIndex
from PyQt5.QtCore import QSize
@ -10,22 +8,9 @@ from PyQt5.QtCore import QVariant
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QFont
from PyQt5.QtWidgets import QTextEdit, qApp
from manuskript import settings
from lxml import etree as ET
from manuskript.enums import Outline
from manuskript import enums
from manuskript.functions import mainWindow, toInt, wordCount
from manuskript.converters import HTML2PlainText
try:
locale.setlocale(locale.LC_ALL, '')
except:
# Invalid locale, but not really a big deal because it's used only for
# number formating
pass
import os
class abstractItem():
@ -53,12 +38,12 @@ class abstractItem():
if xml is not None:
self.setFromXML(xml)
if parent:
parent.appendChild(self)
if ID:
self._data[self.enum.ID] = ID
if parent:
parent.appendChild(self)
#######################################################################
# Model
#######################################################################
@ -107,7 +92,7 @@ class abstractItem():
return self._data.get(self.enum.title, "")
def ID(self):
return self._data.get(self.enum.ID, 0)
return self._data.get(self.enum.ID)
def columnCount(self):
return len(self.enum)
@ -197,7 +182,7 @@ class abstractItem():
###############################################################################
def getUniqueID(self, recursive=False):
self.setData(Outline.ID, self._model.rootItem.findUniqueID())
self.setData(self.enum.ID, self._model.rootItem.findUniqueID())
if recursive:
for c in self.children():
@ -241,109 +226,38 @@ class abstractItem():
#######################################################################
def data(self, column, role=Qt.DisplayRole):
# print("Data: ", column, role)
# Return value in self._data
if role == Qt.DisplayRole or role == Qt.EditRole:
# if column == Outline.compile:
# return self.data(column, Qt.CheckStateRole)
return self._data.get(column, "")
if Outline(column) in self._data:
return self._data[Outline(column)]
elif column == Outline.revisions:
return []
else:
return ""
elif role == Qt.DecorationRole and column == Outline.title:
if self.customIcon():
return QIcon.fromTheme(self.data(Outline.customIcon))
if self.isFolder():
return QIcon.fromTheme("folder")
elif self.isMD():
return QIcon.fromTheme("text-x-generic")
# elif role == Qt.ForegroundRole:
# if self.isCompile() in [0, "0"]:
# return QBrush(Qt.gray)
elif role == Qt.CheckStateRole and column == Outline.compile:
# print(self.title(), self.compile())
# if self._data[Outline(column)] and not self.compile():
# return Qt.PartiallyChecked
# else:
return self._data[Outline(column)]
elif role == Qt.FontRole:
f = QFont()
if column == Outline.wordCount and self.isFolder():
f.setItalic(True)
elif column == Outline.goal and self.isFolder() and self.data(Outline.setGoal) == None:
f.setItalic(True)
if self.isFolder():
f.setBold(True)
return f
# Or return QVariant
return QVariant()
def setData(self, column, data, role=Qt.DisplayRole):
if role not in [Qt.DisplayRole, Qt.EditRole, Qt.CheckStateRole]:
print(column, column == Outline.text, data, role)
return
if column == Outline.text and self.isFolder():
# Folder have no text
return
if column == Outline.goal:
self._data[Outline.setGoal] = toInt(data) if toInt(data) > 0 else ""
# Checking if we will have to recount words
updateWordCount = False
if column in [Outline.wordCount, Outline.goal, Outline.setGoal]:
updateWordCount = not Outline(column) in self._data or self._data[Outline(column)] != data
# Stuff to do before
if column == Outline.text:
self.addRevision()
# Setting data
self._data[Outline(column)] = data
self._data[column] = data
# Stuff to do afterwards
if column == Outline.text:
wc = wordCount(data)
self.setData(Outline.wordCount, wc)
self.emitDataChanged(cols=[Outline.text]) # new in 0.5.0
if column == Outline.compile:
self.emitDataChanged(cols=[Outline.title, Outline.compile], recursive=True)
if column == Outline.customIcon:
# If custom icon changed, we tell views to update title (so that icons
# will be updated as well)
self.emitDataChanged(cols=[Outline.title])
if updateWordCount:
self.updateWordCount()
# Emit signal
self.emitDataChanged(cols=[column]) # new in 0.5.0
###############################################################################
# XML
###############################################################################
# We don't want to write some datas (computed)
XMLExclude = [Outline.wordCount, Outline.goal, Outline.goalPercentage, Outline.revisions]
XMLExclude = []
# We want to force some data even if they're empty
XMLForce = [Outline.compile]
XMLForce = []
def toXML(self):
"""
Returns a string containing the item (and children) in XML.
By default, saves all attributes from self.enum and lastPath.
You can define in XMLExclude and XMLForce what you want to be
excluded or forcibly included.
"""
item = ET.Element(self.name)
## We don't want to write some datas (computed)
#exclude = [Outline.wordCount, Outline.goal, Outline.goalPercentage, Outline.revisions]
## We want to force some data even if they're empty
#force = [Outline.compile]
for attrib in self.enum:
if attrib in self.XMLExclude:
continue
@ -351,72 +265,42 @@ class abstractItem():
if val or attrib in self.XMLForce:
item.set(attrib.name, str(val))
# Saving revisions
rev = self.revisions()
for r in rev:
revItem = ET.Element("revision")
revItem.set("timestamp", str(r[0]))
revItem.set("text", r[1])
item.append(revItem)
# Saving lastPath
item.set("lastPath", self._lastPath)
# Additional stuff for subclasses
item = self.toXMLProcessItem(item)
for i in self.childItems:
item.append(ET.XML(i.toXML()))
return ET.tostring(item)
def toXML_(self):
item = ET.Element("outlineItem")
for attrib in Outline:
if attrib in exclude: continue
val = self.data(attrib.value)
if val or attrib in force:
item.set(attrib.name, str(val))
# Saving revisions
rev = self.revisions()
for r in rev:
revItem = ET.Element("revision")
revItem.set("timestamp", str(r[0]))
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()))
return ET.tostring(item)
def toXMLProcessItem(self, item):
"""
Subclass this to change the behavior of `toXML`.
"""
return item
def setFromXML(self, xml):
root = ET.XML(xml)
for k in root.attrib:
if k in Outline.__members__:
# if k == Outline.compile:
# self.setData(Outline.__members__[k], unicode(root.attrib[k]), Qt.CheckStateRole)
# else:
self.setData(Outline.__members__[k], str(root.attrib[k]))
for k in self.enum:
if k.name in root.attrib:
self.setData(k, str(root.attrib[k.name]))
if "lastPath" in root.attrib:
self._lastPath = root.attrib["lastPath"]
# If loading from an old file format, convert to md and remove html markup
if self.type() in ["txt", "t2t"]:
self.setData(Outline.type, "md")
elif self.type() == "html":
self.setData(Outline.type, "md")
self.setData(Outline.text, HTML2PlainText(self.data(Outline.text)))
self.setData(Outline.notes, HTML2PlainText(self.data(Outline.notes)))
self.setFromXMLProcessMore(root)
for child in root:
if child.tag == "outlineItem":
item = outlineItem(self._model, xml=ET.tostring(child), parent=self)
elif child.tag == "revision":
self.appendRevision(child.attrib["timestamp"], child.attrib["text"])
if child.tag == self.name:
item = self.__class__(self._model, xml=ET.tostring(child), parent=self)
def setFromXMLProcessMore(self, root):
"""
Additional stuff that subclasses must do with the XML to restore
item.
"""
return

View file

@ -16,7 +16,6 @@ from lxml import etree as ET
from manuskript.enums import Outline
from manuskript.functions import mainWindow, toInt, wordCount
from manuskript.converters import HTML2PlainText
from manuskript.models import outlineItem
try:
@ -105,13 +104,13 @@ class abstractModel(QAbstractItemModel):
item = search(self.rootItem)
return item
def getIndexByID(self, ID):
def getIndexByID(self, ID, column=0):
"Returns the index of item whose ID is `ID`. If none, returns QModelIndex()."
item = self.getItemByID(ID)
if not item:
return QModelIndex()
else:
return self.indexFromItem(item)
return self.indexFromItem(item, column)
def parent(self, index=QModelIndex()):
if not index.isValid():
@ -369,6 +368,7 @@ class abstractModel(QAbstractItemModel):
return False
items = self.decodeMimeData(data)
if items is None:
return False

View file

@ -1,12 +1,24 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
import time
import locale
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QIcon
from PyQt5.QtWidgets import qApp
from lxml import etree as ET
from manuskript.models.abstractItem import abstractItem
from manuskript import enums
from manuskript.functions import mainWindow, toInt
from manuskript import functions as F
from manuskript import settings
import time
from manuskript.converters import HTML2PlainText
try:
locale.setlocale(locale.LC_ALL, '')
except:
# Invalid locale, but not really a big deal because it's used only for
# number formating
pass
class outlineItem(abstractItem):
@ -20,7 +32,8 @@ class outlineItem(abstractItem):
abstractItem.__init__(self, model, title, _type, xml, parent, ID)
self.defaultTextType = None
self._data[self.enum.compile] = Qt.Checked
if not self._data.get(self.enum.compile):
self._data[self.enum.compile] = 2
#######################################################################
# Properties
@ -42,7 +55,7 @@ class outlineItem(abstractItem):
return self.data(self.enum.text)
def compile(self):
if self._data[self.enum.compile] in ["0", 0]:
if self._data.get(self.enum.compile, 1) in ["0", 0]:
return False
elif self.parent():
return self.parent().compile()
@ -64,6 +77,87 @@ class outlineItem(abstractItem):
def setCustomIcon(self, customIcon):
self.setData(self.enum.customIcon, customIcon)
def wordCount(self):
return self._data.get(self.enum.wordCount, 0)
#######################################################################
# Data
#######################################################################
def data(self, column, role=Qt.DisplayRole):
data = abstractItem.data(self, column, role)
E = self.enum
if role == Qt.DisplayRole or role == Qt.EditRole:
if column == E.revisions:
return []
else:
return data
elif role == Qt.DecorationRole and column == E.title:
if self.customIcon():
return QIcon.fromTheme(self.data(E.customIcon))
if self.isFolder():
return QIcon.fromTheme("folder")
elif self.isText():
return QIcon.fromTheme("text-x-generic")
elif role == Qt.CheckStateRole and column == E.compile:
return Qt.Checked if self.compile() else Qt.Unchecked
elif role == Qt.FontRole:
f = QFont()
if column == E.wordCount and self.isFolder():
f.setItalic(True)
elif column == E.goal and self.isFolder() and not self.data(E.setGoal):
f.setItalic(True)
if self.isFolder():
f.setBold(True)
return f
def setData(self, column, data, role=Qt.DisplayRole):
E = self.enum
if column == E.text and self.isFolder():
# Folder have no text
return
if column == E.goal:
self._data[E.setGoal] = F.toInt(data) if F.toInt(data) > 0 else ""
# Checking if we will have to recount words
updateWordCount = False
if column in [E.wordCount, E.goal, E.setGoal]:
updateWordCount = not column in self._data or self._data[column] != data
# Stuff to do before
if column == E.text:
self.addRevision()
# Calling base class implementation
abstractItem.setData(self, column, data, role)
# Stuff to do afterwards
if column == E.text:
wc = F.wordCount(data)
self.setData(E.wordCount, wc)
if column == E.compile:
# Title changes when compile changes
self.emitDataChanged(cols=[E.title, E.compile],
recursive=True)
if column == E.customIcon:
# If custom icon changed, we tell views to update title (so that
# icons will be updated as well)
self.emitDataChanged(cols=[E.title])
if updateWordCount:
self.updateWordCount()
#######################################################################
# Wordcount
#######################################################################
@ -82,23 +176,23 @@ class outlineItem(abstractItem):
"""Update word count for item and parents.
If emit is False, no signal is emitted (sometimes cause segfault)"""
if not self.isFolder():
setGoal = toInt(self.data(self.enum.setGoal))
goal = toInt(self.data(self.enum.goal))
setGoal = F.toInt(self.data(self.enum.setGoal))
goal = F.toInt(self.data(self.enum.goal))
if goal != setGoal:
self._data[self.enum.goal] = setGoal
if setGoal:
wc = toInt(self.data(self.enum.wordCount))
wc = F.toInt(self.data(self.enum.wordCount))
self.setData(self.enum.goalPercentage, wc / float(setGoal))
else:
wc = 0
for c in self.children():
wc += toInt(c.data(self.enum.wordCount))
wc += F.toInt(c.data(self.enum.wordCount))
self._data[self.enum.wordCount] = wc
setGoal = toInt(self.data(self.enum.setGoal))
goal = toInt(self.data(self.enum.goal))
setGoal = F.toInt(self.data(self.enum.setGoal))
goal = F.toInt(self.data(self.enum.goal))
if setGoal:
if goal != setGoal:
@ -107,7 +201,7 @@ class outlineItem(abstractItem):
else:
goal = 0
for c in self.children():
goal += toInt(c.data(self.enum.goal))
goal += F.toInt(c.data(self.enum.goal))
self._data[self.enum.goal] = goal
if goal:
@ -123,9 +217,9 @@ class outlineItem(abstractItem):
self.parent().updateWordCount(emit)
def stats(self):
wc = self.data(Outline.wordCount)
goal = self.data(Outline.goal)
progress = self.data(Outline.goalPercentage)
wc = self.data(enums.Outline.wordCount)
goal = self.data(enums.Outline.goal)
progress = self.data(enums.Outline.goalPercentage)
if not wc:
wc = 0
if goal:
@ -242,7 +336,8 @@ class outlineItem(abstractItem):
return lst
def findItemsContaining(self, text, columns, mainWindow=mainWindow(), caseSensitive=False, recursive=True):
def findItemsContaining(self, text, columns, mainWindow=F.mainWindow(),
caseSensitive=False, recursive=True):
"""Returns a list if IDs of all subitems
containing ``text`` in columns ``columns``
(being a list of int).
@ -255,7 +350,8 @@ class outlineItem(abstractItem):
return lst
def itemContains(self, text, columns, mainWindow=mainWindow(), caseSensitive=False):
def itemContains(self, text, columns, mainWindow=F.mainWindow(),
caseSensitive=False):
lst = []
text = text.lower() if not caseSensitive else text
for c in columns:
@ -269,10 +365,10 @@ class outlineItem(abstractItem):
print("Character POV not found:", self.POV())
elif c == self.enum.status:
searchIn = mainWindow.mdlStatus.item(toInt(self.status()), 0).text()
searchIn = mainWindow.mdlStatus.item(F.toInt(self.status()), 0).text()
elif c == self.enum.label:
searchIn = mainWindow.mdlLabels.item(toInt(self.label()), 0).text()
searchIn = mainWindow.mdlLabels.item(F.toInt(self.label()), 0).text()
else:
searchIn = self.data(c)
@ -309,7 +405,7 @@ class outlineItem(abstractItem):
self.appendRevision(
time.time(),
self._data[self.enum.text])
self.text())
if settings.revisions["smartremove"]:
self.cleanRevisions()
@ -358,7 +454,44 @@ class outlineItem(abstractItem):
self._data[self.enum.revisions] = rev2
self.emitDataChanged([self.enum.revisions])
#######################################################################
# XML
#######################################################################
# We don't want to write some datas (computed)
XMLExclude = [enums.Outline.wordCount,
enums.Outline.goal,
enums.Outline.goalPercentage,
enums.Outline.revisions]
# We want to force some data even if they're empty
XMLForce = [enums.Outline.compile]
def toXMLProcessItem(self, item):
# Saving revisions
rev = self.revisions()
for r in rev:
revItem = ET.Element("revision")
revItem.set("timestamp", str(r[0]))
revItem.set("text", r[1])
item.append(revItem)
return item
def setFromXMLProcessMore(self, root):
# If loading from an old file format, convert to md and
# remove html markup
if self.type() in ["txt", "t2t"]:
self.setData(Outline.type, "md")
elif self.type() == "html":
self.setData(Outline.type, "md")
self.setData(Outline.text, HTML2PlainText(self.data(Outline.text)))
self.setData(Outline.notes, HTML2PlainText(self.data(Outline.notes)))
# Revisions
for child in root:
if child.tag == "revision":
self.appendRevision(child.attrib["timestamp"], child.attrib["text"])

View file

@ -43,13 +43,7 @@ class chkOutlineCompile(QCheckBox):
def getCheckedValue(self, index):
item = index.internalPointer()
c = item.data(Outline.compile)
if c:
c = int(c)
else:
c = Qt.Unchecked
return c
return Qt.Checked if item.compile() else Qt.Unchecked
def update(self, topLeft, bottomRight):

View file

@ -31,8 +31,10 @@ class outlineBasics(QAbstractItemView):
if event.button() == Qt.RightButton:
self.menu = self.makePopupMenu()
self.menu.popup(event.globalPos())
else:
QAbstractItemView.mouseReleaseEvent(self, event)
# We don't call QAbstractItemView.mouseReleaseEvent because
# outlineBasics is never subclassed alone. So the others views
# (outlineView, corkView, treeView) that subclass outlineBasics
# call their respective mother class.
def makePopupMenu(self):
index = self.currentIndex()

View file

@ -195,6 +195,9 @@ class outlineCompileDelegate(QStyledItemDelegate):
def displayText(self, value, locale):
return ""
#def createEditor(self, parent, option, index):
#return None
class outlineGoalPercentageDelegate(QStyledItemDelegate):
def __init__(self, rootIndex=None, parent=None):

View file

@ -39,6 +39,10 @@ class textEditView(QTextEdit):
self.setAcceptRichText(False)
# When setting up a theme, this becomes true.
self._fromTheme = False
# Sometimes we need to update index because item has changed its
# position, so we only have it's ID as reference. We store it to
# update at the propper time.
self._updateIndexFromID = None
self.spellcheck = spellcheck
self.currentDict = dict if dict else settings.dict
@ -271,7 +275,15 @@ class textEditView(QTextEdit):
if self._updating:
return
elif self._index and self._index.isValid():
if self._updateIndexFromID:
# We have to update to a new index
self._index = self._index.model().getIndexByID(
self._updateIndexFromID,
self._column)
self._updateIndexFromID = None
if self._index and self._index.isValid():
if topLeft.parent() != self._index.parent():
return
@ -294,13 +306,34 @@ class textEditView(QTextEdit):
self.updateText()
def rowsAboutToBeRemoved(self, parent, first, last):
if self._index:
if self._index and self._index.isValid():
# Has my _index just been removed?
if self._index.parent() == parent and \
first <= self._index.row() <= last:
self._index = None
self.setEnabled(False)
return
# FIXME: self._indexes
# We check if item is a child of the row about to be removed
child = False
p = self._index.parent()
while p:
if p == parent:
child = True
p = None
elif p.isValid():
p = p.parent()
else:
p = None
if child:
# Item might have moved (so will not be valid any more)
ID = self._index.internalPointer().ID()
# We store ID, and we update it in self.update (after the
# rows have been removed).
self._updateIndexFromID = ID
def disconnectDocument(self):
try:
self.document().contentsChanged.disconnect(self.updateTimer.start)

View file

@ -94,7 +94,7 @@ class treeTitleDelegate(QStyledItemDelegate):
# If text color is Compile and item is selected, we have
# to change the color
if settings.viewSettings["Outline"]["Text"] == "Compile" and \
item.compile() in [0, "0"]:
not item.compile():
col = mixColors(textColor, QColor(S.window))
painter.setPen(col)
f = QFont(opt.font)
@ -109,7 +109,7 @@ class treeTitleDelegate(QStyledItemDelegate):
extraText = item.childCount()
extraText = " [{}]".format(extraText)
elif settings.viewSettings["Tree"]["InfoFolder"] == "WC":
extraText = item.data(Outline.wordCount)
extraText = item.wordCount()
extraText = " ({})".format(extraText)
elif settings.viewSettings["Tree"]["InfoFolder"] == "Progress":
extraText = int(toFloat(item.data(Outline.goalPercentage)) * 100)
@ -122,7 +122,7 @@ class treeTitleDelegate(QStyledItemDelegate):
if item.isText() and settings.viewSettings["Tree"]["InfoText"] != "Nothing":
if settings.viewSettings["Tree"]["InfoText"] == "WC":
extraText = item.data(Outline.wordCount)
extraText = item.wordCount()
extraText = " ({})".format(extraText)
elif settings.viewSettings["Tree"]["InfoText"] == "Progress":
extraText = int(toFloat(item.data(Outline.goalPercentage)) * 100)