From 3481e40b4fe504764d917e9f0cbe0013a13d645e Mon Sep 17 00:00:00 2001 From: TheJackiMonster Date: Sun, 30 Oct 2022 17:48:02 +0100 Subject: [PATCH] Implement basic outline overview Signed-off-by: TheJackiMonster --- manuskript/data/__init__.py | 4 +- manuskript/data/goal.py | 17 +- manuskript/data/outline.py | 45 ++++- manuskript/data/project.py | 2 +- manuskript/ui/mainWindow.py | 2 +- manuskript/ui/views/outlineView.py | 257 ++++++++++++++++++++++++++++- manuskript/ui/views/worldView.py | 2 +- manuskript/util/__init__.py | 25 ++- manuskript/util/counter.py | 9 + ui/outline.glade | 126 +++++++++----- 10 files changed, 428 insertions(+), 61 deletions(-) diff --git a/manuskript/data/__init__.py b/manuskript/data/__init__.py index b124673e..9cd784a2 100644 --- a/manuskript/data/__init__.py +++ b/manuskript/data/__init__.py @@ -3,12 +3,12 @@ from manuskript.data.characters import Characters, Character from manuskript.data.color import Color -from manuskript.data.goal import GoalKind, Goal +from manuskript.data.goal import Goal from manuskript.data.importance import Importance from manuskript.data.info import Info from manuskript.data.labels import LabelHost, Label from manuskript.data.links import LinkAction, Links -from manuskript.data.outline import Outline, OutlineFolder, OutlineText +from manuskript.data.outline import Outline, OutlineFolder, OutlineText, OutlineItem, OutlineState from manuskript.data.plots import Plots, PlotLine, PlotStep from manuskript.data.project import Project from manuskript.data.revisions import Revisions diff --git a/manuskript/data/goal.py b/manuskript/data/goal.py index 648892e1..e6f15174 100644 --- a/manuskript/data/goal.py +++ b/manuskript/data/goal.py @@ -1,18 +1,12 @@ #!/usr/bin/env python # --!-- coding: utf8 --!-- -from enum import Enum, unique - - -@unique -class GoalKind(Enum): - WORDS = 0 - CHARACTERS = 1 +from manuskript.util import CounterKind, countText class Goal: - def __init__(self, value: int = 0, kind: GoalKind = GoalKind.WORDS): + def __init__(self, value: int = 0, kind: CounterKind = CounterKind.WORDS): self.value = max(value, 0) self.kind = kind @@ -20,11 +14,14 @@ class Goal: return str(self.value) + " " + self.kind.name.lower() def __str__(self): - if self.kind != GoalKind.WORDS: + if self.kind != CounterKind.WORDS: return self.prettyString() else: return str(self.value) + def count(self, text: str): + return countText(text, self.kind) + @classmethod def parse(cls, string: str): if string is None: @@ -34,7 +31,7 @@ class Goal: try: value = int(parts[0]) - kind = GoalKind[parts[1].upper()] if len(parts) > 1 else GoalKind.WORDS + kind = CounterKind[parts[1].upper()] if len(parts) > 1 else CounterKind.WORDS except ValueError: return None diff --git a/manuskript/data/outline.py b/manuskript/data/outline.py index e3df7c57..6555f771 100644 --- a/manuskript/data/outline.py +++ b/manuskript/data/outline.py @@ -6,8 +6,10 @@ import os from collections import OrderedDict from enum import Enum, unique from manuskript.data.goal import Goal +from manuskript.data.plots import Plots from manuskript.data.unique_id import UniqueIDHost from manuskript.io.mmdFile import MmdFile +from manuskript.util import CounterKind, countText, validString @unique @@ -43,7 +45,9 @@ class OutlineItem: if ID is None: return - item.UID = item.outline.host.loadID(int(ID)) + if (item.UID is None) or (item.UID.value != int(ID)): + item.UID = item.outline.host.loadID(int(ID)) + item.title = metadata.get("title", None) item.type = metadata.get("type", "md") item.summarySentence = metadata.get("summarySentence", None) @@ -76,6 +80,12 @@ class OutlineItem: return metadata + def textCount(self, counterKind: CounterKind = None) -> int: + return 0 + + def goalCount(self) -> int: + return 0 if self.goal is None else self.goal.value + def load(self, optimized: bool = True): raise IOError('Loading undefined!') @@ -90,6 +100,12 @@ class OutlineText(OutlineItem): self.text = "" + def textCount(self, counterKind: CounterKind = None) -> int: + if counterKind is None: + counterKind = CounterKind.WORDS if self.goal is None else self.goal.kind + + return super().textCount(counterKind) + countText(self.text, counterKind) + def load(self, optimized: bool = True): metadata, body = self.file.loadMMD(optimized) OutlineItem.loadMetadata(self, metadata) @@ -126,6 +142,7 @@ class OutlineFolder(OutlineItem): names = os.listdir(folder.dir_path) names.remove("folder.txt") + names.sort() for name in names: path = os.path.join(folder.dir_path, name) @@ -147,6 +164,17 @@ class OutlineFolder(OutlineItem): if type(item) is OutlineFolder: cls.loadItems(outline, item, recursive) + def textCount(self, counterKind: CounterKind = None) -> int: + if counterKind is None: + counterKind = CounterKind.WORDS if self.goal is None else self.goal.kind + + count = super().textCount(counterKind) + + for item in self.items: + count += item.textCount(counterKind) + + return count + def load(self, _: bool = True): metadata, _ = self.file.loadMMD(True) OutlineItem.loadMetadata(self, metadata) @@ -170,14 +198,22 @@ class OutlineFolder(OutlineItem): class Outline: - def __init__(self, path): + def __init__(self, path, plots: Plots): self.dir_path = os.path.join(path, "outline") self.host = UniqueIDHost() + self.plots = plots self.items = list() def __iter__(self): return self.items.__iter__() + def getItemByID(self, ID: int) -> OutlineItem | None: + for item in self.all(): + if item.UID.value == ID: + return item + + return None + def all(self): result = list() queue = list(self.items) @@ -196,7 +232,10 @@ class Outline: def load(self): self.items.clear() - for name in os.listdir(self.dir_path): + names = os.listdir(self.dir_path) + names.sort() + + for name in names: path = os.path.join(self.dir_path, name) if os.path.isdir(path): diff --git a/manuskript/data/project.py b/manuskript/data/project.py index 39187319..3ae24595 100644 --- a/manuskript/data/project.py +++ b/manuskript/data/project.py @@ -32,7 +32,7 @@ class Project: self.characters = Characters(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.outline = Outline(self.file.dir_path, self.plots) self.revisions = Revisions(self.file.dir_path) def __del__(self): diff --git a/manuskript/ui/mainWindow.py b/manuskript/ui/mainWindow.py index b3cb1110..104f7ffa 100644 --- a/manuskript/ui/mainWindow.py +++ b/manuskript/ui/mainWindow.py @@ -68,7 +68,7 @@ class MainWindow: self.charactersView = MainWindow.packViewIntoSlot(builder, "characters_slot", CharactersView, self.project.characters) self.plotView = MainWindow.packViewIntoSlot(builder, "plot_slot", PlotView, self.project.plots) self.worldView = MainWindow.packViewIntoSlot(builder, "world_slot", WorldView, self.project.world) - self.outlineView = MainWindow.packViewIntoSlot(builder, "outline_slot", OutlineView) + self.outlineView = MainWindow.packViewIntoSlot(builder, "outline_slot", OutlineView, self.project.outline) self.editorView = MainWindow.packViewIntoSlot(builder, "editor_slot", EditorView) self.startupWindow = StartupWindow(self) diff --git a/manuskript/ui/views/outlineView.py b/manuskript/ui/views/outlineView.py index 0aebe8d0..9f21ce24 100644 --- a/manuskript/ui/views/outlineView.py +++ b/manuskript/ui/views/outlineView.py @@ -6,14 +6,269 @@ import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk +from manuskript.data import Outline, OutlineFolder, OutlineText, OutlineItem, OutlineState, Plots, PlotLine, Characters, Character, Importance, Goal +from manuskript.ui.util import rgbaFromColor, pixbufFromColor +from manuskript.util import validString, invalidString, validInt, invalidInt, CounterKind, countText + class OutlineView: - def __init__(self): + def __init__(self, outline: Outline): + self.outline = outline + self.outlineItem = None + builder = Gtk.Builder() builder.add_from_file("ui/outline.glade") self.widget = builder.get_object("outline_view") + self.plotsStore = builder.get_object("plots_store") + self.refreshPlotsStore() + + self.charactersStore = builder.get_object("characters_store") + self.refreshCharactersStore() + + self.outlineStore = builder.get_object("outline_store") + self.refreshOutlineStore() + + 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.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.filterOutlineBuffer = builder.get_object("filter_outline") + + self.filterOutlineBuffer.connect("deleted-text", self.filterOutlineDeletedText) + self.filterOutlineBuffer.connect("inserted-text", self.filterOutlineInsertedText) + + self.filteredOutlineStore = builder.get_object("filtered_outline_store") + + self.filteredOutlineStore.set_visible_func(self.filterOutline) + self.filteredOutlineStore.refilter() + + self.outlineSelection = builder.get_object("outline_selection") + + self.outlineSelection.connect("changed", self.outlineSelectionChanged) + + self.goalBuffer = builder.get_object("goal") + self.oneLineSummaryBuffer = builder.get_object("one_line_summary") + self.fewSentencesSummaryBuffer = builder.get_object("few_sentences_summary") + + self.goalBuffer.connect("deleted-text", self.goalDeletedText) + self.goalBuffer.connect("inserted-text", self.goalInsertedText) + + self.oneLineSummaryBuffer.connect("deleted-text", self.oneLineSummaryDeletedText) + self.oneLineSummaryBuffer.connect("inserted-text", self.oneLineSummaryInsertedText) + + self.fewSentencesSummaryBuffer.connect("changed", self.fewSentencesSummaryChanged) + + self.unloadOutlineData() + + def refreshPlotsStore(self): + self.plotsStore.clear() + + for plotLine in self.outline.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 refreshCharactersStore(self): + self.charactersStore.clear() + + for character in self.outline.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 __appendOutlineItem(self, outlineItem: OutlineItem, parent_iter=None): + tree_iter = self.outlineStore.append(parent_iter) + + if tree_iter is None: + return + + if outlineItem.state != OutlineState.COMPLETE: + outlineItem.load(False) + + if type(outlineItem) is OutlineFolder: + for item in outlineItem: + self.__appendOutlineItem(item, tree_iter) + + wordCount = validInt(outlineItem.textCount()) + goal = validInt(outlineItem.goalCount()) + progress = 0 + + if goal > wordCount: + progress = 100 * wordCount / goal + elif goal > 0: + progress = 100 + + self.outlineStore.set_value(tree_iter, 0, outlineItem.UID.value) + self.outlineStore.set_value(tree_iter, 1, validString(outlineItem.title)) + self.outlineStore.set_value(tree_iter, 2, validString(outlineItem.label)) + self.outlineStore.set_value(tree_iter, 3, validString(outlineItem.status)) + self.outlineStore.set_value(tree_iter, 4, outlineItem.compile) + self.outlineStore.set_value(tree_iter, 5, wordCount) + self.outlineStore.set_value(tree_iter, 6, goal) + self.outlineStore.set_value(tree_iter, 7, progress) + + def refreshOutlineStore(self): + self.outlineStore.clear() + + for item in self.outline.items: + self.__appendOutlineItem(item) + + def plotSelectionChanged(self, selection: Gtk.TreeSelection): + model, tree_iter = selection.get_selected() + + if tree_iter is None: + return + + for other in self.plotSelections: + if other != selection: + other.unselect_all() + + def loadOutlineData(self, outlineItem: OutlineItem): + self.outlineItem = None + + self.goalBuffer.set_text(validString(outlineItem.goal), -1) + self.oneLineSummaryBuffer.set_text(validString(outlineItem.summarySentence), -1) + self.fewSentencesSummaryBuffer.set_text(validString(outlineItem.summaryFull), -1) + + self.outlineItem = outlineItem + + def unloadOutlineData(self): + self.outlineItem = None + + self.goalBuffer.set_text("", -1) + self.oneLineSummaryBuffer.set_text("", -1) + self.fewSentencesSummaryBuffer.set_text("", -1) + + def outlineSelectionChanged(self, selection: Gtk.TreeSelection): + model, tree_iter = selection.get_selected() + + if tree_iter is None: + self.unloadOutlineData() + return + + outlineItem = self.outline.getItemByID(model[tree_iter][0]) + + if outlineItem is None: + self.unloadOutlineData() + else: + self.loadOutlineData(outlineItem) + + def __matchOutlineItemByText(self, outlineItem: OutlineItem, text: str): + if type(outlineItem) is OutlineFolder: + for item in outlineItem: + if self.__matchOutlineItemByText(item, text): + return True + + title = validString(outlineItem.title) + return text in title.lower() + + def filterOutline(self, model, iter, userdata): + outlineItem = self.outline.getItemByID(model[iter][0]) + + if outlineItem is None: + return False + + text = validString(self.filterOutlineBuffer.get_text()) + return self.__matchOutlineItemByText(outlineItem, text.lower()) + + def filterOutlineChanged(self, buffer: Gtk.EntryBuffer): + self.filteredOutlineStore.refilter() + + def filterOutlineDeletedText(self, buffer: Gtk.EntryBuffer, position: int, n_chars: int): + self.filterOutlineChanged(buffer) + + def filterOutlineInsertedText(self, buffer: Gtk.EntryBuffer, position: int, chars: str, n_chars: int): + self.filterOutlineChanged(buffer) + + def goalChanged(self, buffer: Gtk.EntryBuffer): + if self.outlineItem is None: + return + + text = buffer.get_text() + + self.outlineItem.goal = Goal.parse(text) + + outline_id = self.outlineItem.UID.value + + wordCount = validInt(self.outlineItem.textCount()) + goal = validInt(self.outlineItem.goalCount()) + progress = 0 + + if goal > wordCount: + progress = 100 * wordCount / goal + elif goal > 0: + progress = 100 + + for row in self.outlineStore: + if row[0] == outline_id: + row[6] = goal + row[7] = progress + break + + def goalDeletedText(self, buffer: Gtk.EntryBuffer, position: int, n_chars: int): + self.goalChanged(buffer) + + def goalInsertedText(self, buffer: Gtk.EntryBuffer, position: int, chars: str, n_chars: int): + self.goalChanged(buffer) + + def oneLineSummaryChanged(self, buffer: Gtk.EntryBuffer): + if self.outlineItem is None: + return + + text = buffer.get_text() + summary = invalidString(text) + + self.outlineItem.summarySentence = summary + + def oneLineSummaryDeletedText(self, buffer: Gtk.EntryBuffer, position: int, n_chars: int): + self.oneLineSummaryChanged(buffer) + + def oneLineSummaryInsertedText(self, buffer: Gtk.EntryBuffer, position: int, chars: str, n_chars: int): + self.oneLineSummaryChanged(buffer) + + def fewSentencesSummaryChanged(self, buffer: Gtk.TextBuffer): + if self.outlineItem 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.outlineItem.summaryFull = invalidString(text) + def show(self): self.widget.show_all() diff --git a/manuskript/ui/views/worldView.py b/manuskript/ui/views/worldView.py index 1e899b0f..64615ff5 100644 --- a/manuskript/ui/views/worldView.py +++ b/manuskript/ui/views/worldView.py @@ -59,7 +59,7 @@ class WorldView: self.unloadWorldData() - def __appendWorldItem(self, worldItem: WorldItem, parent_iter = None): + def __appendWorldItem(self, worldItem: WorldItem, parent_iter=None): tree_iter = self.worldStore.append(parent_iter) if tree_iter is None: diff --git a/manuskript/util/__init__.py b/manuskript/util/__init__.py index db2d5c13..6f3dcf16 100644 --- a/manuskript/util/__init__.py +++ b/manuskript/util/__init__.py @@ -2,7 +2,8 @@ # -*- coding: utf-8 -*- import re -from manuskript.util.counter import CharCounter, WordCounter, PageCounter + +from manuskript.util.counter import CounterKind, CharCounter, WordCounter, PageCounter def safeInt(s: str, d: int) -> int: @@ -15,16 +16,16 @@ def safeInt(s: str, d: int) -> int: return d -def validString(invalid: str) -> str: - return "" if invalid is None else invalid +def validString(invalid) -> str: + return "" if invalid is None else str(invalid) def invalidString(valid: str) -> str: return None if len(valid) == 0 else valid -def validInt(invalid: int) -> int: - return 0 if invalid is None else invalid +def validInt(invalid) -> int: + return 0 if invalid is None else int(invalid) def invalidInt(valid: int) -> int: @@ -46,3 +47,17 @@ def safeFilename(filename: str, extension: str = None) -> str: name = "_" + name return re.sub(r"[^a-zA-Z0-9._\-+()]", "_", name) + + +def countText(text: str, kind: CounterKind = CounterKind.WORDS): + if text is None: + return 0 + + if kind == CounterKind.CHARACTERS: + return CharCounter.count(text) + elif kind == CounterKind.WORDS: + return WordCounter.count(text) + elif kind == CounterKind.PAGES: + return PageCounter.count(text) + else: + return 0 diff --git a/manuskript/util/counter.py b/manuskript/util/counter.py index 1998fcc1..2d3fac60 100644 --- a/manuskript/util/counter.py +++ b/manuskript/util/counter.py @@ -3,6 +3,15 @@ import re +from enum import Enum, unique + + +@unique +class CounterKind(Enum): + WORDS = 0 + CHARACTERS = 1 + PAGES = 2 + class CharCounter: diff --git a/ui/outline.glade b/ui/outline.glade index b9bff451..d77e570c 100644 --- a/ui/outline.glade +++ b/ui/outline.glade @@ -1,5 +1,5 @@ - - + + + + + + + + + @@ -47,25 +55,31 @@ along with Manuskript. If not, see . + + - + + outline_store + + + + + + - - - - - + + plots_store - - - - - + + plots_store + + + plots_store True @@ -102,10 +116,21 @@ along with Manuskript. If not, see . True True - plot_main_store + main_plots_store + False 0 - + + + + + + + + 1 + + + @@ -130,10 +155,21 @@ along with Manuskript. If not, see . True True - plot_secondary_store + secondary_plots_store + False 0 - + + + + + + + + 1 + + + @@ -158,10 +194,21 @@ along with Manuskript. If not, see . True True - plot_minor_store + minor_plots_store + False 0 - + + + + + + + + 1 + + + @@ -206,11 +253,11 @@ along with Manuskript. If not, see . True True - outline_store + filtered_outline_store 0 - both + True - + @@ -218,7 +265,7 @@ along with Manuskript. If not, see . - 0 + 1 @@ -229,7 +276,7 @@ along with Manuskript. If not, see . - 1 + 2 @@ -240,7 +287,7 @@ along with Manuskript. If not, see . - 2 + 3 @@ -251,7 +298,7 @@ along with Manuskript. If not, see . - 3 + 4 @@ -260,9 +307,9 @@ along with Manuskript. If not, see . Word count - + - 4 + 5 @@ -271,9 +318,9 @@ along with Manuskript. If not, see . Goal - + - 5 + 6 @@ -284,7 +331,7 @@ along with Manuskript. If not, see . - 4 + 7 @@ -311,7 +358,7 @@ along with Manuskript. If not, see . False 4 - + True True True @@ -330,7 +377,7 @@ along with Manuskript. If not, see . - + True True True @@ -349,7 +396,7 @@ along with Manuskript. If not, see . - + True True True @@ -371,6 +418,7 @@ along with Manuskript. If not, see . True True + filter_outline Filter @@ -407,6 +455,8 @@ along with Manuskript. If not, see . True True + goal + 0 digits @@ -433,17 +483,17 @@ along with Manuskript. If not, see . True False - character_store + characters_store - 0 + 1 - 1 + 2 @@ -478,6 +528,7 @@ along with Manuskript. If not, see . True True + one_line_summary One line summary @@ -512,6 +563,7 @@ along with Manuskript. If not, see . True True word-char + few_sentences_summary