manuskript/manuskript/ui/views/storylineView.py
2016-03-31 15:58:13 +02:00

328 lines
11 KiB
Python

#!/usr/bin/env python
# --!-- coding: utf8 --!--
from PyQt5.QtCore import Qt, QTimer, QRectF
from PyQt5.QtGui import QBrush, QPen, QFontMetrics, QFontMetricsF, QColor
from PyQt5.QtWidgets import QWidget, QGraphicsScene, QGraphicsSimpleTextItem, QMenu, QAction, QGraphicsRectItem, \
QGraphicsLineItem, QGraphicsEllipseItem
from manuskript.enums import Outline
from manuskript.models import references
from manuskript.ui.views.storylineView_ui import Ui_storylineView
class storylineView(QWidget, Ui_storylineView):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setupUi(self)
self._mdlPlots = None
self.scene = QGraphicsScene()
self.view.setScene(self.scene)
self.reloadTimer = QTimer()
self.reloadTimer.timeout.connect(self.refresh)
self.reloadTimer.setSingleShot(True)
self.reloadTimer.setInterval(500)
self.btnRefresh.clicked.connect(self.refresh)
self.sldTxtSize.sliderMoved.connect(self.reloadTimer.start)
self.generateMenu()
def generateMenu(self):
m = QMenu()
self.actPlots = QAction(self.tr("Show Plots"), m)
self.actPlots.setCheckable(True)
self.actPlots.setChecked(True)
self.actPlots.toggled.connect(self.reloadTimer.start)
m.addAction(self.actPlots)
self.actCharacters = QAction(self.tr("Show Characters"), m)
self.actCharacters.setCheckable(True)
self.actCharacters.setChecked(False)
self.actCharacters.toggled.connect(self.reloadTimer.start)
m.addAction(self.actCharacters)
self.btnSettings.setMenu(m)
def setModels(self, mdlOutline, mdlCharacter, mdlPlots):
self._mdlPlots = mdlPlots
# self._mdlPlots.dataChanged.connect(self.refresh)
# self._mdlPlots.rowsInserted.connect(self.refresh)
self._mdlOutline = mdlOutline
self._mdlOutline.dataChanged.connect(self.updateMaybe)
self._mdlCharacter = mdlCharacter
self._mdlCharacter.dataChanged.connect(self.reloadTimer.start)
def updateMaybe(self, topLeft, bottomRight):
if topLeft.column() <= Outline.notes.value <= bottomRight.column():
self.reloadTimer.start
def plotReferences(self):
"Returns a list of plot references"
if not self._mdlPlots:
pass
plotsID = self._mdlPlots.getPlotsByImportance()
r = []
for importance in plotsID:
for ID in importance:
ref = references.plotReference(ID)
r.append(ref)
return r
def charactersReferences(self):
"Returns a list of character references"
if not self._mdlCharacter:
pass
chars = self._mdlCharacter.getCharactersByImportance()
r = []
for importance in chars:
for c in importance:
ref = references.characterReference(c.ID())
r.append(ref)
return r
def refresh(self):
if not self._mdlPlots or not self._mdlOutline or not self._mdlCharacter:
return
if not self.isVisible():
return
LINE_HEIGHT = 18
SPACING = 3
TEXT_WIDTH = self.sldTxtSize.value()
CIRCLE_WIDTH = 10
LEVEL_HEIGHT = 12
s = self.scene
s.clear()
# Get Max Level (max depth)
root = self._mdlOutline.rootItem
def maxLevel(item, level=0, max=0):
if level > max:
max = level
for c in item.children():
m = maxLevel(c, level + 1)
if m > max:
max = m
return max
MAX_LEVEL = maxLevel(root)
# Get the list of tracked items (array of references)
trackedItems = []
if self.actPlots.isChecked():
trackedItems += self.plotReferences()
if self.actCharacters.isChecked():
trackedItems += self.charactersReferences()
ROWS_HEIGHT = len(trackedItems) * (LINE_HEIGHT + SPACING )
fm = QFontMetrics(s.font())
max_name = 0
for ref in trackedItems:
name = references.title(ref)
max_name = max(fm.width(name), max_name)
TITLE_WIDTH = max_name + 2 * SPACING
# Add Folders and Texts
outline = OutlineRect(0, 0, 0, ROWS_HEIGHT + SPACING + MAX_LEVEL * LEVEL_HEIGHT)
s.addItem(outline)
outline.setPos(TITLE_WIDTH + SPACING, 0)
refCircles = [] # a list of all references, to be added later on the lines
# A Function to add a rect with centered elided text
def addRectText(x, w, parent, text="", level=0, tooltip=""):
deltaH = LEVEL_HEIGHT if level else 0
r = OutlineRect(0, 0, w, parent.rect().height()-deltaH, parent, title=text)
r.setPos(x, deltaH)
txt = QGraphicsSimpleTextItem(text, r)
f = txt.font()
f.setPointSize(8)
fm = QFontMetricsF(f)
elidedText = fm.elidedText(text, Qt.ElideMiddle, w)
txt.setFont(f)
txt.setText(elidedText)
txt.setPos(r.boundingRect().center() - txt.boundingRect().center())
txt.setY(0)
return r
# A function to returns an item's width, by counting its children
def itemWidth(item):
if item.isFolder():
r = 0
for c in item.children():
r += itemWidth(c)
return r or TEXT_WIDTH
else:
return TEXT_WIDTH
def listItems(item, rect, level=0):
delta = 0
for child in item.children():
w = itemWidth(child)
if child.isFolder():
parent = addRectText(delta, w, rect, child.title(), level, tooltip=child.title())
parent.setToolTip(references.tooltip(references.textReference(child.ID())))
listItems(child, parent, level + 1)
else:
rectChild = addRectText(delta, TEXT_WIDTH, rect, "", level, tooltip=child.title())
rectChild.setToolTip(references.tooltip(references.textReference(child.ID())))
# Find tracked references in that scene (or parent folders)
for ref in trackedItems:
result = []
# Tests if POV
scenePOV = False # Will hold true of character is POV of the current text, not containing folder
if references.type(ref) == references.CharacterLetter:
ID = references.ID(ref)
c = child
while c:
if c.POV() == ID:
result.append(c.ID())
if c == child: scenePOV = True
c = c.parent()
# Search in notes/references
c = child
while c:
result += references.findReferencesTo(ref, c, recursive=False)
c = c.parent()
if result:
ref2 = result[0]
# Create a RefCircle with the reference
c = RefCircle(TEXT_WIDTH / 2, - CIRCLE_WIDTH / 2, CIRCLE_WIDTH, ID=ref2, important=scenePOV)
# Store it, with the position of that item, to display it on the line later on
refCircles.append((ref, c, rect.mapToItem(outline, rectChild.pos())))
delta += w
listItems(root, outline)
OUTLINE_WIDTH = itemWidth(root)
# Add Tracked items
i = 0
itemsRect = s.addRect(0, 0, 0, 0)
itemsRect.setPos(0, MAX_LEVEL * LEVEL_HEIGHT + SPACING)
# Set of colors for plots (as long as they don't have their own colors)
colors = [
"#D97777", "#AE5F8C", "#D9A377", "#FFC2C2", "#FFDEC2", "#D2A0BC",
"#7B0F0F", "#7B400F", "#620C3D", "#AA3939", "#AA6C39", "#882D61",
"#4C0000", "#4C2200", "#3D0022",
]
for ref in trackedItems:
if references.type(ref) == references.CharacterLetter:
color = self._mdlCharacter.getCharacterByID(references.ID(ref)).color()
else:
color = QColor(colors[i % len(colors)])
# Rect
r = QGraphicsRectItem(0, 0, TITLE_WIDTH, LINE_HEIGHT, itemsRect)
r.setPen(QPen(Qt.NoPen))
r.setBrush(QBrush(color))
r.setPos(0, i * LINE_HEIGHT + i * SPACING)
r.setToolTip(references.tooltip(ref))
i += 1
# Text
name = references.title(ref)
txt = QGraphicsSimpleTextItem(name, r)
txt.setPos(r.boundingRect().center() - txt.boundingRect().center())
# Line
line = PlotLine(0, 0,
OUTLINE_WIDTH + SPACING, 0)
line.setPos(TITLE_WIDTH, r.mapToScene(r.rect().center()).y())
s.addItem(line)
line.setPen(QPen(color, 5))
line.setToolTip(references.tooltip(ref))
# We add the circles / references to text, on the line
for ref2, circle, pos in refCircles:
if ref2 == ref:
circle.setParentItem(line)
circle.setPos(pos.x(), 0)
# self.view.fitInView(0, 0, TOTAL_WIDTH, i * LINE_HEIGHT, Qt.KeepAspectRatioByExpanding) # KeepAspectRatio
self.view.setSceneRect(0, 0, 0, 0)
class OutlineRect(QGraphicsRectItem):
def __init__(self, x, y, w, h, parent=None, title=None):
QGraphicsRectItem.__init__(self, x, y, w, h, parent)
self.setBrush(Qt.white)
self.setAcceptHoverEvents(True)
self._title = title
def hoverEnterEvent(self, event):
self.setBrush(Qt.lightGray)
def hoverLeaveEvent(self, event):
self.setBrush(Qt.white)
class RefCircle(QGraphicsEllipseItem):
def __init__(self, x, y, diameter, parent=None, ID=None, important=False):
QGraphicsEllipseItem.__init__(self, x, y, diameter, diameter, parent)
self.setBrush(Qt.white)
self._ref = references.textReference(ID)
self.setToolTip(references.tooltip(self._ref))
self.setPen(QPen(Qt.black, 2))
self.setAcceptHoverEvents(True)
if important:
self.setBrush(Qt.black)
def multiplyDiameter(self, factor):
r1 = self.rect()
r2 = QRectF(0, 0, r1.width() * factor, r1.height() * factor)
self.setRect(r2)
self.setPos(self.pos() + r1.center() - r2.center())
def mouseDoubleClickEvent(self, event):
references.open(self._ref)
def hoverEnterEvent(self, event):
self.multiplyDiameter(2)
def hoverLeaveEvent(self, event):
self.multiplyDiameter(.5)
class PlotLine(QGraphicsLineItem):
def __init__(self, x1, y1, x2, y2, parent=None):
QGraphicsLineItem.__init__(self, x1, y1, x2, y2, parent)
self.setAcceptHoverEvents(True)
def hoverEnterEvent(self, QGraphicsSceneHoverEvent):
p = self.pen()
p.setWidth(10)
self.setPen(p)
def hoverLeaveEvent(self, QGraphicsSceneHoverEvent):
p = self.pen()
p.setWidth(5)
self.setPen(p)