# -*- coding: utf-8 -*- """novelWriter GUI Project Settings novelWriter – GUI Project Settings ==================================== Class holding the project settings dialog File History: Created: 2018-09-29 [0.0.1] This file is a part of novelWriter Copyright 2018–2020, Veronica Berglyd Olsen This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import nw import logging from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush from PyQt5.QtWidgets import ( QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QPlainTextEdit, QLabel, QWidget, QDialogButtonBox, QListWidget, QPushButton, QListWidgetItem, QColorDialog, QAbstractItemView, QTreeWidget, QTreeWidgetItem, QComboBox ) from nw.constants import nwAlert from nw.gui.custom import QSwitch, PagedDialog, QConfigLayout logger = logging.getLogger(__name__) class GuiProjectSettings(PagedDialog): def __init__(self, theParent, theProject): PagedDialog.__init__(self, theParent) logger.debug("Initialising GuiProjectSettings ...") self.setObjectName("GuiProjectSettings") self.mainConf = nw.CONFIG self.theParent = theParent self.theProject = theProject self.optState = theProject.optState self.theProject.countStatus() self.setWindowTitle("Project Settings") wW = self.mainConf.pxInt(570) wH = self.mainConf.pxInt(375) self.setMinimumWidth(wW) self.setMinimumHeight(wH) self.resize( self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winWidth", wW)), self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winHeight", wH)) ) self.tabMain = GuiProjectEditMain(self.theParent, self.theProject) self.tabMeta = GuiProjectEditMeta(self.theParent, self.theProject) self.tabStatus = GuiProjectEditStatus(self.theParent, self.theProject, True) self.tabImport = GuiProjectEditStatus(self.theParent, self.theProject, False) self.tabReplace = GuiProjectEditReplace(self.theParent, self.theProject) self.addTab(self.tabMain, "Settings") self.addTab(self.tabMeta, "Details") self.addTab(self.tabStatus, "Status") self.addTab(self.tabImport, "Importance") self.addTab(self.tabReplace, "Auto-Replace") self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox.accepted.connect(self._doSave) self.buttonBox.rejected.connect(self._doClose) self.addControls(self.buttonBox) logger.debug("GuiProjectSettings initialisation complete") return ## # Slots ## def _doSave(self): """Save settings and close dialog. """ logger.verbose("GuiProjectSettings save button clicked") projName = self.tabMain.editName.text() bookTitle = self.tabMain.editTitle.text() bookAuthors = self.tabMain.editAuthors.toPlainText() spellLang = self.tabMain.spellLang.currentData() doBackup = not self.tabMain.doBackup.isChecked() self.theProject.setProjectName(projName) self.theProject.setBookTitle(bookTitle) self.theProject.setBookAuthors(bookAuthors) self.theProject.setSpellLang(spellLang) self.theProject.setProjBackup(doBackup) if self.tabStatus.colChanged: statusCol = self.tabStatus.getNewList() self.theProject.setStatusColours(statusCol) if self.tabImport.colChanged: importCol = self.tabImport.getNewList() self.theProject.setImportColours(importCol) if self.tabStatus.colChanged or self.tabImport.colChanged: self.theParent.rebuildTree() if self.tabReplace.arChanged: newList = self.tabReplace.getNewList() self.theProject.setAutoReplace(newList) self._doClose() return def _doClose(self): """Save settings and close the dialog. """ winWidth = self.mainConf.rpxInt(self.width()) winHeight = self.mainConf.rpxInt(self.height()) replaceColW = self.mainConf.rpxInt(self.tabReplace.listBox.columnWidth(0)) self.optState.setValue("GuiProjectSettings", "winWidth", winWidth) self.optState.setValue("GuiProjectSettings", "winHeight", winHeight) self.optState.setValue("GuiProjectSettings", "replaceColW", replaceColW) self.close() return # END Class GuiProjectSettings class GuiProjectEditMain(QWidget): def __init__(self, theParent, theProject): QWidget.__init__(self, theParent) self.mainConf = nw.CONFIG self.theParent = theParent self.theProject = theProject # The Form self.mainForm = QConfigLayout() self.mainForm.setHelpTextStyle(self.theParent.theTheme.helpText) self.setLayout(self.mainForm) self.mainForm.addGroupLabel("Project Settings") xW = self.mainConf.pxInt(250) xH = self.mainConf.pxInt(100) self.editName = QLineEdit() self.editName.setMaxLength(200) self.editName.setFixedWidth(xW) self.editName.setText(self.theProject.projName) self.mainForm.addRow( "Working title", self.editName, "Should be set only once." ) self.editTitle = QLineEdit() self.editTitle.setMaxLength(200) self.editTitle.setFixedWidth(xW) self.editTitle.setText(self.theProject.bookTitle) self.mainForm.addRow( "Novel title", self.editTitle, "Change whenever you want!" ) self.editAuthors = QPlainTextEdit() bookAuthors = "" for bookAuthor in self.theProject.bookAuthors: bookAuthors += bookAuthor+"\n" self.editAuthors.setPlainText(bookAuthors) self.editAuthors.setFixedHeight(xH) self.editAuthors.setFixedWidth(xW) self.mainForm.addRow( "Author(s)", self.editAuthors, "One name per line." ) self.spellLang = QComboBox(self) theDict = self.theParent.docEditor.theDict self.spellLang.addItem("Default", "None") if theDict is not None: for spTag, spName in theDict.listDictionaries(): self.spellLang.addItem(spName, spTag) self.mainForm.addRow( "Spell check language", self.spellLang, "Overrides main preferences." ) if self.theProject.projLang is None: spellIdx = 0 else: spellIdx = self.spellLang.findData(self.theProject.projLang) if spellIdx != -1: self.spellLang.setCurrentIndex(spellIdx) self.doBackup = QSwitch(self) self.doBackup.setChecked(not self.theProject.doBackup) self.mainForm.addRow( "No backup on close", self.doBackup, "Overrides main preferences." ) return # END Class GuiProjectEditMain class GuiProjectEditMeta(QWidget): def __init__(self, theParent, theProject): QWidget.__init__(self, theParent) self.mainConf = nw.CONFIG self.theParent = theParent self.theProject = theProject xInd = self.mainConf.pxInt(8) # The Form self.mainForm = QGridLayout() self.setLayout(self.mainForm) self.headLabel = QLabel("Project Details") self.nameLabel = QLabel("Working title:") self.nameLabel.setIndent(xInd) self.nameValue = QLabel(self.theProject.projName) self.nameValue.setWordWrap(True) self.pathLabel = QLabel("Project path:") self.pathLabel.setIndent(xInd) self.pathValue = QLabel(self.theProject.projPath) self.pathValue.setWordWrap(True) self.pathValue.setTextInteractionFlags(Qt.TextSelectableByMouse) self.pathValue.setCursor(Qt.IBeamCursor) self.revLabel = QLabel("Revision count:") self.revLabel.setIndent(xInd) self.revValue = QLabel("{:n}".format(self.theProject.saveCount)) self.editLabel = QLabel("Edit time:") self.editLabel.setIndent(xInd) self.editValue = QLabel("{:.2f} hours".format(self.theProject.editTime/3600)) self.statsLabel = QLabel("Project Stats") nR, nD, nF = self.theProject.projTree.countTypes() self.nRootLabel = QLabel("Root folders:") self.nRootLabel.setIndent(xInd) self.nRootValue = QLabel("{:n}".format(nR)) self.nDirLabel = QLabel("Folders:") self.nDirLabel.setIndent(xInd) self.nDirValue = QLabel("{:n}".format(nD)) self.nFileLabel = QLabel("Documents:") self.nFileLabel.setIndent(xInd) self.nFileValue = QLabel("{:n}".format(nF)) self.wordsLabel = QLabel("Word count:") self.wordsLabel.setIndent(xInd) self.wordsValue = QLabel("{:n}".format(self.theProject.currWCount)) self.mainForm.addWidget(self.headLabel, 0, 0, 1, 2, Qt.AlignTop) self.mainForm.addWidget(self.nameLabel, 1, 0, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.nameValue, 1, 1, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.pathLabel, 2, 0, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.pathValue, 2, 1, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.revLabel, 3, 0, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.revValue, 3, 1, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.editLabel, 4, 0, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.editValue, 4, 1, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.statsLabel, 5, 0, 1, 2, Qt.AlignTop) self.mainForm.addWidget(self.nRootLabel, 6, 0, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.nRootValue, 6, 1, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.nDirLabel, 7, 0, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.nDirValue, 7, 1, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.nFileLabel, 8, 0, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.nFileValue, 8, 1, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.wordsLabel, 9, 0, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.wordsValue, 9, 1, 1, 1, Qt.AlignTop) self.mainForm.setVerticalSpacing(self.mainConf.pxInt(6)) self.mainForm.setHorizontalSpacing(self.mainConf.pxInt(12)) self.mainForm.setColumnStretch(0, 0) self.mainForm.setColumnStretch(1, 1) self.mainForm.setRowStretch(10, 1) return # END Class GuiProjectEditMeta class GuiProjectEditStatus(QWidget): def __init__(self, theParent, theProject, isStatus): QWidget.__init__(self, theParent) self.mainConf = nw.CONFIG self.theParent = theParent self.theProject = theProject self.theTheme = theParent.theTheme if isStatus: self.theStatus = self.theProject.statusItems else: self.theStatus = self.theProject.importItems self.colData = [] self.colCounts = [] self.colChanged = False self.selColour = None self.iPx = self.theTheme.baseIconSize self.outerBox = QVBoxLayout() self.mainBox = QHBoxLayout() self.mainForm = QVBoxLayout() self.listBox = QListWidget() self.listBox.setDragDropMode(QAbstractItemView.InternalMove) self.listBox.itemSelectionChanged.connect(self._selectedItem) self.listBox.model().rowsMoved.connect(self._rowsMoved) for iName, iCol, nUse in self.theStatus: self._addItem(iName, iCol, iName, nUse) self.editName = QLineEdit() self.editName.setMaxLength(40) self.editName.setEnabled(False) self.newButton = QPushButton("New") self.delButton = QPushButton("Delete") self.saveButton = QPushButton("Save") self.colPixmap = QPixmap(self.iPx, self.iPx) self.colPixmap.fill(QColor(120, 120, 120)) self.colButton = QPushButton(QIcon(self.colPixmap), "Colour") self.colButton.setIconSize(self.colPixmap.rect().size()) self.newButton.clicked.connect(self._newItem) self.delButton.clicked.connect(self._delItem) self.saveButton.clicked.connect(self._saveItem) self.colButton.clicked.connect(self._selectColour) self.mainForm.addWidget(self.newButton) self.mainForm.addWidget(self.delButton) self.mainForm.addStretch(1) self.mainForm.addWidget(QLabel("Name")) self.mainForm.addWidget(self.editName) self.mainForm.addWidget(self.colButton) self.mainForm.addStretch(1) self.mainForm.addWidget(self.saveButton) self.mainBox.addWidget(self.listBox) self.mainBox.addLayout(self.mainForm) if isStatus: self.outerBox.addWidget(QLabel("Novel File Status Levels")) else: self.outerBox.addWidget(QLabel("Note File Importance Levels")) self.outerBox.addLayout(self.mainBox) self.setLayout(self.outerBox) return def getNewList(self): if self.colChanged: newList = [] for n in range(self.listBox.count()): nItem = self.listBox.item(n) nIdx = nItem.data(Qt.UserRole) newList.append(self.colData[nIdx]) return newList return None ## # User Actions ## def _selectColour(self): """Open a dialog to select the status icon colour. """ logger.verbose("Item colour button clicked") if self.selColour is not None: newCol = QColorDialog.getColor( self.selColour, self, "Select Colour", QColorDialog.DontUseNativeDialog ) if newCol.isValid(): self.selColour = newCol colPixmap = QPixmap(self.iPx, self.iPx) colPixmap.fill(newCol) self.colButton.setIcon(QIcon(colPixmap)) self.colButton.setIconSize(colPixmap.rect().size()) return def _newItem(self): """Create a new status item. """ logger.verbose("New item button clicked") newItem = self._addItem("New Item", (0, 0, 0), None, 0) newItem.setBackground(QBrush(QColor(0, 255, 0, 80))) self.colChanged = True return def _delItem(self): """Delete a status item. """ logger.verbose("Delete item button clicked") selItem = self._getSelectedItem() if selItem is not None: iRow = self.listBox.row(selItem) selIdx = selItem.data(Qt.UserRole) if self.colCounts[selIdx] == 0: self.listBox.takeItem(iRow) self.colChanged = True else: self.theParent.makeAlert( "Cannot delete status item that is in use.", nwAlert.ERROR ) return def _saveItem(self): """Save changes made to a status item. """ logger.verbose("Save item button clicked") selItem = self._getSelectedItem() if selItem is not None: selIdx = selItem.data(Qt.UserRole) self.colData[selIdx] = ( self.editName.text().strip(), self.selColour.red(), self.selColour.green(), self.selColour.blue(), self.colData[selIdx][4] ) selItem.setText("%s [%d]" % (self.colData[selIdx][0], self.colCounts[selIdx])) selItem.setIcon(self.colButton.icon()) self.editName.setEnabled(False) self.colChanged = True return def _addItem(self, iName, iCol, oName, nUse): """Add a status item to the list. """ newIcon = QPixmap(self.iPx, self.iPx) newIcon.fill(QColor(*iCol)) newItem = QListWidgetItem() newItem.setText("%s [%d]" % (iName, nUse)) newItem.setIcon(QIcon(newIcon)) newItem.setData(Qt.UserRole, len(self.colData)) self.listBox.addItem(newItem) self.colData.append((iName, iCol[0], iCol[1], iCol[2], oName)) self.colCounts.append(nUse) return newItem def _selectedItem(self): """Extract the info of a selected item and populate the settings boxes and button. """ logger.verbose("Item selected") selItem = self._getSelectedItem() if selItem is not None: selIdx = selItem.data(Qt.UserRole) selVal = self.colData[selIdx] self.selColour = QColor(selVal[1], selVal[2], selVal[3]) newIcon = QPixmap(self.iPx, self.iPx) newIcon.fill(self.selColour) self.editName.setText(selVal[0]) self.colButton.setIcon(QIcon(newIcon)) self.editName.setEnabled(True) self.editName.selectAll() self.editName.setFocus() return ## # Internal Functions ## def _getSelectedItem(self): """Get the currently selected item. """ selItem = self.listBox.selectedItems() if len(selItem) == 0: return None if isinstance(selItem[0], QListWidgetItem): return selItem[0] return None def _rowsMoved(self): """A row has been moved, so sett the changed flag. """ logger.verbose("A drag move event occurred") self.colChanged = True return # END Class GuiProjectEditStatus class GuiProjectEditReplace(QWidget): def __init__(self, theParent, theProject): QWidget.__init__(self, theParent) self.mainConf = nw.CONFIG self.theParent = theParent self.theTheme = theParent.theTheme self.theProject = theProject self.optState = theProject.optState self.arChanged = False self.outerBox = QVBoxLayout() self.bottomBox = QHBoxLayout() wCol0 = self.mainConf.pxInt( self.optState.getInt("GuiProjectSettings", "replaceColW", 100) ) self.listBox = QTreeWidget() self.listBox.setHeaderLabels(["Keyword", "Replace With"]) self.listBox.itemSelectionChanged.connect(self._selectedItem) self.listBox.setColumnWidth(0, wCol0) self.listBox.setIndentation(0) for aKey, aVal in self.theProject.autoReplace.items(): newItem = QTreeWidgetItem(["<%s>" % aKey, aVal]) self.listBox.addTopLevelItem(newItem) self.listBox.sortByColumn(0, Qt.AscendingOrder) self.listBox.setSortingEnabled(True) self.editKey = QLineEdit() self.editValue = QLineEdit() self.saveButton = QPushButton(self.theTheme.getIcon("done"), "") self.addButton = QPushButton(self.theTheme.getIcon("add"), "") self.delButton = QPushButton(self.theTheme.getIcon("remove"), "") self.saveButton.setToolTip("Save entry") self.addButton.setToolTip("Add new entry") self.delButton.setToolTip("Delete selected entry") self.editKey.setEnabled(False) self.editKey.setMaxLength(40) self.editValue.setEnabled(False) self.editValue.setMaxLength(80) self.saveButton.clicked.connect(self._saveEntry) self.addButton.clicked.connect(self._addEntry) self.delButton.clicked.connect(self._delEntry) self.bottomBox.addWidget(self.editKey, 2) self.bottomBox.addWidget(self.editValue, 3) self.bottomBox.addWidget(self.saveButton) self.bottomBox.addWidget(self.addButton) self.bottomBox.addWidget(self.delButton) self.outerBox.addWidget(QLabel("Text Replace List for Preview and Export")) self.outerBox.addWidget(self.listBox) self.outerBox.addLayout(self.bottomBox) self.setLayout(self.outerBox) return def getNewList(self): """Extract the list from the widget. """ newList = {} for n in range(self.listBox.topLevelItemCount()): tItem = self.listBox.topLevelItem(n) aKey = self._stripNotAllowed(tItem.text(0)) aVal = tItem.text(1) if len(aKey) > 0: newList[aKey] = aVal return newList ## # Internal Functions ## def _selectedItem(self): """Extract the details from the selected item and populate the edit form. """ selItem = self._getSelectedItem() if selItem is None: return False editKey = self._stripNotAllowed(selItem.text(0)) editVal = selItem.text(1) self.editKey.setText(editKey) self.editValue.setText(editVal) self.editKey.setEnabled(True) self.editValue.setEnabled(True) self.editKey.selectAll() self.editKey.setFocus() return True def _saveEntry(self): """Save the form data into the list widget. """ selItem = self._getSelectedItem() if selItem is None: return False newKey = self.editKey.text() newVal = self.editValue.text() saveKey = self._stripNotAllowed(newKey) if len(saveKey) > 0 and len(newVal) > 0: selItem.setText(0, "<%s>" % saveKey) selItem.setText(1, newVal) self.editKey.clear() self.editValue.clear() self.editKey.setEnabled(False) self.editValue.setEnabled(False) self.listBox.clearSelection() self.arChanged = True return def _addEntry(self): """Add a new list entry. """ saveKey = "" % (self.listBox.topLevelItemCount() + 1) newVal = "" newItem = QTreeWidgetItem([saveKey, newVal]) self.listBox.addTopLevelItem(newItem) return True def _delEntry(self): """Delete the selected entry. """ selItem = self._getSelectedItem() if selItem is None: return False self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(selItem)) self.arChanged = True return True def _getSelectedItem(self): """Extract the currently selected item. """ selItem = self.listBox.selectedItems() if len(selItem) == 0: return None return selItem[0] def _stripNotAllowed(self, theKey): """Clean up the replace key string. """ retKey = "" for c in theKey: if c.isalnum(): retKey += c return retKey # END Class GuiProjectEditReplace