diff --git a/manuskript/data/characters.py b/manuskript/data/characters.py
index cea33843..9b4bdf3a 100644
--- a/manuskript/data/characters.py
+++ b/manuskript/data/characters.py
@@ -4,6 +4,8 @@
import os
from collections import OrderedDict
+from collections.abc import Callable
+
from manuskript.data.color import Color
from manuskript.data.importance import Importance
from manuskript.data.unique_id import UniqueIDHost
@@ -16,6 +18,7 @@ class Character:
def __init__(self, path, characters):
self.file = MmdFile(path, 21)
self.characters = characters
+ self.links = list()
self.UID = None
self.name = None
@@ -32,14 +35,21 @@ class Character:
self.color = None
self.details = dict()
+ def link(self, callback: Callable[[int], None]):
+ self.links.append(callback)
+
+ def unlink(self, callback: Callable[[int], None]):
+ self.links.remove(callback)
+
def allowPOV(self) -> bool:
return True if self.POV is None else self.POV
def remove(self):
- if self.UID is None:
- return
+ for link in self.links:
+ link(self.UID.value)
- self.characters.removeByID(self.UID.value)
+ self.links.clear()
+ self.characters.remove(self)
@classmethod
def loadAttribute(cls, metadata: dict, name: str, defaultValue=None):
@@ -134,8 +144,9 @@ class Characters:
def getByID(self, ID: int) -> Character:
return self.data.get(ID, None)
- def removeByID(self, ID: int):
- self.data.pop(ID)
+ def remove(self, character: Character):
+ self.host.removeID(character.UID)
+ self.data.pop(character.UID.value)
def load(self):
self.data.clear()
diff --git a/manuskript/data/plots.py b/manuskript/data/plots.py
index 5fbe71d5..8de9684c 100644
--- a/manuskript/data/plots.py
+++ b/manuskript/data/plots.py
@@ -4,7 +4,7 @@
import os
from lxml import etree
-from enum import Enum, unique
+from manuskript.data.characters import Characters
from manuskript.data.importance import Importance
from manuskript.data.unique_id import UniqueIDHost, UniqueID
from manuskript.io.xmlFile import XmlFile
@@ -26,10 +26,13 @@ class PlotStep:
class PlotLine:
- def __init__(self, plots, UID: UniqueID, name: str, importance: Importance = Importance.MINOR):
+ def __init__(self, plots, UID: UniqueID, name: str = None, importance: Importance = Importance.MINOR):
self.plots = plots
self.host = UniqueIDHost()
+ if name is None:
+ name = "New plot"
+
self.UID = UID
self.name = name
self.importance = importance
@@ -38,6 +41,24 @@ class PlotLine:
self.result = ""
self.steps = list()
+ def addCharacterByID(self, ID: int):
+ character = self.plots.characters.getByID(ID)
+
+ if character is None:
+ return
+
+ character.link(self.removeCharacterByID)
+ self.characters.append(character.UID.value)
+
+ def removeCharacterByID(self, ID: int):
+ character = self.plots.characters.getByID(ID)
+
+ if character is None:
+ self.characters.remove(ID)
+ else:
+ character.unlink(self.removeCharacterByID)
+ self.characters.remove(character.UID.value)
+
def addStep(self, name: str, meta: str = "", summary: str = ""):
step = PlotStep(self, self.host.newID(), name, meta, summary)
self.steps.append(step)
@@ -61,17 +82,18 @@ class PlotLine:
class Plots:
- def __init__(self, path):
+ def __init__(self, path, characters: Characters):
self.file = XmlFile(os.path.join(path, "plots.xml"))
self.host = UniqueIDHost()
+ self.characters = characters
self.lines = dict()
- def addLine(self, name: str, importance: Importance = Importance.MINOR):
+ def addLine(self, name: str = None, importance: Importance = Importance.MINOR):
line = PlotLine(self, self.host.newID(), name, importance)
self.lines[line.UID.value] = line
return line
- def loadLine(self, ID: int, name: str, importance: Importance = Importance.MINOR):
+ def loadLine(self, ID: int, name: str = None, importance: Importance = Importance.MINOR):
line = PlotLine(self, self.host.loadID(ID), name, importance)
self.lines[line.UID.value] = line
return line
@@ -80,6 +102,9 @@ class Plots:
self.host.removeID(line.UID)
self.lines.pop(line.UID.value)
+ def getLineByID(self, ID: int) -> PlotLine:
+ return self.lines.get(ID, None)
+
def __iter__(self):
return self.lines.values().__iter__()
@@ -110,10 +135,13 @@ class Plots:
line.result = element.get("result")
for characterID in element.get("characters", "").split(','):
- #TODO: Character loadings/adding should link to models!
-
try:
- line.characters.append(int(characterID))
+ character = plots.characters.getByID(int(characterID))
+
+ if character is None:
+ continue
+
+ line.addCharacterByID(character.UID.value)
except ValueError:
continue
diff --git a/manuskript/data/project.py b/manuskript/data/project.py
index 440f79c4..39187319 100644
--- a/manuskript/data/project.py
+++ b/manuskript/data/project.py
@@ -30,7 +30,7 @@ class Project:
self.statuses = StatusHost(self.file.dir_path)
self.settings = Settings(self.file.dir_path)
self.characters = Characters(self.file.dir_path)
- self.plots = Plots(self.file.dir_path)
+ self.plots = Plots(self.file.dir_path, self.characters)
self.world = World(self.file.dir_path)
self.outline = Outline(self.file.dir_path)
self.revisions = Revisions(self.file.dir_path)
diff --git a/manuskript/ui/mainWindow.py b/manuskript/ui/mainWindow.py
index e5c768f4..7e44ad21 100644
--- a/manuskript/ui/mainWindow.py
+++ b/manuskript/ui/mainWindow.py
@@ -66,7 +66,7 @@ class MainWindow:
self.generalView = MainWindow.packViewIntoSlot(builder, "general_slot", GeneralView, self.project.info)
self.summaryView = MainWindow.packViewIntoSlot(builder, "summary_slot", SummaryView, self.project.summary)
self.charactersView = MainWindow.packViewIntoSlot(builder, "characters_slot", CharactersView, self.project.characters)
- self.plotView = MainWindow.packViewIntoSlot(builder, "plot_slot", PlotView)
+ self.plotView = MainWindow.packViewIntoSlot(builder, "plot_slot", PlotView, self.project.plots)
self.worldView = MainWindow.packViewIntoSlot(builder, "world_slot", WorldView)
self.outlineView = MainWindow.packViewIntoSlot(builder, "outline_slot", OutlineView)
self.editorView = MainWindow.packViewIntoSlot(builder, "editor_slot", EditorView)
diff --git a/manuskript/ui/views/charactersView.py b/manuskript/ui/views/charactersView.py
index 0d91bf3d..b5b6bb96 100644
--- a/manuskript/ui/views/charactersView.py
+++ b/manuskript/ui/views/charactersView.py
@@ -30,7 +30,13 @@ class CharactersView:
self.secondaryCharactersStore = builder.get_object("secondary_characters_store")
self.minorCharactersStore = builder.get_object("minor_characters_store")
+ self.filterCharactersBuffer = builder.get_object("filter_characters")
+
+ self.filterCharactersBuffer.connect("deleted-text", self.filterCharactersDeletedText)
+ self.filterCharactersBuffer.connect("inserted-text", self.filterCharactersInsertedText)
+
self.filteredCharactersStore.set_visible_func(self.filterCharacters)
+ self.filteredCharactersStore.refilter()
self.mainCharactersStore.set_visible_func(lambda model, iter, userdata: model[iter][3] == Importance.MAIN.value)
self.secondaryCharactersStore.set_visible_func(lambda model, iter, userdata: model[iter][3] == Importance.SECONDARY.value)
@@ -51,14 +57,10 @@ class CharactersView:
self.addCharacterButton = builder.get_object("add_character")
self.removeCharacterButton = builder.get_object("remove_character")
- self.filterCharactersBuffer = builder.get_object("filter_characters")
self.addCharacterButton.connect("clicked", self.addCharacterClicked)
self.removeCharacterButton.connect("clicked", self.removeCharacterClicked)
- self.filterCharactersBuffer.connect("deleted-text", self.filterCharactersDeletedText)
- self.filterCharactersBuffer.connect("inserted-text", self.filterCharactersInsertedText)
-
self.colorButton = builder.get_object("color")
self.importanceCombo = builder.get_object("importance")
self.allowPOVCheck = builder.get_object("allow_POV")
@@ -184,11 +186,15 @@ class CharactersView:
self.loadCharacterData(character)
def addCharacterClicked(self, button: Gtk.Button):
- character = self.characters.add()
+ name = invalidString(self.filterCharactersBuffer.get_text())
+ character = self.characters.add(name)
if character is None:
return
+ if self.character is not None:
+ character.importance = self.character.importance
+
self.refreshCharacterStore()
def removeCharacterClicked(self, button: Gtk.Button):
@@ -200,9 +206,9 @@ class CharactersView:
def filterCharacters(self, model, iter, userdata):
name = validString(model[iter][1])
- text = self.filterCharactersBuffer.get_text()
+ text = validString(self.filterCharactersBuffer.get_text())
- return text in name
+ return text.lower() in name.lower()
def filterCharactersChanged(self, buffer: Gtk.EntryBuffer):
self.filteredCharactersStore.refilter()
diff --git a/manuskript/ui/views/plotView.py b/manuskript/ui/views/plotView.py
index df634a8d..9337acf7 100644
--- a/manuskript/ui/views/plotView.py
+++ b/manuskript/ui/views/plotView.py
@@ -6,14 +6,248 @@ import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
+from manuskript.data import Plots, PlotLine, PlotStep, Importance
+from manuskript.ui.util import rgbaFromColor, pixbufFromColor
+from manuskript.util import validString, invalidString, validInt, invalidInt
+
class PlotView:
- def __init__(self):
+ def __init__(self, plots: Plots):
+ self.plots = plots
+ self.plotLine = None
+ self.plotStep = None
+
builder = Gtk.Builder()
builder.add_from_file("ui/plot.glade")
self.widget = builder.get_object("plot_view")
+ self.plotsStore = builder.get_object("plots_store")
+ self.refreshPlotsStore()
+
+ self.charactersStore = builder.get_object("characters_store")
+ self.refreshCharacterStore()
+
+ self.filteredPlotsStore = builder.get_object("filtered_plots_store")
+ self.mainPlotsStore = builder.get_object("main_plots_store")
+ self.secondaryPlotsStore = builder.get_object("secondary_plots_store")
+ self.minorPlotsStore = builder.get_object("minor_plots_store")
+
+ self.filterPlotsBuffer = builder.get_object("filter_plots")
+
+ self.filterPlotsBuffer.connect("deleted-text", self.filterPlotsDeletedText)
+ self.filterPlotsBuffer.connect("inserted-text", self.filterPlotsInsertedText)
+
+ self.filteredPlotsStore.set_visible_func(self.filterPlots)
+ self.filteredPlotsStore.refilter()
+
+ self.mainPlotsStore.set_visible_func(
+ lambda model, iter, userdata: model[iter][2] == Importance.MAIN.value)
+ self.secondaryPlotsStore.set_visible_func(
+ lambda model, iter, userdata: model[iter][2] == Importance.SECONDARY.value)
+ self.minorPlotsStore.set_visible_func(
+ lambda model, iter, userdata: model[iter][2] == Importance.MINOR.value)
+
+ self.mainPlotsStore.refilter()
+ self.secondaryPlotsStore.refilter()
+ self.minorPlotsStore.refilter()
+
+ self.plotSelections = [
+ builder.get_object("minor_plot_selection"),
+ builder.get_object("secondary_plot_selection"),
+ builder.get_object("main_plot_selection")
+ ]
+
+ for selection in self.plotSelections:
+ selection.connect("changed", self.plotSelectionChanged)
+
+ self.addPlotButton = builder.get_object("add_plot")
+ self.removePlotButton = builder.get_object("remove_plot")
+
+ self.addPlotButton.connect("clicked", self.addPlotClicked)
+ self.removePlotButton.connect("clicked", self.removePlotClicked)
+
+ self.importanceCombo = builder.get_object("importance")
+
+ self.nameBuffer = builder.get_object("name")
+ self.descriptionBuffer = builder.get_object("description")
+ self.resultBuffer = builder.get_object("result")
+ self.stepSummaryBuffer = builder.get_object("step_summary")
+
+ self.nameBuffer.connect("deleted-text", self.nameDeletedText)
+ self.nameBuffer.connect("inserted-text", self.nameInsertedText)
+
+ self.plotCharactersStore = builder.get_object("plot_characters_store")
+
+ self.plotCharactersStore.set_visible_func(self.filterPlotCharacters)
+ self.plotCharactersStore.refilter()
+
+ self.descriptionBuffer.connect("changed", self.descriptionChanged)
+ self.resultBuffer.connect("changed", self.resultChanged)
+ self.stepSummaryBuffer.connect("changed", self.stepSummaryChanged)
+
+ def refreshPlotsStore(self):
+ self.plotsStore.clear()
+
+ for plotLine in self.plots:
+ tree_iter = self.plotsStore.append()
+
+ if tree_iter is None:
+ continue
+
+ self.plotsStore.set_value(tree_iter, 0, plotLine.UID.value)
+ self.plotsStore.set_value(tree_iter, 1, validString(plotLine.name))
+ self.plotsStore.set_value(tree_iter, 2, Importance.asValue(plotLine.importance))
+
+ def refreshCharacterStore(self):
+ self.charactersStore.clear()
+
+ for character in self.plots.characters:
+ tree_iter = self.charactersStore.append()
+
+ if tree_iter is None:
+ continue
+
+ self.charactersStore.set_value(tree_iter, 0, character.UID.value)
+ self.charactersStore.set_value(tree_iter, 1, validString(character.name))
+ self.charactersStore.set_value(tree_iter, 2, pixbufFromColor(character.color))
+
+ def loadPlotData(self, plotLine: PlotLine):
+ self.plotLine = None
+
+ self.importanceCombo.set_active(Importance.asValue(plotLine.importance))
+
+ self.nameBuffer.set_text(validString(plotLine.name), -1)
+ self.descriptionBuffer.set_text(validString(plotLine.description), -1)
+ self.resultBuffer.set_text(validString(plotLine.result), -1)
+
+ self.plotLine = plotLine
+
+ self.plotCharactersStore.refilter()
+
+ def unloadPlotData(self):
+ self.plotLine = None
+
+ self.nameBuffer.set_text("", -1)
+ self.descriptionBuffer.set_text("", -1)
+ self.resultBuffer.set_text("", -1)
+ self.stepSummaryBuffer.set_text("", -1)
+
+ def plotSelectionChanged(self, selection: Gtk.TreeSelection):
+ model, tree_iter = selection.get_selected()
+
+ if tree_iter is None:
+ self.unloadPlotData()
+ return
+
+ for other in self.plotSelections:
+ if other != selection:
+ other.unselect_all()
+
+ plotLine = self.plots.getLineByID(model[tree_iter][0])
+
+ if plotLine is None:
+ self.unloadPlotData()
+ else:
+ self.loadPlotData(plotLine)
+
+ def addPlotClicked(self, button: Gtk.Button):
+ name = invalidString(self.filterPlotsBuffer.get_text())
+ plotLine = self.plots.addLine(name)
+
+ if plotLine is None:
+ return
+
+ if self.plotLine is not None:
+ plotLine.importance = self.plotLine.importance
+
+ self.refreshPlotsStore()
+
+ def removePlotClicked(self, button: Gtk.Button):
+ if self.plotLine is None:
+ return
+
+ self.plots.removeLine(self.plotLine)
+ self.refreshPlotsStore()
+
+ def filterPlots(self, model, iter, userdata):
+ name = validString(model[iter][1])
+ text = validString(self.filterPlotsBuffer.get_text())
+
+ return text.lower() in name.lower()
+
+ def filterPlotsChanged(self, buffer: Gtk.EntryBuffer):
+ self.filteredPlotsStore.refilter()
+
+ def filterPlotsDeletedText(self, buffer: Gtk.EntryBuffer, position: int, n_chars: int):
+ self.filterPlotsChanged(buffer)
+
+ def filterPlotsInsertedText(self, buffer: Gtk.EntryBuffer, position: int, chars: str, n_chars: int):
+ self.filterPlotsChanged(buffer)
+
+ def nameChanged(self, buffer: Gtk.EntryBuffer):
+ if self.plotLine is None:
+ return
+
+ text = buffer.get_text()
+ name = invalidString(text)
+
+ self.plotLine.name = name
+
+ plot_id = self.plotLine.UID.value
+
+ for row in self.plotsStore:
+ if row[0] == plot_id:
+ row[1] = validString(name)
+ break
+
+ def nameDeletedText(self, buffer: Gtk.EntryBuffer, position: int, n_chars: int):
+ self.nameChanged(buffer)
+
+ def nameInsertedText(self, buffer: Gtk.EntryBuffer, position: int, chars: str, n_chars: int):
+ self.nameChanged(buffer)
+
+ def filterPlotCharacters(self, model, iter, userdata):
+ ID = validInt(model[iter][0])
+
+ if self.plotLine is None:
+ return False
+
+ return ID in self.plotLine.characters
+
+ def descriptionChanged(self, buffer: Gtk.TextBuffer):
+ if self.plotLine is None:
+ return
+
+ start_iter = buffer.get_start_iter()
+ end_iter = buffer.get_end_iter()
+
+ text = buffer.get_text(start_iter, end_iter, False)
+
+ self.plotLine.description = invalidString(text)
+
+ def resultChanged(self, buffer: Gtk.TextBuffer):
+ if self.plotLine is None:
+ return
+
+ start_iter = buffer.get_start_iter()
+ end_iter = buffer.get_end_iter()
+
+ text = buffer.get_text(start_iter, end_iter, False)
+
+ self.plotLine.result = invalidString(text)
+
+ def stepSummaryChanged(self, buffer: Gtk.TextBuffer):
+ if self.plotStep is None:
+ return
+
+ start_iter = buffer.get_start_iter()
+ end_iter = buffer.get_end_iter()
+
+ text = buffer.get_text(start_iter, end_iter, False)
+
+ self.plotStep.summary = invalidString(text)
+
def show(self):
self.widget.show_all()
diff --git a/ui/plot.glade b/ui/plot.glade
index 63ea1be4..53ec8dc1 100644
--- a/ui/plot.glade
+++ b/ui/plot.glade
@@ -1,5 +1,5 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
1
@@ -430,10 +511,31 @@ along with Manuskript. If not, see .
0
0
-
+
200
True
- False
+ True
+ plot_characters_store
+ False
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ 1
+
+
+
+
@@ -508,7 +610,7 @@ along with Manuskript. If not, see .
-
+
True
False
importance_store
@@ -616,7 +718,7 @@ along with Manuskript. If not, see .
False
4
-
+
True
True
True
@@ -635,7 +737,7 @@ along with Manuskript. If not, see .
-
+
True
True
True
@@ -709,6 +811,7 @@ along with Manuskript. If not, see .
True
True
word-char
+ step_summary