manuskript/manuskript/mainWindow.py
Curtis Gedak 589eb42235 Fixes: after project close, open or create project fails
See issue #96.

This change creates a new class disconnectAll() to ensure that all
connections are disconnected for a specific signal and slot.  Now when
a project is closed, the UI signals are disconnected.  As before,
these signals and slots are connected when a project is opened.

Because Manuskript permits working on only one project at a time, the
menu items for File Open and Recents are disabled after a project is
loaded.  These menu items are enabled when a project is closed.

Prior to this commit, subsequent open project selections would
complain with the following message:

  TypeError: connection is not unique
2017-05-21 12:04:27 -06:00

1159 lines
46 KiB
Python

#!/usr/bin/env python
# --!-- coding: utf8 --!--
import imp
import os
from PyQt5.QtCore import pyqtSignal, QSignalMapper, QTimer, QSettings, Qt, QRegExp, QUrl, QSize
from PyQt5.QtGui import QStandardItemModel, QIcon, QColor
from PyQt5.QtWidgets import QMainWindow, QHeaderView, qApp, QMenu, QActionGroup, QAction, QStyle, QListWidgetItem, \
QLabel
from manuskript import settings
from manuskript.enums import Character, PlotStep, Plot, World, Outline
from manuskript.functions import AUC, wordCount, appPath, findWidgetsOfClass
from manuskript import loadSave
from manuskript.models.characterModel import characterModel
from manuskript.models.outlineModel import outlineModel
from manuskript.models.plotModel import plotModel
from manuskript.models.worldModel import worldModel
from manuskript.settingsWindow import settingsWindow
from manuskript.ui import style
from manuskript.ui.collapsibleDockWidgets import collapsibleDockWidgets
from manuskript.ui.exporters.exporter import exporterDialog
from manuskript.ui.helpLabel import helpLabel
from manuskript.ui.mainWindow import Ui_MainWindow
from manuskript.ui.tools.frequencyAnalyzer import frequencyAnalyzer
from manuskript.ui.views.outlineDelegates import outlineCharacterDelegate
from manuskript.ui.views.plotDelegate import plotDelegate
# Spellcheck support
from manuskript.ui.views.textEditView import textEditView
try:
import enchant
except ImportError:
enchant = None
class MainWindow(QMainWindow, Ui_MainWindow):
dictChanged = pyqtSignal(str)
# Tab indexes
TabInfos = 0
TabSummary = 1
TabPersos = 2
TabPlots = 3
TabWorld = 4
TabOutline = 5
TabRedac = 6
def __init__(self):
QMainWindow.__init__(self)
self.setupUi(self)
self.currentProject = None
self.readSettings()
# UI
self.setupMoreUi()
# Welcome
self.welcome.updateValues()
self.stack.setCurrentIndex(0)
# Word count
self.mprWordCount = QSignalMapper(self)
for t, i in [
(self.txtSummarySentence, 0),
(self.txtSummaryPara, 1),
(self.txtSummaryPage, 2),
(self.txtSummaryFull, 3)
]:
t.textChanged.connect(self.mprWordCount.map)
self.mprWordCount.setMapping(t, i)
self.mprWordCount.mapped.connect(self.wordCount)
# Snowflake Method Cycle
self.mapperCycle = QSignalMapper(self)
for t, i in [
(self.btnStepTwo, 0),
(self.btnStepThree, 1),
(self.btnStepFour, 2),
(self.btnStepFive, 3),
(self.btnStepSix, 4),
(self.btnStepSeven, 5),
(self.btnStepEight, 6)
]:
t.clicked.connect(self.mapperCycle.map)
self.mapperCycle.setMapping(t, i)
self.mapperCycle.mapped.connect(self.clickCycle)
self.cmbSummary.currentIndexChanged.connect(self.summaryPageChanged)
self.cmbSummary.setCurrentIndex(0)
self.cmbSummary.currentIndexChanged.emit(0)
# Main Menu
for i in [self.actSave, self.actSaveAs, self.actCloseProject,
self.menuEdit, self.menuView, self.menuTools, self.menuHelp]:
i.setEnabled(False)
self.actOpen.triggered.connect(self.welcome.openFile)
self.actSave.triggered.connect(self.saveDatas)
self.actSaveAs.triggered.connect(self.welcome.saveAsFile)
self.actCompile.triggered.connect(self.doCompile)
self.actLabels.triggered.connect(self.settingsLabel)
self.actStatus.triggered.connect(self.settingsStatus)
self.actSettings.triggered.connect(self.settingsWindow)
self.actCloseProject.triggered.connect(self.closeProject)
self.actQuit.triggered.connect(self.close)
self.actToolFrequency.triggered.connect(self.frequencyAnalyzer)
self.generateViewMenu()
self.actModeGroup = QActionGroup(self)
self.actModeSimple.setActionGroup(self.actModeGroup)
self.actModeFiction.setActionGroup(self.actModeGroup)
self.actModeSnowflake.setActionGroup(self.actModeGroup)
self.actModeSimple.triggered.connect(self.setViewModeSimple)
self.actModeFiction.triggered.connect(self.setViewModeFiction)
self.actModeSnowflake.setEnabled(False)
self.makeUIConnections()
# self.loadProject(os.path.join(appPath(), "test_project.zip"))
###############################################################################
# SUMMARY
###############################################################################
def summaryPageChanged(self, index):
fractalButtons = [
self.btnStepTwo,
self.btnStepThree,
self.btnStepFive,
self.btnStepSeven,
]
for b in fractalButtons:
b.setVisible(fractalButtons.index(b) == index)
###############################################################################
# OUTLINE
###############################################################################
def outlineRemoveItemsRedac(self):
self.treeRedacOutline.delete()
def outlineRemoveItemsOutline(self):
self.treeOutlineOutline.delete()
###############################################################################
# CHARACTERS
###############################################################################
def changeCurrentCharacter(self, trash=None):
"""
@return:
"""
c = self.lstCharacters.currentCharacter()
if not c:
self.tabPlot.setEnabled(False)
return
self.tabPersos.setEnabled(True)
index = c.index()
for w in [
self.txtPersoName,
self.sldPersoImportance,
self.txtPersoMotivation,
self.txtPersoGoal,
self.txtPersoConflict,
self.txtPersoEpiphany,
self.txtPersoSummarySentence,
self.txtPersoSummaryPara,
self.txtPersoSummaryFull,
self.txtPersoNotes,
]:
w.setCurrentModelIndex(index)
# Button color
self.updateCharacterColor(c.ID())
# Character Infos
self.tblPersoInfos.setRootIndex(index)
if self.mdlCharacter.rowCount(index):
self.updatePersoInfoView()
def updatePersoInfoView(self):
self.tblPersoInfos.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
self.tblPersoInfos.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
self.tblPersoInfos.verticalHeader().hide()
def updateCharacterColor(self, ID):
c = self.mdlCharacter.getCharacterByID(ID)
color = c.color().name()
self.btnPersoColor.setStyleSheet("background:{};".format(color))
###############################################################################
# PLOTS
###############################################################################
def changeCurrentPlot(self):
index = self.lstPlots.currentPlotIndex()
if not index.isValid():
self.tabPlot.setEnabled(False)
return
self.tabPlot.setEnabled(True)
self.txtPlotName.setCurrentModelIndex(index)
self.txtPlotDescription.setCurrentModelIndex(index)
self.txtPlotResult.setCurrentModelIndex(index)
self.sldPlotImportance.setCurrentModelIndex(index)
self.lstPlotPerso.setRootIndex(index.sibling(index.row(),
Plot.characters.value))
subplotindex = index.sibling(index.row(), Plot.steps.value)
self.lstSubPlots.setRootIndex(subplotindex)
if self.mdlPlots.rowCount(subplotindex):
self.updateSubPlotView()
# self.txtSubPlotSummary.setCurrentModelIndex(QModelIndex())
self.txtSubPlotSummary.setEnabled(False)
self._updatingSubPlot = True
self.txtSubPlotSummary.setPlainText("")
self._updatingSubPlot = False
self.lstPlotPerso.selectionModel().clear()
def updateSubPlotView(self):
# Hide columns
for i in range(self.mdlPlots.columnCount()):
self.lstSubPlots.hideColumn(i)
self.lstSubPlots.showColumn(PlotStep.name.value)
self.lstSubPlots.showColumn(PlotStep.meta.value)
self.lstSubPlots.horizontalHeader().setSectionResizeMode(
PlotStep.name.value, QHeaderView.Stretch)
self.lstSubPlots.horizontalHeader().setSectionResizeMode(
PlotStep.meta.value, QHeaderView.ResizeToContents)
self.lstSubPlots.verticalHeader().hide()
def changeCurrentSubPlot(self, index):
# Got segfaults when using textEditView model system, so ad hoc stuff.
index = index.sibling(index.row(), PlotStep.summary.value)
item = self.mdlPlots.itemFromIndex(index)
if not item:
self.txtSubPlotSummary.setEnabled(False)
return
self.txtSubPlotSummary.setEnabled(True)
txt = item.text()
self._updatingSubPlot = True
self.txtSubPlotSummary.setPlainText(txt)
self._updatingSubPlot = False
def updateSubPlotSummary(self):
if self._updatingSubPlot:
return
index = self.lstSubPlots.currentIndex()
if not index.isValid():
return
index = index.sibling(index.row(), PlotStep.summary.value)
item = self.mdlPlots.itemFromIndex(index)
self._updatingSubPlot = True
item.setText(self.txtSubPlotSummary.toPlainText())
self._updatingSubPlot = False
def plotPersoSelectionChanged(self):
"Enables or disables remove plot perso button."
self.btnRmPlotPerso.setEnabled(
len(self.lstPlotPerso.selectedIndexes()) != 0)
###############################################################################
# WORLD
###############################################################################
def changeCurrentWorld(self):
index = self.mdlWorld.selectedIndex()
if not index.isValid():
self.tabWorld.setEnabled(False)
return
self.tabWorld.setEnabled(True)
self.txtWorldName.setCurrentModelIndex(index)
self.txtWorldDescription.setCurrentModelIndex(index)
self.txtWorldPassion.setCurrentModelIndex(index)
self.txtWorldConflict.setCurrentModelIndex(index)
# ###############################################################################
# # LOAD AND SAVE
# ###############################################################################
def loadProject(self, project, loadFromFile=True):
"""Loads the project ``project``.
If ``loadFromFile`` is False, then it does not load datas from file.
It assumes that the datas have been populated in a different way."""
if loadFromFile and not os.path.exists(project):
print(self.tr("The file {} does not exist. Try again.").format(project))
self.statusBar().showMessage(
self.tr("The file {} does not exist. Try again.").format(project),
5000)
return
if loadFromFile:
# Load empty settings
imp.reload(settings)
# Load data
self.loadEmptyDatas()
self.loadDatas(project)
self.makeConnections()
# Load settings
if settings.openIndexes:
self.mainEditor.tabSplitter.restoreOpenIndexes(settings.openIndexes)
self.generateViewMenu()
self.mainEditor.sldCorkSizeFactor.setValue(settings.corkSizeFactor)
self.actSpellcheck.setChecked(settings.spellcheck)
self.toggleSpellcheck(settings.spellcheck)
self.updateMenuDict()
self.setDictionary()
self.mainEditor.setFolderView(settings.folderView)
self.mainEditor.updateFolderViewButtons(settings.folderView)
self.mainEditor.tabSplitter.updateStyleSheet()
self.tabMain.setCurrentIndex(settings.lastTab)
# We force to emit even if it opens on the current tab
self.tabMain.currentChanged.emit(settings.lastTab)
self.mainEditor.updateCorkBackground()
if settings.viewMode == "simple":
self.setViewModeSimple()
else:
self.setViewModeFiction()
# Set autosave
self.saveTimer = QTimer()
self.saveTimer.setInterval(settings.autoSaveDelay * 60 * 1000)
self.saveTimer.setSingleShot(False)
self.saveTimer.timeout.connect(self.saveDatas)
if settings.autoSave:
self.saveTimer.start()
# Set autosave if no changes
self.saveTimerNoChanges = QTimer()
self.saveTimerNoChanges.setInterval(settings.autoSaveNoChangesDelay * 1000)
self.saveTimerNoChanges.setSingleShot(True)
self.mdlFlatData.dataChanged.connect(self.startTimerNoChanges)
self.mdlOutline.dataChanged.connect(self.startTimerNoChanges)
self.mdlCharacter.dataChanged.connect(self.startTimerNoChanges)
self.mdlPlots.dataChanged.connect(self.startTimerNoChanges)
self.mdlWorld.dataChanged.connect(self.startTimerNoChanges)
# self.mdlPersosInfos.dataChanged.connect(self.startTimerNoChanges)
self.mdlStatus.dataChanged.connect(self.startTimerNoChanges)
self.mdlLabels.dataChanged.connect(self.startTimerNoChanges)
self.saveTimerNoChanges.timeout.connect(self.saveDatas)
self.saveTimerNoChanges.stop()
# UI
for i in [self.actOpen, self.menuRecents]:
i.setEnabled(False)
for i in [self.actSave, self.actSaveAs, self.actCloseProject,
self.menuEdit, self.menuView, self.menuTools, self.menuHelp]:
i.setEnabled(True)
# FIXME: set Window's name: project name
# Stuff
# self.checkPersosID() # Should'n be necessary any longer
self.currentProject = project
QSettings().setValue("lastProject", project)
# Show main Window
self.stack.setCurrentIndex(1)
def closeProject(self):
if not self.currentProject:
return
# Close open tabs in editor
self.mainEditor.closeAllTabs()
# Save datas
self.saveDatas()
self.currentProject = None
QSettings().setValue("lastProject", "")
# Clear datas
self.loadEmptyDatas()
self.saveTimer.stop()
loadSave.clearSaveCache()
self.breakConnections()
# UI
for i in [self.actOpen, self.menuRecents]:
i.setEnabled(True)
for i in [self.actSave, self.actSaveAs, self.actCloseProject,
self.menuEdit, self.menuView, self.menuTools, self.menuHelp]:
i.setEnabled(False)
# Reload recent files
self.welcome.updateValues()
# Show welcome dialog
self.stack.setCurrentIndex(0)
def readSettings(self):
# Load State and geometry
sttgns = QSettings(qApp.organizationName(), qApp.applicationName())
if sttgns.contains("geometry"):
self.restoreGeometry(sttgns.value("geometry"))
if sttgns.contains("windowState"):
self.restoreState(sttgns.value("windowState"))
else:
self.dckCheatSheet.hide()
self.dckSearch.hide()
if sttgns.contains("metadataState"):
state = [False if v == "false" else True for v in sttgns.value("metadataState")]
self.redacMetadata.restoreState(state)
if sttgns.contains("revisionsState"):
state = [False if v == "false" else True for v in sttgns.value("revisionsState")]
self.redacMetadata.revisions.restoreState(state)
if sttgns.contains("splitterRedacH"):
self.splitterRedacH.restoreState(sttgns.value("splitterRedacH"))
if sttgns.contains("splitterRedacV"):
self.splitterRedacV.restoreState(sttgns.value("splitterRedacV"))
if sttgns.contains("toolbar"):
# self.toolbar is not initialized yet, so we just store balue
self._toolbarState = sttgns.value("toolbar")
else:
self._toolbarState = ""
def closeEvent(self, event):
# Save State and geometry and other things
sttgns = QSettings(qApp.organizationName(), qApp.applicationName())
sttgns.setValue("geometry", self.saveGeometry())
sttgns.setValue("windowState", self.saveState())
sttgns.setValue("metadataState", self.redacMetadata.saveState())
sttgns.setValue("revisionsState", self.redacMetadata.revisions.saveState())
sttgns.setValue("splitterRedacH", self.splitterRedacH.saveState())
sttgns.setValue("splitterRedacV", self.splitterRedacV.saveState())
sttgns.setValue("toolbar", self.toolbar.saveState())
# Specific settings to save before quitting
settings.lastTab = self.tabMain.currentIndex()
if self.currentProject:
# Remembering the current items (stores outlineItem's ID)
settings.openIndexes = self.mainEditor.tabSplitter.openIndexes()
# Save data from models
if self.currentProject and settings.saveOnQuit:
self.saveDatas()
# closeEvent
# QMainWindow.closeEvent(self, event) # Causin segfaults?
def startTimerNoChanges(self):
if settings.autoSaveNoChanges:
self.saveTimerNoChanges.start()
def saveDatas(self, projectName=None):
"""Saves the current project (in self.currentProject).
If ``projectName`` is given, currentProject becomes projectName.
In other words, it "saves as...".
"""
if projectName:
self.currentProject = projectName
QSettings().setValue("lastProject", projectName)
loadSave.saveProject() # version=0
self.saveTimerNoChanges.stop()
# Giving some feedback
print(self.tr("Project {} saved.").format(self.currentProject))
self.statusBar().showMessage(
self.tr("Project {} saved.").format(self.currentProject), 5000)
def loadEmptyDatas(self):
self.mdlFlatData = QStandardItemModel(self)
self.mdlCharacter = characterModel(self)
# self.mdlPersosProxy = persosProxyModel(self)
# self.mdlPersosInfos = QStandardItemModel(self)
self.mdlLabels = QStandardItemModel(self)
self.mdlStatus = QStandardItemModel(self)
self.mdlPlots = plotModel(self)
self.mdlOutline = outlineModel(self)
self.mdlWorld = worldModel(self)
def loadDatas(self, project):
errors = loadSave.loadProject(project)
# Giving some feedback
if not errors:
print(self.tr("Project {} loaded.").format(project))
self.statusBar().showMessage(
self.tr("Project {} loaded.").format(project), 5000)
else:
print(self.tr("Project {} loaded with some errors:").format(project))
for e in errors:
print(self.tr(" * {} wasn't found in project file.").format(e))
self.statusBar().showMessage(
self.tr("Project {} loaded with some errors.").format(project), 5000)
###############################################################################
# MAIN CONNECTIONS
###############################################################################
def makeUIConnections(self):
"Connections that have to be made once only, even when a new project is loaded."
self.lstCharacters.currentItemChanged.connect(self.changeCurrentCharacter, AUC)
self.txtPlotFilter.textChanged.connect(self.lstPlots.setFilter, AUC)
self.lstPlots.currentItemChanged.connect(self.changeCurrentPlot, AUC)
self.txtSubPlotSummary.document().contentsChanged.connect(
self.updateSubPlotSummary, AUC)
self.lstSubPlots.clicked.connect(self.changeCurrentSubPlot, AUC)
self.btnRedacAddFolder.clicked.connect(self.treeRedacOutline.addFolder, AUC)
self.btnOutlineAddFolder.clicked.connect(self.treeOutlineOutline.addFolder, AUC)
self.btnRedacAddText.clicked.connect(self.treeRedacOutline.addText, AUC)
self.btnOutlineAddText.clicked.connect(self.treeOutlineOutline.addText, AUC)
self.btnRedacRemoveItem.clicked.connect(self.outlineRemoveItemsRedac, AUC)
self.btnOutlineRemoveItem.clicked.connect(self.outlineRemoveItemsOutline, AUC)
self.tabMain.currentChanged.connect(self.toolbar.setCurrentGroup)
def makeConnections(self):
# Flat datas (Summary and general infos)
for widget, col in [
(self.txtSummarySituation, 0),
(self.txtSummarySentence, 1),
(self.txtSummarySentence_2, 1),
(self.txtSummaryPara, 2),
(self.txtSummaryPara_2, 2),
(self.txtPlotSummaryPara, 2),
(self.txtSummaryPage, 3),
(self.txtSummaryPage_2, 3),
(self.txtPlotSummaryPage, 3),
(self.txtSummaryFull, 4),
(self.txtPlotSummaryFull, 4),
]:
widget.setModel(self.mdlFlatData)
widget.setColumn(col)
widget.setCurrentModelIndex(self.mdlFlatData.index(1, col))
for widget, col in [
(self.txtGeneralTitle, 0),
(self.txtGeneralSubtitle, 1),
(self.txtGeneralSerie, 2),
(self.txtGeneralVolume, 3),
(self.txtGeneralGenre, 4),
(self.txtGeneralLicense, 5),
(self.txtGeneralAuthor, 6),
(self.txtGeneralEmail, 7),
]:
widget.setModel(self.mdlFlatData)
widget.setColumn(col)
widget.setCurrentModelIndex(self.mdlFlatData.index(0, col))
# Characters
self.lstCharacters.setCharactersModel(self.mdlCharacter)
self.tblPersoInfos.setModel(self.mdlCharacter)
self.btnAddPerso.clicked.connect(self.mdlCharacter.addCharacter, AUC)
try:
self.btnRmPerso.clicked.connect(self.lstCharacters.removeCharacter, AUC)
self.btnPersoColor.clicked.connect(self.lstCharacters.choseCharacterColor, AUC)
self.btnPersoAddInfo.clicked.connect(self.lstCharacters.addCharacterInfo, AUC)
self.btnPersoRmInfo.clicked.connect(self.lstCharacters.removeCharacterInfo, AUC)
except TypeError:
# Connection has already been made
pass
for w, c in [
(self.txtPersoName, Character.name.value),
(self.sldPersoImportance, Character.importance.value),
(self.txtPersoMotivation, Character.motivation.value),
(self.txtPersoGoal, Character.goal.value),
(self.txtPersoConflict, Character.conflict.value),
(self.txtPersoEpiphany, Character.epiphany.value),
(self.txtPersoSummarySentence, Character.summarySentence.value),
(self.txtPersoSummaryPara, Character.summaryPara.value),
(self.txtPersoSummaryFull, Character.summaryFull.value),
(self.txtPersoNotes, Character.notes.value)
]:
w.setModel(self.mdlCharacter)
w.setColumn(c)
self.tabPersos.setEnabled(False)
# Plots
self.lstPlots.setPlotModel(self.mdlPlots)
self.lstPlotPerso.setModel(self.mdlPlots)
self.lstSubPlots.setModel(self.mdlPlots)
self._updatingSubPlot = False
self.btnAddPlot.clicked.connect(self.mdlPlots.addPlot, AUC)
self.btnRmPlot.clicked.connect(lambda:
self.mdlPlots.removePlot(self.lstPlots.currentPlotIndex()), AUC)
self.btnAddSubPlot.clicked.connect(self.mdlPlots.addSubPlot, AUC)
self.btnAddSubPlot.clicked.connect(self.updateSubPlotView, AUC)
self.btnRmSubPlot.clicked.connect(self.mdlPlots.removeSubPlot, AUC)
self.lstPlotPerso.selectionModel().selectionChanged.connect(self.plotPersoSelectionChanged)
self.btnRmPlotPerso.clicked.connect(self.mdlPlots.removePlotPerso, AUC)
for w, c in [
(self.txtPlotName, Plot.name.value),
(self.txtPlotDescription, Plot.description.value),
(self.txtPlotResult, Plot.result.value),
(self.sldPlotImportance, Plot.importance.value),
]:
w.setModel(self.mdlPlots)
w.setColumn(c)
self.tabPlot.setEnabled(False)
self.mdlPlots.updatePlotPersoButton()
self.mdlCharacter.dataChanged.connect(self.mdlPlots.updatePlotPersoButton)
self.lstOutlinePlots.setPlotModel(self.mdlPlots)
self.lstOutlinePlots.setShowSubPlot(True)
self.plotCharacterDelegate = outlineCharacterDelegate(self.mdlCharacter, self)
self.lstPlotPerso.setItemDelegate(self.plotCharacterDelegate)
self.plotDelegate = plotDelegate(self)
self.lstSubPlots.setItemDelegateForColumn(PlotStep.meta.value, self.plotDelegate)
# World
self.treeWorld.setModel(self.mdlWorld)
for i in range(self.mdlWorld.columnCount()):
self.treeWorld.hideColumn(i)
self.treeWorld.showColumn(0)
self.btnWorldEmptyData.setMenu(self.mdlWorld.emptyDataMenu())
self.treeWorld.selectionModel().selectionChanged.connect(self.changeCurrentWorld, AUC)
self.btnAddWorld.clicked.connect(self.mdlWorld.addItem, AUC)
self.btnRmWorld.clicked.connect(self.mdlWorld.removeItem, AUC)
for w, c in [
(self.txtWorldName, World.name.value),
(self.txtWorldDescription, World.description.value),
(self.txtWorldPassion, World.passion.value),
(self.txtWorldConflict, World.conflict.value),
]:
w.setModel(self.mdlWorld)
w.setColumn(c)
self.tabWorld.setEnabled(False)
self.treeWorld.expandAll()
# Outline
self.treeRedacOutline.setModel(self.mdlOutline)
self.treeOutlineOutline.setModelCharacters(self.mdlCharacter)
self.treeOutlineOutline.setModelLabels(self.mdlLabels)
self.treeOutlineOutline.setModelStatus(self.mdlStatus)
self.redacMetadata.setModels(self.mdlOutline, self.mdlCharacter,
self.mdlLabels, self.mdlStatus)
self.outlineItemEditor.setModels(self.mdlOutline, self.mdlCharacter,
self.mdlLabels, self.mdlStatus)
self.treeOutlineOutline.setModel(self.mdlOutline)
# self.redacEditor.setModel(self.mdlOutline)
self.storylineView.setModels(self.mdlOutline, self.mdlCharacter, self.mdlPlots)
self.treeOutlineOutline.selectionModel().selectionChanged.connect(self.outlineItemEditor.selectionChanged, AUC)
self.treeOutlineOutline.clicked.connect(self.outlineItemEditor.selectionChanged, AUC)
# Sync selection
self.treeRedacOutline.selectionModel().selectionChanged.connect(self.redacMetadata.selectionChanged, AUC)
self.treeRedacOutline.clicked.connect(self.redacMetadata.selectionChanged, AUC)
self.treeRedacOutline.selectionModel().selectionChanged.connect(self.mainEditor.selectionChanged, AUC)
# Cheat Sheet
self.cheatSheet.setModels()
# Debug
self.mdlFlatData.setVerticalHeaderLabels(["Infos générales", "Summary"])
self.tblDebugFlatData.setModel(self.mdlFlatData)
self.tblDebugPersos.setModel(self.mdlCharacter)
self.tblDebugPersosInfos.setModel(self.mdlCharacter)
self.tblDebugPersos.selectionModel().currentChanged.connect(
lambda: self.tblDebugPersosInfos.setRootIndex(self.mdlCharacter.index(
self.tblDebugPersos.selectionModel().currentIndex().row(),
Character.name.value)), AUC)
self.tblDebugPlots.setModel(self.mdlPlots)
self.tblDebugPlotsPersos.setModel(self.mdlPlots)
self.tblDebugSubPlots.setModel(self.mdlPlots)
self.tblDebugPlots.selectionModel().currentChanged.connect(
lambda: self.tblDebugPlotsPersos.setRootIndex(self.mdlPlots.index(
self.tblDebugPlots.selectionModel().currentIndex().row(),
Plot.characters.value)), AUC)
self.tblDebugPlots.selectionModel().currentChanged.connect(
lambda: self.tblDebugSubPlots.setRootIndex(self.mdlPlots.index(
self.tblDebugPlots.selectionModel().currentIndex().row(),
Plot.steps.value)), AUC)
self.treeDebugWorld.setModel(self.mdlWorld)
self.treeDebugOutline.setModel(self.mdlOutline)
self.lstDebugLabels.setModel(self.mdlLabels)
self.lstDebugStatus.setModel(self.mdlStatus)
def disconnectAll(self, signal, oldHandler=None):
# Disconnect all "oldHandler" slot connections for a signal
#
# Ref: PyQt Widget connect() and disconnect()
# https://stackoverflow.com/questions/21586643/pyqt-widget-connect-and-disconnect
#
# The loop is needed for safely disconnecting a specific handler,
# because it may have been connected multiple times, and
# disconnect only removes one connection at a time.
while True:
try:
if oldHandler is not None:
signal.disconnect(oldHandler)
else:
signal.disconnect()
except TypeError:
break
def breakConnections(self):
# Break connections for UI elements that were connected in makeConnections()
# Characters
self.disconnectAll(self.btnAddPerso.clicked, self.mdlCharacter.addCharacter)
self.disconnectAll(self.btnRmPerso.clicked, self.lstCharacters.removeCharacter)
self.disconnectAll(self.btnPersoColor.clicked, self.lstCharacters.choseCharacterColor)
self.disconnectAll(self.btnPersoAddInfo.clicked, self.lstCharacters.addCharacterInfo)
self.disconnectAll(self.btnPersoRmInfo.clicked, self.lstCharacters.removeCharacterInfo)
# Plots
self._updatingSubPlot = False
self.disconnectAll(self.btnAddPlot.clicked, self.mdlPlots.addPlot)
self.disconnectAll(self.btnRmPlot.clicked, lambda:
self.mdlPlots.removePlot(self.lstPlots.currentPlotIndex()))
self.disconnectAll(self.btnAddSubPlot.clicked, self.mdlPlots.addSubPlot)
self.disconnectAll(self.btnAddSubPlot.clicked, self.updateSubPlotView)
self.disconnectAll(self.btnRmSubPlot.clicked, self.mdlPlots.removeSubPlot)
self.disconnectAll(self.lstPlotPerso.selectionModel().selectionChanged, self.plotPersoSelectionChanged)
self.disconnectAll(self.btnRmPlotPerso.clicked, self.mdlPlots.removePlotPerso)
self.disconnectAll(self.mdlCharacter.dataChanged, self.mdlPlots.updatePlotPersoButton)
# World
self.disconnectAll(self.treeWorld.selectionModel().selectionChanged, self.changeCurrentWorld)
self.disconnectAll(self.btnAddWorld.clicked, self.mdlWorld.addItem)
self.disconnectAll(self.btnRmWorld.clicked, self.mdlWorld.removeItem)
# Outline
self.disconnectAll(self.treeOutlineOutline.selectionModel().selectionChanged, self.outlineItemEditor.selectionChanged)
self.disconnectAll(self.treeOutlineOutline.clicked, self.outlineItemEditor.selectionChanged)
# Sync selection
self.disconnectAll(self.treeRedacOutline.selectionModel().selectionChanged, self.redacMetadata.selectionChanged)
self.disconnectAll(self.treeRedacOutline.clicked, self.redacMetadata.selectionChanged)
self.disconnectAll(self.treeRedacOutline.selectionModel().selectionChanged, self.mainEditor.selectionChanged)
# Debug
self.disconnectAll(self.tblDebugPersos.selectionModel().currentChanged,
lambda: self.tblDebugPersosInfos.setRootIndex(self.mdlCharacter.index(
self.tblDebugPersos.selectionModel().currentIndex().row(),
Character.name.value)))
self.disconnectAll(self.tblDebugPlots.selectionModel().currentChanged,
lambda: self.tblDebugPlotsPersos.setRootIndex(self.mdlPlots.index(
self.tblDebugPlots.selectionModel().currentIndex().row(),
Plot.characters.value)))
self.disconnectAll(self.tblDebugPlots.selectionModel().currentChanged,
lambda: self.tblDebugSubPlots.setRootIndex(self.mdlPlots.index(
self.tblDebugPlots.selectionModel().currentIndex().row(),
Plot.steps.value)))
###############################################################################
# GENERAL AKA UNSORTED
###############################################################################
def clickCycle(self, i):
if i == 0: # step 2 - paragraph summary
self.tabMain.setCurrentIndex(self.TabSummary)
self.tabSummary.setCurrentIndex(1)
if i == 1: # step 3 - characters summary
self.tabMain.setCurrentIndex(self.TabPersos)
self.tabPersos.setCurrentIndex(0)
if i == 2: # step 4 - page summary
self.tabMain.setCurrentIndex(self.TabSummary)
self.tabSummary.setCurrentIndex(2)
if i == 3: # step 5 - characters description
self.tabMain.setCurrentIndex(self.TabPersos)
self.tabPersos.setCurrentIndex(1)
if i == 4: # step 6 - four page synopsis
self.tabMain.setCurrentIndex(self.TabSummary)
self.tabSummary.setCurrentIndex(3)
if i == 5: # step 7 - full character charts
self.tabMain.setCurrentIndex(self.TabPersos)
self.tabPersos.setCurrentIndex(2)
if i == 6: # step 8 - scene list
self.tabMain.setCurrentIndex(self.TabPlots)
def wordCount(self, i):
src = {
0: self.txtSummarySentence,
1: self.txtSummaryPara,
2: self.txtSummaryPage,
3: self.txtSummaryFull
}[i]
lbl = {
0: self.lblSummaryWCSentence,
1: self.lblSummaryWCPara,
2: self.lblSummaryWCPage,
3: self.lblSummaryWCFull
}[i]
wc = wordCount(src.toPlainText())
if i in [2, 3]:
pages = self.tr(" (~{} pages)").format(int(wc / 25) / 10.)
else:
pages = ""
lbl.setText(self.tr("Words: {}{}").format(wc, pages))
def setupMoreUi(self):
style.styleMainWindow(self)
# Tool bar on the right
self.toolbar = collapsibleDockWidgets(Qt.RightDockWidgetArea, self)
self.toolbar.addCustomWidget(self.tr("Book summary"), self.grpPlotSummary, self.TabPlots)
self.toolbar.addCustomWidget(self.tr("Project tree"), self.treeRedacWidget, self.TabRedac)
self.toolbar.addCustomWidget(self.tr("Metadata"), self.redacMetadata, self.TabRedac)
self.toolbar.addCustomWidget(self.tr("Story line"), self.storylineView, self.TabRedac)
if self._toolbarState:
self.toolbar.restoreState(self._toolbarState)
# Custom "tab" bar on the left
self.lstTabs.setIconSize(QSize(48, 48))
for i in range(self.tabMain.count()):
icons = ["general-128px.png",
"summary-128px.png",
"characters-128px.png",
"plot-128px.png",
"world-128px.png",
"outline-128px.png",
"redaction-128px.png",
""
]
self.tabMain.setTabIcon(i, QIcon(appPath("icons/Custom/Tabs/{}".format(icons[i]))))
item = QListWidgetItem(self.tabMain.tabIcon(i),
self.tabMain.tabText(i))
item.setSizeHint(QSize(item.sizeHint().width(), 64))
item.setTextAlignment(Qt.AlignCenter)
self.lstTabs.addItem(item)
self.tabMain.tabBar().hide()
self.lstTabs.currentRowChanged.connect(self.tabMain.setCurrentIndex)
self.tabMain.currentChanged.connect(self.lstTabs.setCurrentRow)
# Splitters
self.splitterPersos.setStretchFactor(0, 25)
self.splitterPersos.setStretchFactor(1, 75)
self.splitterPlot.setStretchFactor(0, 20)
self.splitterPlot.setStretchFactor(1, 60)
self.splitterPlot.setStretchFactor(2, 30)
self.splitterWorld.setStretchFactor(0, 25)
self.splitterWorld.setStretchFactor(1, 75)
self.splitterOutlineH.setStretchFactor(0, 25)
self.splitterOutlineH.setStretchFactor(1, 75)
self.splitterOutlineV.setStretchFactor(0, 75)
self.splitterOutlineV.setStretchFactor(1, 25)
self.splitterRedacV.setStretchFactor(0, 75)
self.splitterRedacV.setStretchFactor(1, 25)
self.splitterRedacH.setStretchFactor(0, 30)
self.splitterRedacH.setStretchFactor(1, 40)
self.splitterRedacH.setStretchFactor(2, 30)
# QFormLayout stretch
for w in [self.txtWorldDescription, self.txtWorldPassion, self.txtWorldConflict]:
s = w.sizePolicy()
s.setVerticalStretch(1)
w.setSizePolicy(s)
# Help box
references = [
(self.lytTabOverview,
self.tr("Enter infos about your book, and yourself."),
0),
(self.lytSituation,
self.tr(
"""The basic situation, in the form of a 'What if...?' question. Ex: 'What if the most dangerous
evil wizard could wasn't abled to kill a baby?' (Harry Potter)"""),
1),
(self.lytSummary,
self.tr(
"""Take time to think about a one sentence (~50 words) summary of your book. Then expand it to
a paragraph, then to a page, then to a full summary."""),
1),
(self.lytTabPersos,
self.tr("Create your characters."),
0),
(self.lytTabPlot,
self.tr("Develop plots."),
0),
(self.lytTabOutline,
self.tr("Create the outline of your masterpiece."),
0),
(self.lytTabRedac,
self.tr("Write."),
0),
(self.lytTabDebug,
self.tr("Debug infos. Sometimes useful."),
0)
]
for widget, text, pos in references:
label = helpLabel(text, self)
self.actShowHelp.toggled.connect(label.setVisible, AUC)
widget.layout().insertWidget(pos, label)
self.actShowHelp.setChecked(False)
# Spellcheck
if enchant:
self.menuDict = QMenu(self.tr("Dictionary"))
self.menuDictGroup = QActionGroup(self)
self.updateMenuDict()
self.menuTools.addMenu(self.menuDict)
self.actSpellcheck.toggled.connect(self.toggleSpellcheck, AUC)
self.dictChanged.connect(self.mainEditor.setDict, AUC)
self.dictChanged.connect(self.redacMetadata.setDict, AUC)
self.dictChanged.connect(self.outlineItemEditor.setDict, AUC)
else:
# No Spell check support
self.actSpellcheck.setVisible(False)
a = QAction(self.tr("Install PyEnchant to use spellcheck"), self)
a.setIcon(self.style().standardIcon(QStyle.SP_MessageBoxWarning))
a.triggered.connect(self.openPyEnchantWebPage, AUC)
self.menuTools.addAction(a)
###############################################################################
# SPELLCHECK
###############################################################################
def updateMenuDict(self):
if not enchant:
return
self.menuDict.clear()
for i in enchant.list_dicts():
a = QAction(str(i[0]), self)
a.setCheckable(True)
if settings.dict is None:
settings.dict = enchant.get_default_language()
if str(i[0]) == settings.dict:
a.setChecked(True)
a.triggered.connect(self.setDictionary, AUC)
self.menuDictGroup.addAction(a)
self.menuDict.addAction(a)
def setDictionary(self):
if not enchant:
return
for i in self.menuDictGroup.actions():
if i.isChecked():
# self.dictChanged.emit(i.text().replace("&", ""))
settings.dict = i.text().replace("&", "")
# Find all textEditView from self, and toggle spellcheck
for w in self.findChildren(textEditView, QRegExp(".*"),
Qt.FindChildrenRecursively):
w.setDict(settings.dict)
def openPyEnchantWebPage(self):
from PyQt5.QtGui import QDesktopServices
QDesktopServices.openUrl(QUrl("http://pythonhosted.org/pyenchant/"))
def toggleSpellcheck(self, val):
settings.spellcheck = val
# Find all textEditView from self, and toggle spellcheck
for w in self.findChildren(textEditView, QRegExp(".*"),
Qt.FindChildrenRecursively):
w.toggleSpellcheck(val)
###############################################################################
# SETTINGS
###############################################################################
def settingsLabel(self):
self.settingsWindow(3)
def settingsStatus(self):
self.settingsWindow(4)
def settingsWindow(self, tab=None):
self.sw = settingsWindow(self)
self.sw.hide()
self.sw.setWindowModality(Qt.ApplicationModal)
self.sw.setWindowFlags(Qt.Dialog)
r = self.sw.geometry()
r2 = self.geometry()
self.sw.move(r2.center() - r.center())
if tab:
self.sw.setTab(tab)
self.sw.show()
###############################################################################
# TOOLS
###############################################################################
def frequencyAnalyzer(self):
self.fw = frequencyAnalyzer(self)
self.fw.show()
###############################################################################
# VIEW MENU
###############################################################################
def generateViewMenu(self):
values = [
(self.tr("Nothing"), "Nothing"),
(self.tr("POV"), "POV"),
(self.tr("Label"), "Label"),
(self.tr("Progress"), "Progress"),
(self.tr("Compile"), "Compile"),
]
menus = [
(self.tr("Tree"), "Tree"),
(self.tr("Index cards"), "Cork"),
(self.tr("Outline"), "Outline")
]
submenus = {
"Tree": [
(self.tr("Icon color"), "Icon"),
(self.tr("Text color"), "Text"),
(self.tr("Background color"), "Background"),
],
"Cork": [
(self.tr("Icon"), "Icon"),
(self.tr("Text"), "Text"),
(self.tr("Background"), "Background"),
(self.tr("Border"), "Border"),
(self.tr("Corner"), "Corner"),
],
"Outline": [
(self.tr("Icon color"), "Icon"),
(self.tr("Text color"), "Text"),
(self.tr("Background color"), "Background"),
],
}
self.menuView.clear()
self.menuView.addMenu(self.menuMode)
self.menuView.addSeparator()
# print("Generating menus with", settings.viewSettings)
for mnu, mnud in menus:
m = QMenu(mnu, self.menuView)
for s, sd in submenus[mnud]:
m2 = QMenu(s, m)
agp = QActionGroup(m2)
for v, vd in values:
a = QAction(v, m)
a.setCheckable(True)
a.setData("{},{},{}".format(mnud, sd, vd))
if settings.viewSettings[mnud][sd] == vd:
a.setChecked(True)
a.triggered.connect(self.setViewSettingsAction, AUC)
agp.addAction(a)
m2.addAction(a)
m.addMenu(m2)
self.menuView.addMenu(m)
def setViewSettingsAction(self):
action = self.sender()
item, part, element = action.data().split(",")
self.setViewSettings(item, part, element)
def setViewSettings(self, item, part, element):
settings.viewSettings[item][part] = element
if item == "Cork":
self.mainEditor.updateCorkView()
if item == "Outline":
self.mainEditor.updateTreeView()
self.treeOutlineOutline.viewport().update()
if item == "Tree":
self.treeRedacOutline.viewport().update()
###############################################################################
# VIEW MODES
###############################################################################
def setViewModeSimple(self):
settings.viewMode = "simple"
self.tabMain.setCurrentIndex(self.TabRedac)
self.viewModeFictionVisibilitySwitch(False)
self.actModeSimple.setChecked(True)
def setViewModeFiction(self):
settings.viewMode = "fiction"
self.viewModeFictionVisibilitySwitch(True)
self.actModeFiction.setChecked(True)
def viewModeFictionVisibilitySwitch(self, val):
"""
Swtiches the visibility of some UI components useful for fiction only
@param val: sets visibility to val
"""
# Menu navigation & boutton in toolbar
self.toolbar.setDockVisibility(self.dckNavigation, val)
# POV in metadatas
from manuskript.ui.views.propertiesView import propertiesView
for w in findWidgetsOfClass(propertiesView):
w.lblPOV.setVisible(val)
w.cmbPOV.setVisible(val)
# POV in outline view
if Outline.POV.value in settings.outlineViewColumns:
settings.outlineViewColumns.remove(Outline.POV.value)
from manuskript.ui.views.outlineView import outlineView
for w in findWidgetsOfClass(outlineView):
w.hideColumns()
# TODO: clean up all other fiction things in non-fiction view mode
# Character in search widget
# POV in settings / views
###############################################################################
# COMPILE
###############################################################################
def doCompile(self):
self.dialog = exporterDialog(mw=self)
self.dialog.show()
r = self.dialog.geometry()
r2 = self.geometry()
self.dialog.move(r2.center() - r.center())