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 @@
+
+
@@ -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
###############################################################################