mirror of
https://github.com/olivierkes/manuskript.git
synced 2024-09-28 07:21:31 +12:00
331 lines
12 KiB
Python
331 lines
12 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
|
|
import manuskript.functions as F
|
|
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.setIcon(F.themeIcon("plots"))
|
|
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.setIcon(F.themeIcon("characters"))
|
|
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 <= 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)
|