From b05377b417517aa00662db6cb0b651898abfb7f4 Mon Sep 17 00:00:00 2001 From: Arne Sostack Date: Sat, 13 May 2023 23:34:22 +0200 Subject: [PATCH] Implemented history back and forward navigation --- manuskript/functions/history/History.py | 56 +++++ .../functions/history/NavigatedEvent.py | 5 + manuskript/functions/history/Signal.py | 23 ++ manuskript/mainWindow.py | 200 +++++++++++++++++- manuskript/ui/mainWindow.py | 18 ++ manuskript/ui/mainWindow.ui | 32 +++ manuskript/ui/views/plotTreeView.py | 10 +- 7 files changed, 339 insertions(+), 5 deletions(-) create mode 100644 manuskript/functions/history/History.py create mode 100644 manuskript/functions/history/NavigatedEvent.py create mode 100644 manuskript/functions/history/Signal.py diff --git a/manuskript/functions/history/History.py b/manuskript/functions/history/History.py new file mode 100644 index 0000000..a17aa42 --- /dev/null +++ b/manuskript/functions/history/History.py @@ -0,0 +1,56 @@ +from manuskript.functions.history.NavigatedEvent import NavigatedEvent +from manuskript.functions.history.Signal import Signal + + +class History(): + def __init__(self) -> None: + self._entries = [] + self._position = 0 + self.navigated = Signal() + self._navigating = False + + def next(self, entry): + if self._navigating: + return + + while self._position < len(self._entries) - 1: + self._entries.pop() + + self._entries.append(entry) + self._position = len(self._entries) - 1 + self._navigating = True + self.navigated.fire(NavigatedEvent(self._position, len(self._entries), entry)) + self._navigating = False + + + def replace(self, entry): + if self._navigating: + return + + while self._position < len(self._entries): + self._entries.pop() + + self._entries.append(entry) + self._position = len(self._entries) - 1 + self._navigating = True + self.navigated.fire(NavigatedEvent(self._position, len(self._entries), entry)) + self._navigating = False + + def forward(self): + if self._position < len(self._entries) - 1: + self._position += 1 + self._navigating = True + self.navigated.fire(NavigatedEvent(self._position, len(self._entries), self._entries[self._position])) + self._navigating = False + + def back(self): + if self._position > 0: + self._position -= 1 + self._navigating = True + self.navigated.fire(NavigatedEvent(self._position, len(self._entries), self._entries[self._position])) + self._navigating = False + + def reset(self): + self._entries.clear() + self._position = 0 + self.navigated.fire(NavigatedEvent(self._position, len(self._entries), None)) diff --git a/manuskript/functions/history/NavigatedEvent.py b/manuskript/functions/history/NavigatedEvent.py new file mode 100644 index 0000000..03b6ae0 --- /dev/null +++ b/manuskript/functions/history/NavigatedEvent.py @@ -0,0 +1,5 @@ +class NavigatedEvent(): + def __init__(self, position, count, entry) -> None: + self.position = position + self.count = count + self.entry = entry \ No newline at end of file diff --git a/manuskript/functions/history/Signal.py b/manuskript/functions/history/Signal.py new file mode 100644 index 0000000..6f39c7b --- /dev/null +++ b/manuskript/functions/history/Signal.py @@ -0,0 +1,23 @@ + +class Signal(): + def __init__(self) -> None: + self._methods = [] + + def connect(self, func): + self._methods.append(func) + + def disconnect(self, func): + try: + self._methods.remove(func) + except ValueError: + raise TypeError + + def disconnect(self): + if len(self._methods) == 0: + raise TypeError + self._methods.pop() + + def fire(self, data): + for m in self._methods: + m(data) + diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index 3e184cf..1504401 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -16,6 +16,7 @@ from manuskript.enums import Character, PlotStep, Plot, World, Outline from manuskript.functions import wordCount, appPath, findWidgetsOfClass, openURL, showInFolder import manuskript.functions as F from manuskript import loadSave +from manuskript.functions.history.History import History from manuskript.logging import getLogFilePath from manuskript.models.characterModel import characterModel from manuskript.models import outlineModel @@ -73,6 +74,8 @@ class MainWindow(QMainWindow, Ui_MainWindow): # value. In manuskript.main. self._autoLoadProject = None # Used to load a command line project self.sessionStartWordCount = 0 # Used to track session targets + self.history = History() + self._previousSelectionEmpty = True self.readSettings() @@ -104,7 +107,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): # Main Menu for i in [self.actSave, self.actSaveAs, self.actCloseProject, self.menuEdit, self.menuView, self.menuOrganize, - self.menuTools, self.menuHelp, self.actImport, + self.menuNavigate, self.menuTools, self.menuHelp, self.actImport, self.actCompile, self.actSettings]: i.setEnabled(False) @@ -158,6 +161,10 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.actSplitCursor.triggered.connect(self.documentsSplitCursor) self.actMerge.triggered.connect(self.documentsMerge) + # Main menu:: Navigate + self.actBack.triggered.connect(self.navigateBack) + self.actForward.triggered.connect(self.navigateForward) + # Main Menu:: view self.generateViewMenu() self.actModeGroup = QActionGroup(self) @@ -256,6 +263,58 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.actDelete, self.actRename]: i.setEnabled(tabIsEditor) + match self.tabMain.currentIndex(): + case self.TabPersos: + selectedCharacters = self.lstCharacters.currentCharacters() + characterSelectionIsEmpty = not any(selectedCharacters) + + if characterSelectionIsEmpty: + self.pushHistory(("character", None)) + self._previousSelectionEmpty = True + else: + character = selectedCharacters[0] + self.pushHistory(("character", character.ID())) + self._previousSelectionEmpty = False + + case self.TabPlots: + id = self.lstPlots.currentPlotID() + self.pushHistory(("plot", id)) + self._previousSelectionEmpty = id is None + + case self.TabWorld: + index = self.mdlWorld.selectedIndex() + + if index.isValid(): + id = self.mdlWorld.ID(index) + self.pushHistory(("world", id)) + self._previousSelectionEmpty = id is not None + else: + self.pushHistory(("world", None)) + self._previousSelectionEmpty = True + + case self.TabOutline: + index = self.treeOutlineOutline.selectionModel().currentIndex() + if index.isValid(): + id = self.mdlOutline.ID(index) + self.pushHistory(("outline", id)) + self._previousSelectionEmpty = id is not None + else: + self.pushHistory(("outline", None)) + self._previousSelectionEmpty = False + + case self.TabRedac: + index = self.treeRedacOutline.selectionModel().currentIndex() + if index.isValid(): + id = self.mdlOutline.ID(index) + self.pushHistory(("redac", id)) + self._previousSelectionEmpty = id is not None + else: + self.pushHistory(("redac", None)) + self._previousSelectionEmpty = False + + case _: + self.pushHistory(("main", self.tabMain.currentIndex())) + self._previousSelectionEmpty = False def focusChanged(self, old, new): """ @@ -295,6 +354,17 @@ class MainWindow(QMainWindow, Ui_MainWindow): # OUTLINE ############################################################################### + def outlineChanged(self, selected, deselected): + index = self.treeOutlineOutline.selectionModel().currentIndex() + if not index.isValid(): + self.pushHistory(("outline", None)) + self._previousSelectionEmpty = True + return + + self.pushHistory(("outline", self.mdlOutline.ID(index))) + self._previousSelectionEmpty = False + + def outlineRemoveItemsRedac(self): self.treeRedacOutline.delete() @@ -428,16 +498,23 @@ class MainWindow(QMainWindow, Ui_MainWindow): widget = tabData['widget'] title = tabData['title'] self.tabPersos.addTab(widget, title) + def handleCharacterSelectionChanged(self): selectedCharacters = self.lstCharacters.currentCharacters() characterSelectionIsEmpty = not any(selectedCharacters) if characterSelectionIsEmpty: + self.pushHistory(("character", None)) self.tabPersos.setEnabled(False) + self._previousSelectionEmpty = True return + cList = list(filter(None, self.lstCharacters.currentCharacters())) #cList contains all valid characters character = cList[0] self.changeCurrentCharacter(character) + self.pushHistory(("character", character.ID())) + self._previousSelectionEmpty = False + if len(selectedCharacters) > 1: self.setPersoBulkMode(True) else: @@ -464,7 +541,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): for character in self.lstCharacters.currentCharacters(): self.bulkAffectedCharacters.append(character.name()) - def changeCurrentCharacter(self, character, trash=None): + def changeCurrentCharacter(self, character): if character is None: return @@ -545,11 +622,17 @@ class MainWindow(QMainWindow, Ui_MainWindow): def changeCurrentPlot(self): index = self.lstPlots.currentPlotIndex() + id = self.lstPlots.currentPlotID() if not index.isValid(): self.tabPlot.setEnabled(False) + self.pushHistory(("plot", None)) + self._previousSelectionEmpty = True return + self.pushHistory(("plot", id)) + self._previousSelectionEmpty = False + self.tabPlot.setEnabled(True) self.txtPlotName.setCurrentModelIndex(index) self.txtPlotDescription.setCurrentModelIndex(index) @@ -618,8 +701,13 @@ class MainWindow(QMainWindow, Ui_MainWindow): if not index.isValid(): self.tabWorld.setEnabled(False) + self.pushHistory(("world", None)) + self._previousSelectionEmpty = True return + self.pushHistory(("world", self.mdlWorld.ID(index))) + self._previousSelectionEmpty = False + self.tabWorld.setEnabled(True) self.txtWorldName.setCurrentModelIndex(index) self.txtWorldDescription.setCurrentModelIndex(index) @@ -630,6 +718,16 @@ class MainWindow(QMainWindow, Ui_MainWindow): # EDITOR ############################################################################### + def redacOutlineChanged(self): + index = self.treeRedacOutline.selectionModel().currentIndex() + if not index.isValid(): + self.pushHistory(("redac", None)) + self._previousSelectionEmpty = True + return + + self.pushHistory(("redac", self.mdlOutline.ID(index))) + self._previousSelectionEmpty = False + def openIndex(self, index): self.treeRedacOutline.setCurrentIndex(index) @@ -727,6 +825,94 @@ class MainWindow(QMainWindow, Ui_MainWindow): "Merges selected item(s)." if self._lastFocus: self._lastFocus.merge() + # Navigate + + def navigateBack(self): + self.history.back() + + def navigateForward(self): + self.history.forward() + + def pushHistory(self, entry): + if self._previousSelectionEmpty: + self.history.replace(entry) + else: + self.history.next(entry) + + def navigated(self, event): + if event.entry: + match event.entry[0]: + case "character": + if self.tabMain.currentIndex() != self.TabPersos: + self.tabMain.setCurrentIndex(self.TabPersos) + + if event.entry[1] is None: + self.lstCharacters.setCurrentItem(None) + self.lstCharacters.clearSelection() + else: + if self.lstCharacters.currentCharacterID() != event.entry[1]: + char = self.lstCharacters.getItemByID(event.entry[1]) + if char != None: + self.lstCharacters.clearSelection() + self.lstCharacters.setCurrentItem(char) + case "plot": + if self.tabMain.currentIndex() != self.TabPlots: + self.tabMain.setCurrentIndex(self.TabPlots) + + if event.entry[1] is None: + self.lstPlots.setCurrentItem(None) + else: + index = self.lstPlots.currentPlotIndex() + if index and index.row() != event.entry[1]: + plot = self.lstPlots.getItemByID(event.entry[1]) + if plot != None: + self.lstPlots.setCurrentItem(plot) + case "world": + if self.tabMain.currentIndex() != self.TabWorld: + self.tabMain.setCurrentIndex(self.TabWorld) + + if event.entry[1] is None: + self.treeWorld.selectionModel().clear() + else: + index = self.mdlWorld.selectedIndex() + if index and self.mdlWorld.ID(index) != event.entry[1]: + world = self.mdlWorld.indexByID(event.entry[1]) + if world != None: + self.treeWorld.setCurrentIndex(world) + + case "outline": + if self.tabMain.currentIndex() != self.TabOutline: + self.tabMain.setCurrentIndex(self.TabOutline) + + if event.entry[1] is None: + self.treeOutlineOutline.selectionModel().clear() + else: + index = self.treeOutlineOutline.selectionModel().currentIndex() + if index and self.mdlOutline.ID(index) != event.entry[1]: + outline = self.mdlOutline.getIndexByID(event.entry[1]) + if outline is not None: + self.treeOutlineOutline.setCurrentIndex(outline) + + case "redac": + if self.tabMain.currentIndex() != self.TabRedac: + self.tabMain.setCurrentIndex(self.TabRedac) + + if event.entry[1] is None: + self.treeRedacOutline.selectionModel().clear() + else: + index = self.treeRedacOutline.selectionModel().currentIndex() + if index and self.mdlOutline.ID(index) != event.entry[1]: + outline = self.mdlOutline.getIndexByID(event.entry[1]) + if outline is not None: + self.treeRedacOutline.setCurrentIndex(outline) + + case "main": + if self.tabMain.currentIndex() != event.entry[1]: + self.lstTabs.setCurrentRow(event.entry[1]) + + self.actBack.setEnabled(event.position > 0) + self.actForward.setEnabled(event.position < event.count - 1) + ############################################################################### # LOAD AND SAVE ############################################################################### @@ -803,6 +989,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): i.setEnabled(False) for i in [self.actSave, self.actSaveAs, self.actCloseProject, self.menuEdit, self.menuView, self.menuOrganize, + self.menuNavigate, self.menuTools, self.menuHelp, self.actImport, self.actCompile, self.actSettings]: i.setEnabled(True) @@ -820,6 +1007,9 @@ class MainWindow(QMainWindow, Ui_MainWindow): # Add project name to Window's name self.setWindowTitle(self.projectName() + " - " + self.tr("Manuskript")) + # Reset history + self.history.reset() + # Show main Window self.switchToProject() @@ -1080,6 +1270,8 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.tabMain.currentChanged.connect(self.toolbar.setCurrentGroup) self.tabMain.currentChanged.connect(self.tabMainChanged) + self.history.navigated.connect(self.navigated) + qApp.focusChanged.connect(self.focusChanged) def makeConnections(self): @@ -1218,10 +1410,12 @@ class MainWindow(QMainWindow, Ui_MainWindow): # self.redacEditor.setModel(self.mdlOutline) self.storylineView.setModels(self.mdlOutline, self.mdlCharacter, self.mdlPlots) + self.treeOutlineOutline.selectionModel().selectionChanged.connect(self.outlineChanged, F.AUC) self.treeOutlineOutline.selectionModel().selectionChanged.connect(self.outlineItemEditor.selectionChanged, F.AUC) self.treeOutlineOutline.clicked.connect(self.outlineItemEditor.selectionChanged, F.AUC) # Sync selection + self.treeRedacOutline.selectionModel().selectionChanged.connect(self.redacOutlineChanged, F.AUC) self.treeRedacOutline.selectionModel().selectionChanged.connect(self.redacMetadata.selectionChanged, F.AUC) self.treeRedacOutline.clicked.connect(self.redacMetadata.selectionChanged, F.AUC) @@ -1329,6 +1523,8 @@ class MainWindow(QMainWindow, Ui_MainWindow): lambda: self.tblDebugSubPlots.setRootIndex(self.mdlPlots.index( self.tblDebugPlots.selectionModel().currentIndex().row(), Plot.steps))) + + self.disconnectAll(self.history.navigated) ############################################################################### # HELP diff --git a/manuskript/ui/mainWindow.py b/manuskript/ui/mainWindow.py index 27161d1..53c52a0 100644 --- a/manuskript/ui/mainWindow.py +++ b/manuskript/ui/mainWindow.py @@ -1016,6 +1016,8 @@ class Ui_MainWindow(object): self.menuMode.setObjectName("menuMode") self.menuOrganize = QtWidgets.QMenu(self.menubar) self.menuOrganize.setObjectName("menuOrganize") + self.menuNavigate = QtWidgets.QMenu(self.menubar) + self.menuNavigate.setObjectName("menuNavigate") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") @@ -1200,6 +1202,16 @@ class Ui_MainWindow(object): self.actMoveDown.setIcon(icon) self.actMoveDown.setShortcut("Ctrl+Shift+Down") self.actMoveDown.setObjectName("actMoveDown") + self.actBack = QtWidgets.QAction(MainWindow) + icon = QtGui.QIcon.fromTheme("arrow-left") + self.actBack.setIcon(icon) + self.actBack.setShortcut("Ctrl+<") + self.actBack.setObjectName("actBack") + self.actForward = QtWidgets.QAction(MainWindow) + icon = QtGui.QIcon.fromTheme("arrow-right") + self.actForward.setIcon(icon) + self.actForward.setShortcut("Ctrl+>") + self.actForward.setObjectName("actForward") self.actRename = QtWidgets.QAction(MainWindow) icon = QtGui.QIcon.fromTheme("edit-rename") self.actRename.setIcon(icon) @@ -1355,9 +1367,12 @@ class Ui_MainWindow(object): self.menuOrganize.addAction(self.actMerge) self.menuOrganize.addAction(self.actSplitDialog) self.menuOrganize.addAction(self.actSplitCursor) + self.menuNavigate.addAction(self.actBack) + self.menuNavigate.addAction(self.actForward) self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuEdit.menuAction()) self.menubar.addAction(self.menuOrganize.menuAction()) + self.menubar.addAction(self.menuNavigate.menuAction()) self.menubar.addAction(self.menuView.menuAction()) self.menubar.addAction(self.menuTools.menuAction()) self.menubar.addAction(self.menuHelp.menuAction()) @@ -1559,6 +1574,7 @@ class Ui_MainWindow(object): self.menuView.setTitle(_translate("MainWindow", "&View")) self.menuMode.setTitle(_translate("MainWindow", "&Mode")) self.menuOrganize.setTitle(_translate("MainWindow", "Organi&ze")) + self.menuNavigate.setTitle(_translate("MainWindow", "&Navigate")) self.dckCheatSheet.setWindowTitle(_translate("MainWindow", "Cheat Sheet")) self.dckSearch.setWindowTitle(_translate("MainWindow", "Search")) self.dckNavigation.setWindowTitle(_translate("MainWindow", "&Navigation")) @@ -1592,6 +1608,8 @@ class Ui_MainWindow(object): self.actDelete.setText(_translate("MainWindow", "&Delete")) self.actMoveUp.setText(_translate("MainWindow", "&Move Up")) self.actMoveDown.setText(_translate("MainWindow", "M&ove Down")) + self.actBack.setText(_translate("MainWindow", "Go &back")) + self.actForward.setText(_translate("MainWindow", "Go &forward")) self.actRename.setText(_translate("MainWindow", "&Rename")) self.actHeaderSetextL1.setText(_translate("MainWindow", "&Level 1 (setext)")) self.actHeaderSetextL2.setText(_translate("MainWindow", "Level &2")) diff --git a/manuskript/ui/mainWindow.ui b/manuskript/ui/mainWindow.ui index 034bc79..5f85326 100644 --- a/manuskript/ui/mainWindow.ui +++ b/manuskript/ui/mainWindow.ui @@ -2124,9 +2124,17 @@ + + + &Navigate + + + + + @@ -2568,6 +2576,30 @@ Ctrl+Shift+Down + + + + .. + + + Go &back + + + Ctrl+< + + + + + + .. + + + Go &forward + + + Ctrl+> + + diff --git a/manuskript/ui/views/plotTreeView.py b/manuskript/ui/views/plotTreeView.py index 97c46fc..564f747 100644 --- a/manuskript/ui/views/plotTreeView.py +++ b/manuskript/ui/views/plotTreeView.py @@ -65,13 +65,17 @@ class plotTreeView(QTreeWidget): return find(self.invisibleRootItem(), ID) def currentPlotIndex(self): - "Returns index of the current item in plot model." + "Returns index of the current item in plot model." + return self._model.getIndexFromID(self.currentPlotID()) + + def currentPlotID(self): + "Returns ID of the current item in plot model." ID = None if self.currentItem(): ID = self.currentItem().data(0, Qt.UserRole) - return self._model.getIndexFromID(ID) - + return ID + ############################################################################### # UPDATES ###############################################################################