From cfa745229261f84c8f95b70cac00a692fc6515e2 Mon Sep 17 00:00:00 2001 From: TheJackiMonster Date: Sat, 29 Oct 2022 18:42:55 +0200 Subject: [PATCH] Start implementing plot view Signed-off-by: TheJackiMonster --- manuskript/data/characters.py | 21 ++- manuskript/data/plots.py | 44 ++++- manuskript/data/project.py | 2 +- manuskript/ui/mainWindow.py | 2 +- manuskript/ui/views/charactersView.py | 20 ++- manuskript/ui/views/plotView.py | 236 +++++++++++++++++++++++++- ui/plot.glade | 169 ++++++++++++++---- 7 files changed, 438 insertions(+), 56 deletions(-) diff --git a/manuskript/data/characters.py b/manuskript/data/characters.py index cea3384..9b4bdf3 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 5fbe71d..8de9684 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 440f79c..3918731 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 e5c768f..7e44ad2 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 0d91bf3..b5b6bb9 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 df634a8..9337acf 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 63ea1be..53ec8dc 100644 --- a/ui/plot.glade +++ b/ui/plot.glade @@ -1,5 +1,5 @@ - + + + + + + + + + + + + characters_store + + + + + Main + 2 Secondary + 1 Minor + 0 + + + + + + + + + + + + + plots_store + + + filtered_plots_store + + + filtered_plots_store + + + filtered_plots_store + @@ -59,6 +102,8 @@ along with Manuskript. If not, see . + + True True @@ -109,9 +154,24 @@ along with Manuskript. If not, see . - + True - False + True + main_plots_store + False + + + + + + + + + 1 + + + + False @@ -132,9 +192,24 @@ along with Manuskript. If not, see . - + True - False + True + secondary_plots_store + False + + + + + + + + + 1 + + + + False @@ -155,12 +230,27 @@ along with Manuskript. If not, see . - + True - False + True + minor_plots_store + False + + + + + + + + + 1 + + + + - True + False True 5 @@ -185,7 +275,7 @@ along with Manuskript. If not, see . 8 4 - + True True True @@ -204,7 +294,7 @@ along with Manuskript. If not, see . - + True True True @@ -226,6 +316,7 @@ along with Manuskript. If not, see . True True + filter_plots Filter @@ -265,7 +356,7 @@ along with Manuskript. If not, see . 8 8 - + True False start @@ -279,7 +370,7 @@ along with Manuskript. If not, see . - + True False start @@ -306,6 +397,7 @@ along with Manuskript. If not, see . True True word-char + result @@ -331,6 +423,7 @@ along with Manuskript. If not, see . True True word-char + description @@ -343,7 +436,7 @@ along with Manuskript. If not, see . - + True False start @@ -357,7 +450,7 @@ along with Manuskript. If not, see . - + True False Importance @@ -369,7 +462,7 @@ along with Manuskript. If not, see . - + True False Plot @@ -389,6 +482,7 @@ along with Manuskript. If not, see . True True + name True @@ -396,19 +490,6 @@ along with Manuskript. If not, see . 1 - - - True - True - True - - - False - True - end - 2 - - 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