diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index f64bc973..28de89d7 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,75 +2,131 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission +* Publishing others' private information, such as a physical or email address, + without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project leader at issues (at) novelwriter.io. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +reported to the community leaders responsible for enforcement at issues (at) +novelwriter.io. All complaints will be reviewed and investigated promptly and +fairly. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/docs/source/usage_shortcuts.rst b/docs/source/usage_shortcuts.rst index 69c2e773..e9e7bb36 100644 --- a/docs/source/usage_shortcuts.rst +++ b/docs/source/usage_shortcuts.rst @@ -25,7 +25,7 @@ The main shorcuts are as follows: ":kbd:`Alt`:kbd:`4`", "Switch focus to outline view. On Windows, use :kbd:`Ctrl`:kbd:`Alt`:kbd:`4`." ":kbd:`Alt`:kbd:`Left`", "Move backward in the view history of the document viewer." ":kbd:`Alt`:kbd:`Right`", "Move forward in the view history of the document viewer." - ":kbd:`Ctrl`:kbd:`.`", "Open menu to correct word under cursor." + ":kbd:`Ctrl`:kbd:`.`", "Open the context menu in the project tree or the document editor." ":kbd:`Ctrl`:kbd:`,`", "Open the :guilabel:`Preferences` dialog." ":kbd:`Ctrl`:kbd:`/`", "Toggle block format as comment." ":kbd:`Ctrl`:kbd:`0`", "Remove block formatting for block under cursor." @@ -42,13 +42,12 @@ The main shorcuts are as follows: ":kbd:`Ctrl`:kbd:`B`", "Format selected text, or word under cursor, with strong emphasis (bold)." ":kbd:`Ctrl`:kbd:`C`", "Copy selected text to clipboard." ":kbd:`Ctrl`:kbd:`D`", "Strikethrough selected text, or word under cursor." - ":kbd:`Ctrl`:kbd:`E`", "If in the project tree, edit a document or folder settings." ":kbd:`Ctrl`:kbd:`F`", "Open the search bar and search for the selected word, if any is selected." ":kbd:`Ctrl`:kbd:`G`", "Find next occurrence of search word in current document." ":kbd:`Ctrl`:kbd:`H`", "Open the search and replace bar and search for the selected word, if any is selected. (On Mac, this is :kbd:`Cmd`:kbd:`=`.)" ":kbd:`Ctrl`:kbd:`I`", "Format selected text, or word under cursor, with emphasis (italic)." ":kbd:`Ctrl`:kbd:`K`", "Activate the insert commands. The commands are listed in :ref:`a_kb_ins`." - ":kbd:`Ctrl`:kbd:`N`", "Create new document." + ":kbd:`Ctrl`:kbd:`N`", "Create new project item." ":kbd:`Ctrl`:kbd:`O`", "Open selected document." ":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter." ":kbd:`Ctrl`:kbd:`R`", "If in the project tree, open a document for viewing. If the editor has focus, open current document for viewing." @@ -72,7 +71,6 @@ The main shorcuts are as follows: ":kbd:`Ctrl`:kbd:`Shift`:kbd:`A`", "Select all text in current paragraph." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`G`", "Find previous occurrence of search word in current document." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`I`", "Import text to the current document from a text file." - ":kbd:`Ctrl`:kbd:`Shift`:kbd:`N`", "Create new folder." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`O`", "Open a project." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`R`", "Close the document viewer." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project." diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index ae157e2c..d4106803 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -83,3 +83,8 @@ view_build = typ_export.svg view_editor = mixed_edit.svg view_novel = typ_book-grey.svg view_outline = typ_puzzle-outline.svg + +deco_doc_h1 = nw_deco-h1.svg +deco_doc_h2 = nw_deco-h2.svg +deco_doc_h3 = nw_deco-h3.svg +deco_doc_h4 = nw_deco-h4.svg diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h0.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h0.svg new file mode 100644 index 00000000..3c1618c9 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h0.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h1.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h1.svg new file mode 100644 index 00000000..1c0dec9b --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h1.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h2.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h2.svg new file mode 100644 index 00000000..0f86e5bb --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h2.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h3.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h3.svg new file mode 100644 index 00000000..f05e46e6 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h3.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h4.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h4.svg new file mode 100644 index 00000000..aa74e6f3 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h4.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index 4683bb19..8d267a47 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -83,3 +83,8 @@ view_build = typ_export.svg view_editor = mixed_edit.svg view_novel = typ_book-grey.svg view_outline = typ_puzzle-outline.svg + +deco_doc_h1 = nw_deco-h1.svg +deco_doc_h2 = nw_deco-h2.svg +deco_doc_h3 = nw_deco-h3.svg +deco_doc_h4 = nw_deco-h4.svg diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h0.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h0.svg new file mode 100644 index 00000000..3c1618c9 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h0.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h1.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h1.svg new file mode 100644 index 00000000..e6c8efdc --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h1.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h2.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h2.svg new file mode 100644 index 00000000..7caa4203 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h2.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h3.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h3.svg new file mode 100644 index 00000000..61feca1b --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h3.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h4.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h4.svg new file mode 100644 index 00000000..b76fd7da --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h4.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/constants.py b/novelwriter/constants.py index dc62dbd3..3d1b5c13 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -57,6 +57,15 @@ class nwRegEx: # END Class nwRegEx +class nwHeaders: + + H_VALID = ("H0", "H1", "H2", "H3", "H4") + H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} + TT_NONE = "T000000" + +# END Class nwHeaders + + class nwFiles: PROJ_FILE = "nwProject.nwx" diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index f86de8c2..0b3afdc5 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -34,7 +34,7 @@ from time import time from novelwriter.enum import nwItemType, nwItemLayout from novelwriter.error import logException -from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode +from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode, nwHeaders from novelwriter.core.document import NWDoc from novelwriter.common import ( checkInt, isHandle, isItemClass, isTitleTag, jsonEncode @@ -42,10 +42,6 @@ from novelwriter.common import ( logger = logging.getLogger(__name__) -H_VALID = ("H0", "H1", "H2", "H3", "H4") -H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} -TT_NONE = "T000000" - class NWIndex: """This class holds the entire index for a given project. The index @@ -477,7 +473,7 @@ class NWIndex: """ hCount = [0, 0, 0, 0, 0] for _, _, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): - iLevel = H_LEVEL.get(hItem.level, 0) + iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) hCount[iLevel] += 1 return hCount @@ -502,11 +498,6 @@ class NWIndex: """ return self._itemIndex.mainItemHeader(tHandle) - def getHandleHeaderIntLevel(self, tHandle): - """Get the integer header level of the first header of a handle. - """ - return H_LEVEL.get(self._itemIndex.mainItemHeader(tHandle), 0) - def getTableOfContents(self, maxDepth, skipExcl=True): """Generate a table of contents up to a maximum depth. """ @@ -515,7 +506,7 @@ class NWIndex: pKey = None for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): tKey = f"{tHandle}:{sTitle}" - iLevel = H_LEVEL.get(hItem.level, 0) + iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if iLevel > maxDepth: if pKey in tData: tData[pKey]["words"] += hItem.wordCount @@ -665,7 +656,7 @@ class TagsIndex: """ if tagKey in self._tags: return self._tags.get(tagKey).get("heading") - return TT_NONE + return nwHeaders.TT_NONE def tagClass(self, tagKey): """Get the class of a given tag. @@ -911,7 +902,7 @@ class IndexItem: self._index = 0 # Add a placeholder heading - self._headings[TT_NONE] = IndexHeading(TT_NONE) + self._headings[nwHeaders.TT_NONE] = IndexHeading(nwHeaders.TT_NONE) return @@ -945,8 +936,8 @@ class IndexItem: """Add a heading to the item. Also remove the placeholder entry if it exists. """ - if TT_NONE in self._headings: - self._headings.pop(TT_NONE) + if nwHeaders.TT_NONE in self._headings: + self._headings.pop(nwHeaders.TT_NONE) self._headings[tHeading.key] = tHeading return @@ -1115,7 +1106,7 @@ class IndexHeading: def setLevel(self, level): """Set the level of the header if it's a valid value. """ - if level in H_VALID: + if level in nwHeaders.H_VALID: self._level = level return diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index 144ba4ac..d64970ae 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -28,6 +28,8 @@ import os import json import logging +from enum import Enum + from novelwriter.error import logException from novelwriter.common import checkBool, checkFloat, checkInt, checkString from novelwriter.constants import nwFiles @@ -56,7 +58,8 @@ VALID_MAP = { "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2", "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble" }, - "GuiWordList": {"winWidth", "winHeight"} + "GuiWordList": {"winWidth", "winHeight"}, + "GuiNovelView": {"lastCol"}, } @@ -137,7 +140,10 @@ class OptionState(): if group not in self._theState: self._theState[group] = {} - self._theState[group][name] = value + if isinstance(value, Enum): + self._theState[group][name] = value.name + else: + self._theState[group][name] = value return True @@ -186,4 +192,16 @@ class OptionState(): return checkBool(self._theState[group].get(name, default), default) return default + def getEnum(self, group, name, lookup, default): + """Return the value mapped to an enum. Otherwise return the + default value + """ + if issubclass(lookup, Enum): + if group in self._theState: + if name in self._theState[group]: + value = self._theState[group][name] + if value in lookup.__members__: + return lookup[value] + return default + # END Class OptionState diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 460fb91f..fb81f033 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -45,7 +45,7 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.error import logException from novelwriter.common import ( checkString, checkBool, checkInt, isHandle, formatTimeStamp, - makeFileNameSafe, hexToInt, simplified + makeFileNameSafe, hexToInt, minmax, simplified ) from novelwriter.constants import trConst, nwFiles, nwLabels @@ -56,11 +56,11 @@ class NWProject(): FILE_VERSION = "1.4" # The current project file format version - def __init__(self, theParent): + def __init__(self, mainGui): # Internal - self.theParent = theParent - self.mainConf = novelwriter.CONFIG + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui # Core Elements self._optState = OptionState(self) # Project-specific GUI options @@ -102,6 +102,8 @@ class NWProject(): self.importItems = None # Note file importance values self.lastEdited = None # The handle of the last file to be edited self.lastViewed = None # The handle of the last file to be viewed + self.lastNovel = None # The handle of the last novel root viewed + self.lastOutline = None # The handle of the last outline root viewed self.lastWCount = 0 # The project word count from last session self.lastNovelWC = 0 # The novel files word count from last session self.lastNotesWC = 0 # The note files word count from last session @@ -154,6 +156,8 @@ class NWProject(): def newFolder(self, label, pHandle): """Add a new folder with a given label and parent item. """ + if pHandle not in self._projTree: + return None newItem = NWItem(self) newItem.setName(label) newItem.setType(nwItemType.FOLDER) @@ -164,6 +168,8 @@ class NWProject(): def newFile(self, label, pHandle): """Add a new file with a given label and parent item. """ + if pHandle not in self._projTree: + return None newItem = NWItem(self) newItem.setName(label) newItem.setType(nwItemType.FILE) @@ -171,6 +177,32 @@ class NWProject(): self._projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle + def writeNewFile(self, tHandle, hLevel, isDocument): + """Write content to a new document after it is created. This + will not run if the file exists and is not empty. + """ + tItem = self._projTree[tHandle] + if tItem is None: + return False + if tItem.itemType != nwItemType.FILE: + return False + + newDoc = NWDoc(self, tHandle) + if newDoc.readDocument().strip(): + return False + + hshText = "#"*minmax(hLevel, 1, 4) + newText = f"{hshText} {tItem.itemName}\n\n" + if tItem.isNovelLike() and isDocument: + tItem.setLayout(nwItemLayout.DOCUMENT) + else: + tItem.setLayout(nwItemLayout.NOTE) + + newDoc.writeDocument(newText) + self._projIndex.scanText(tHandle, newText) + + return True + def trashFolder(self): """Add the special trash root folder to the project. """ @@ -384,7 +416,7 @@ class NWProject(): if not os.path.isfile(fileName): fileName = os.path.join(fileName, nwFiles.PROJ_FILE) if not os.path.isfile(fileName): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "File not found: {0}" ).format(fileName), nwAlert.ERROR) return False @@ -408,7 +440,7 @@ class NWProject(): legacyList = [] # Cleanup is done later for projItem in os.listdir(self.projPath): logger.verbose("Project contains: %s", projItem) - if projItem.startswith("data_"): + if projItem.startswith("data_") and len(projItem) == 6: legacyList.append(projItem) # Project Lock @@ -435,20 +467,20 @@ class NWProject(): try: nwXML = etree.parse(fileName) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to parse project xml." ), nwAlert.ERROR, exception=exc) # Trying to open backup file instead backFile = fileName[:-3]+"bak" if os.path.isfile(backFile): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Attempting to open backup project file instead." ), nwAlert.INFO) try: nwXML = etree.parse(backFile) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to parse project xml." ), nwAlert.ERROR, exception=exc) self.clearProject() @@ -470,8 +502,8 @@ class NWProject(): # Check File Type # =============== - if not nwxRoot == "novelWriterXML": - self.theParent.makeAlert(self.tr( + if nwxRoot != "novelWriterXML": + self.mainGui.makeAlert(self.tr( "Project file does not appear to be a novelWriterXML file." ), nwAlert.ERROR) self.clearProject() @@ -497,7 +529,7 @@ class NWProject(): # stored and handled. Introduced in version 1.7. if fileVersion not in ("1.0", "1.1", "1.2", "1.3", "1.4"): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Unknown or unsupported novelWriter project file format. " "The project cannot be opened by this version of novelWriter. " "The file was saved with novelWriter version {0}." @@ -506,7 +538,7 @@ class NWProject(): return False if fileVersion != self.FILE_VERSION: - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("File Version"), self.tr( "The file format of your project is about to be updated. " @@ -522,7 +554,7 @@ class NWProject(): # ========================= if hexToInt(hexVersion) > hexToInt(novelwriter.__hexversion__): - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("Version Conflict"), self.tr( "This project was saved by a newer version of " @@ -582,6 +614,10 @@ class NWProject(): self.lastEdited = checkString(xItem.text, None, True) elif xItem.tag == "lastViewed": self.lastViewed = checkString(xItem.text, None, True) + elif xItem.tag == "lastNovel": + self.lastNovel = checkString(xItem.text, None, True) + elif xItem.tag == "lastOutline": + self.lastOutline = checkString(xItem.text, None, True) elif xItem.tag == "lastWordCount": self.lastWCount = checkInt(xItem.text, 0, False) elif xItem.tag == "novelWordCount": @@ -612,11 +648,14 @@ class NWProject(): # Sort out old file locations if legacyList: - errList = [] - for projItem in legacyList: - errList = self._legacyDataFolder(projItem, errList) - if errList: - self.theParent.makeAlert(errList, nwAlert.ERROR) + try: + for projItem in legacyList: + self._legacyDataFolder(projItem) + except Exception: + self.mainGui.makeAlert(self.tr( + "There was an error updating the project. " + "Some data may not have been preserved." + ), nwAlert.ERROR) # Clean up no longer used files self._deprecatedFiles() @@ -642,7 +681,7 @@ class NWProject(): self._writeLockFile() self.setProjectChanged(False) - self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName)) + self.mainGui.setStatus(self.tr("Opened Project: {0}").format(self.projName)) return True @@ -653,7 +692,7 @@ class NWProject(): file. """ if self.projPath is None: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Project path not set, cannot save project." ), nwAlert.ERROR) return False @@ -699,6 +738,8 @@ class NWProject(): self._packProjectValue(xSettings, "autoOutline", self.autoOutline) self._packProjectValue(xSettings, "lastEdited", self.lastEdited) self._packProjectValue(xSettings, "lastViewed", self.lastViewed) + self._packProjectValue(xSettings, "lastNovel", self.lastNovel) + self._packProjectValue(xSettings, "lastOutline", self.lastOutline) self._packProjectValue(xSettings, "lastWordCount", self.currWCount) self._packProjectValue(xSettings, "novelWordCount", self.currNovelWC) self._packProjectValue(xSettings, "notesWordCount", self.currNotesWC) @@ -733,7 +774,7 @@ class NWProject(): xml_declaration=True )) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to save project." ), nwAlert.ERROR, exception=exc) return False @@ -745,7 +786,7 @@ class NWProject(): os.replace(saveFile, backFile) os.replace(tempFile, saveFile) except OSError as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to save project." ), nwAlert.ERROR, exception=exc) return False @@ -758,7 +799,7 @@ class NWProject(): self.mainConf.saveRecentCache() self._writeLockFile() - self.theParent.setStatus(self.tr("Saved Project: {0}").format(self.projName)) + self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self.projName)) self.setProjectChanged(False) return True @@ -806,22 +847,22 @@ class NWProject(): def zipIt(self, doNotify): """Create a zip file of the entire project. """ - if not self.theParent.hasProject: + if not self.mainGui.hasProject: logger.error("No project open") return False logger.info("Backing up project") - self.theParent.setStatus(self.tr("Backing up project ...")) + self.mainGui.setStatus(self.tr("Backing up project ...")) if not (self.mainConf.backupPath and os.path.isdir(self.mainConf.backupPath)): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot backup project because no valid backup path is set. " "Please set a valid backup location in Preferences." ), nwAlert.ERROR) return False if not self.projName: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot backup project because no project name is set. " "Please set a Working Title in Project Settings." ), nwAlert.ERROR) @@ -834,13 +875,13 @@ class NWProject(): os.mkdir(baseDir) logger.debug("Created folder: %s", baseDir) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Could not create backup folder." ), nwAlert.ERROR, exception=exc) return False if baseDir and baseDir.startswith(self.projPath): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot backup project because the backup path is within the " "project folder to be backed up. Please choose a different " "backup path in Preferences." @@ -856,17 +897,17 @@ class NWProject(): self._writeLockFile() logger.info("Backup written to: %s", archName) if doNotify: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Backup archive file written to: {0}" ).format(f"{os.path.join(cleanName, archName)}.zip"), nwAlert.INFO) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Could not write backup archive." ), nwAlert.ERROR, exception=exc) return False - self.theParent.setStatus(self.tr( + self.mainGui.setStatus(self.tr( "Project backed up to '{0}'" ).format(f"{baseName}.zip")) @@ -894,7 +935,7 @@ class NWProject(): shutil.unpack_archive(pkgSample, projPath) isSuccess = True except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to create a new example project." ), nwAlert.ERROR, exception=exc) @@ -916,12 +957,12 @@ class NWProject(): isSuccess = True except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to create a new example project." ), nwAlert.ERROR, exception=exc) else: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to create a new example project. " "Could not find the necessary files. " "They seem to be missing from this installation." @@ -929,8 +970,8 @@ class NWProject(): if isSuccess: self.clearProject() - self.theParent.openProject(projPath) - self.theParent.rebuildIndex() + self.mainGui.openProject(projPath) + self.mainGui.rebuildIndex() return isSuccess @@ -955,14 +996,14 @@ class NWProject(): os.mkdir(projPath) logger.debug("Created folder: %s", projPath) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Could not create new project folder." ), nwAlert.ERROR, exception=exc) return False if os.path.isdir(projPath): if os.listdir(self.projPath): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "New project folder is not empty. " "Each project requires a dedicated project folder." ), nwAlert.ERROR) @@ -1012,14 +1053,14 @@ class NWProject(): self.doBackup = doBackup if doBackup: if not os.path.isdir(self.mainConf.backupPath): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "You must set a valid backup path in Preferences to use " "the automatic project backup feature." ), nwAlert.WARN) return False if self.projName == "": - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "You must set a valid project name in Project Settings to " "use the automatic project backup feature." ), nwAlert.WARN) @@ -1090,6 +1131,14 @@ class NWProject(): self.setProjectChanged(True) return True + def setLastNovelViewed(self, tHandle): + """Set last viewed novel root in the novel tree. + """ + if self.lastNovel != tHandle: + self.lastNovel = tHandle + self.setProjectChanged(True) + return True + def setStatusColours(self, newCols, delCols): """Update the list of novel file status flags. """ @@ -1124,7 +1173,7 @@ class NWProject(): information to the GUI statusbar. """ self.projChanged = bValue - self.theParent.statusBar.doUpdateProjectStatus(bValue) + self.mainGui.statusBar.doUpdateProjectStatus(bValue) if bValue: # If we've changed the project at all, this should be True self.projAltered = True @@ -1354,7 +1403,7 @@ class NWProject(): os.mkdir(thePath) logger.debug("Created folder: %s", thePath) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Could not create folder." ), nwAlert.ERROR, exception=exc) return False @@ -1417,7 +1466,7 @@ class NWProject(): # Report status if len(orphanFiles) > 0: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Found {0} orphaned file(s) in project folder." ).format(len(orphanFiles)), nwAlert.WARN) else: @@ -1474,7 +1523,7 @@ class NWProject(): self._projTree.updateItemData(orphItem.itemHandle) if noWhere: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "One or more orphaned files could not be added back into the project. " "Make sure at least a Novel root folder exists." ), nwAlert.WARN) @@ -1528,82 +1577,37 @@ class NWProject(): # Legacy Data Structure Handlers ## - def _legacyDataFolder(self, theFolder, errList): + def _legacyDataFolder(self, dataDir): """Clean up legacy data folders. """ - theData = os.path.join(self.projPath, theFolder) - if not os.path.isdir(theData): - errList.append(self.tr("Not a folder: {0}").format(theData)) - return errList + dataPath = os.path.join(self.projPath, dataDir) + if not os.path.isdir(dataPath): + return False - logger.info("Old data folder %s found", theFolder) + logger.info("Old data folder found: %s", dataDir) # Move Documents to Content - for dataItem in os.listdir(theData): - theFile = os.path.join(theData, dataItem) - if not os.path.isfile(theFile): - theErr = self._moveUnknownItem(theData, dataItem) - if theErr: - errList.append(theErr) + for dataItem in os.listdir(dataPath): + dataFile = os.path.join(dataPath, dataItem) + if not os.path.isfile(dataFile): continue if len(dataItem) == 21 and dataItem.endswith("_main.nwd"): - tHandle = theFolder[-1]+dataItem[:12] - newPath = os.path.join(self.projContent, tHandle+".nwd") - try: - os.rename(theFile, newPath) - logger.info("Moved file: %s", theFile) - logger.info("New location: %s", newPath) - except Exception: - errList.append(self.tr("Could not move: {0}").format(theFile)) - logger.error("Could not move: %s", theFile) - logException() + tHandle = dataDir[-1] + dataItem[:12] + newPath = os.path.join(self.projContent, f"{tHandle}.nwd") + os.rename(dataFile, newPath) + logger.info("Moved file: %s", dataFile) elif len(dataItem) == 21 and dataItem.endswith("_main.bak"): - try: - os.unlink(theFile) - logger.info("Deleted file: %s", theFile) - except Exception: - errList.append(self.tr("Could not delete: {0}").format(theFile)) - logger.error("Could not delete: %s", theFile) - logException() - - else: - theErr = self._moveUnknownItem(theData, dataItem) - if theErr: - errList.append(theErr) + os.unlink(dataFile) + logger.info("Deleted file: %s", dataFile) # Remove Data Folder - try: - os.rmdir(theData) - logger.info("Deleted folder: %s", theFolder) - except Exception: - errList.append(self.tr("Could not delete: {0}").format(theFolder)) - logger.error("Could not delete: %s", theFolder) - logException() + if not os.listdir(dataPath): + os.rmdir(dataPath) + logger.info("Deleted folder: %s", dataDir) - return errList - - def _moveUnknownItem(self, theDir, theItem): - """Move an item that doesn't belong in the project folder to - a junk folder. - """ - theJunk = os.path.join(self.projPath, "junk") - if not self._checkFolder(theJunk): - return self.tr("Could not make folder: {0}").format(theJunk) - - theSrc = os.path.join(theDir, theItem) - theDst = os.path.join(theJunk, theItem) - - try: - os.rename(theSrc, theDst) - logger.info("Moved to junk: %s", theSrc) - except Exception: - logger.error("Could not move item %s to junk", theSrc) - logException() - return self.tr("Could not move item {0} to {1}.").format(theSrc, theJunk) - - return "" + return True def _deprecatedFiles(self): """Delete files that are no longer used by novelWriter. diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index bb5f3d8e..3a3b52f1 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -82,7 +82,6 @@ class Tokenizer(ABC): def __init__(self, theProject): self.theProject = theProject - self.theParent = theProject.theParent self.mainConf = novelwriter.CONFIG # Data Variables diff --git a/novelwriter/dialogs/__init__.py b/novelwriter/dialogs/__init__.py index 30bd5d82..03efef5c 100644 --- a/novelwriter/dialogs/__init__.py +++ b/novelwriter/dialogs/__init__.py @@ -22,7 +22,7 @@ along with this program. If not, see . from novelwriter.dialogs.about import GuiAbout from novelwriter.dialogs.docmerge import GuiDocMerge from novelwriter.dialogs.docsplit import GuiDocSplit -from novelwriter.dialogs.itemeditor import GuiItemEditor +from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.projdetails import GuiProjectDetails from novelwriter.dialogs.projload import GuiProjectLoad @@ -35,7 +35,7 @@ __all__ = [ "GuiAbout", "GuiDocMerge", "GuiDocSplit", - "GuiItemEditor", + "GuiEditLabel", "GuiPreferences", "GuiProjectDetails", "GuiProjectLoad", diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index 2bb26b97..c1a9ec22 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -43,15 +43,15 @@ logger = logging.getLogger(__name__) class GuiAbout(QDialog): - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiAbout ...") self.setObjectName("GuiAbout") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme self.outerBox = QVBoxLayout() self.innerBox = QHBoxLayout() @@ -63,7 +63,7 @@ class GuiAbout(QDialog): nPx = self.mainConf.pxInt(96) self.nwIcon = QLabel() - self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx))) + self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx))) self.lblName = QLabel("novelWriter") self.lblVers = QLabel(f"v{novelwriter.__version__}") self.lblDate = QLabel(datetime.strptime(novelwriter.__date__, "%Y-%m-%d").strftime("%x")) @@ -192,37 +192,37 @@ class GuiAbout(QDialog): ]) ) - theTheme = self.theParent.theTheme - theIcons = self.theParent.theTheme.theIcons - if theTheme.themeName and theTheme.themeAuthor != "N/A": - licURL = f"{theTheme.themeLicense}" + mainTheme = self.mainGui.mainTheme + iconCache = self.mainGui.mainTheme.iconCache + if mainTheme.themeName and mainTheme.themeAuthor != "N/A": + licURL = f"{mainTheme.themeLicense}" aboutMsg += "

{0}

{1}

".format( - self.tr("Theme: {0}").format(theTheme.themeName), + self.tr("Theme: {0}").format(mainTheme.themeName), self._wrapTable([ - (self.tr("Author"), theTheme.themeAuthor), - (self.tr("Credit"), theTheme.themeCredit), + (self.tr("Author"), mainTheme.themeAuthor), + (self.tr("Credit"), mainTheme.themeCredit), (self.tr("Licence"), licURL), ]) ) - if theIcons.themeName: - licURL = f"{theIcons.themeLicense}" + if iconCache.themeName: + licURL = f"{iconCache.themeLicense}" aboutMsg += "

{0}

{1}

".format( - self.tr("Icons: {0}").format(theIcons.themeName), + self.tr("Icons: {0}").format(iconCache.themeName), self._wrapTable([ - (self.tr("Author"), theIcons.themeAuthor), - (self.tr("Credit"), theIcons.themeCredit), + (self.tr("Author"), iconCache.themeAuthor), + (self.tr("Credit"), iconCache.themeCredit), (self.tr("Licence"), licURL), ]) ) - if theTheme.syntaxName: - licURL = f"{theTheme.syntaxLicense}" + if mainTheme.syntaxName: + licURL = f"{mainTheme.syntaxLicense}" aboutMsg += "

{0}

{1}

".format( - self.tr("Syntax: {0}").format(theTheme.syntaxName), + self.tr("Syntax: {0}").format(mainTheme.syntaxName), self._wrapTable([ - (self.tr("Author"), theTheme.syntaxAuthor), - (self.tr("Credit"), theTheme.syntaxCredit), + (self.tr("Author"), mainTheme.syntaxAuthor), + (self.tr("Credit"), mainTheme.syntaxCredit), (self.tr("Licence"), licURL), ]) ) @@ -280,12 +280,12 @@ class GuiAbout(QDialog): " padding-right: 0.8em;" "}}\n" ).format( - hColR=self.theParent.theTheme.colHead[0], - hColG=self.theParent.theTheme.colHead[1], - hColB=self.theParent.theTheme.colHead[2], - kColR=self.theTheme.colKey[0], - kColG=self.theTheme.colKey[1], - kColB=self.theTheme.colKey[2], + hColR=self.mainGui.mainTheme.colHead[0], + hColG=self.mainGui.mainTheme.colHead[1], + hColB=self.mainGui.mainTheme.colHead[2], + kColR=self.mainTheme.colKey[0], + kColG=self.mainTheme.colKey[1], + kColB=self.mainTheme.colKey[2], ) self.pageAbout.document().setDefaultStyleSheet(styleSheet) self.pageNotes.document().setDefaultStyleSheet(styleSheet) diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 033f3698..30eb602f 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -41,15 +41,15 @@ logger = logging.getLogger(__name__) class GuiDocMerge(QDialog): - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiDocMerge ...") self.setObjectName("GuiDocMerge") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theProject = mainGui.theProject self.sourceItem = None self.outerBox = QVBoxLayout() @@ -57,7 +57,7 @@ class GuiDocMerge(QDialog): self.headLabel = QLabel("{0}".format(self.tr("Documents to Merge"))) self.helpLabel = QHelpLabel( - self.tr("Drag and drop items to change the order."), self.theParent.theTheme.helpText + self.tr("Drag and drop items to change the order."), self.mainGui.mainTheme.helpText ) self.listBox = QListWidget() @@ -102,7 +102,7 @@ class GuiDocMerge(QDialog): finalOrder.append(self.listBox.item(i).data(Qt.UserRole)) if len(finalOrder) == 0: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "No source documents found. Nothing to do." ), nwAlert.ERROR) return False @@ -113,21 +113,21 @@ class GuiDocMerge(QDialog): docText = inDoc.readDocument() docErr = inDoc.getError() if docText is None and docErr: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Failed to open document file."), docErr ], nwAlert.ERROR) if docText: theText += docText.rstrip("\n")+"\n\n" if self.sourceItem is None: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "No source folder selected. Nothing to do." ), nwAlert.ERROR) return False srcItem = self.theProject.tree[self.sourceItem] if srcItem is None: - self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR) + self.mainGui.makeAlert(self.tr("Internal error."), nwAlert.ERROR) return False nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent) @@ -137,13 +137,13 @@ class GuiDocMerge(QDialog): outDoc = NWDoc(self.theProject, nHandle) if not outDoc.writeDocument(theText): - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Could not save document."), outDoc.getError() ], nwAlert.ERROR) return False - self.theParent.treeView.revealNewTreeItem(nHandle) - self.theParent.openDocument(nHandle, doScroll=True) + self.mainGui.projView.revealNewTreeItem(nHandle) + self.mainGui.openDocument(nHandle, doScroll=True) self._doClose() @@ -165,7 +165,7 @@ class GuiDocMerge(QDialog): are then added to the list view in order. The list itself can be reordered by the user. """ - tHandle = self.theParent.treeView.getSelectedHandle() + tHandle = self.mainGui.projView.getSelectedHandle() self.sourceItem = tHandle if tHandle is None: return False @@ -175,12 +175,12 @@ class GuiDocMerge(QDialog): return False if nwItem.itemType is not nwItemType.FOLDER: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Element selected in the project tree must be a folder." ), nwAlert.ERROR) return False - for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle): + for sHandle in self.mainGui.projView.getTreeFromHandle(tHandle): newItem = QListWidgetItem() nwItem = self.theProject.tree[sHandle] if nwItem.itemType is not nwItemType.FILE: diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 76e64d5f..b507479e 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -41,15 +41,15 @@ logger = logging.getLogger(__name__) class GuiDocSplit(QDialog): - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiDocSplit ...") self.setObjectName("GuiDocSplit") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theProject = mainGui.theProject self.sourceItem = None self.sourceText = [] @@ -60,7 +60,7 @@ class GuiDocSplit(QDialog): self.headLabel = QLabel("{0}".format(self.tr("Document Headers"))) self.helpLabel = QHelpLabel( self.tr("Select the maximum level to split into files."), - self.theParent.theTheme.helpText + self.mainGui.mainTheme.helpText ) self.listBox = QListWidget() @@ -115,14 +115,14 @@ class GuiDocSplit(QDialog): logger.verbose("GuiDocSplit split button clicked") if self.sourceItem is None: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "No source document selected. Nothing to do." ), nwAlert.ERROR) return False srcItem = self.theProject.tree[self.sourceItem] if srcItem is None: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Could not parse source document." ), nwAlert.ERROR) return False @@ -132,7 +132,7 @@ class GuiDocSplit(QDialog): docErr = inDoc.getError() if theText is None and docErr: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Failed to open document file."), docErr ], nwAlert.ERROR) @@ -153,12 +153,12 @@ class GuiDocSplit(QDialog): nFiles = len(finalOrder) if nFiles == 0: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "No headers found. Nothing to do." ), nwAlert.ERROR) return False - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("Split Document"), "{0}

{1}".format( self.tr( @@ -175,7 +175,7 @@ class GuiDocSplit(QDialog): # Create the folder fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent) - self.theParent.treeView.revealNewTreeItem(fHandle) + self.mainGui.projView.revealNewTreeItem(fHandle) logger.verbose("Creating folder '%s'", fHandle) # Loop through, and create the files @@ -196,12 +196,12 @@ class GuiDocSplit(QDialog): outDoc = NWDoc(self.theProject, nHandle) if not outDoc.writeDocument(theText): - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Could not save document."), outDoc.getError() ], nwAlert.ERROR) return False - self.theParent.treeView.revealNewTreeItem(nHandle) + self.mainGui.projView.revealNewTreeItem(nHandle) self._doClose() @@ -226,7 +226,7 @@ class GuiDocSplit(QDialog): """ self.listBox.clear() if self.sourceItem is None: - self.sourceItem = self.theParent.treeView.getSelectedHandle() + self.sourceItem = self.mainGui.projView.getSelectedHandle() if self.sourceItem is None: return False @@ -236,7 +236,7 @@ class GuiDocSplit(QDialog): return False if nwItem.itemType is not nwItemType.FILE: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Element selected in the project tree must be a file." ), nwAlert.ERROR) return False diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py new file mode 100644 index 00000000..03bf9d08 --- /dev/null +++ b/novelwriter/dialogs/editlabel.py @@ -0,0 +1,84 @@ +""" +novelWriter – Edit Label Dialog +=============================== +A simple dialog for editing a label + +File History: +Created: 2022-06-11 [1.7b1] + +This file is a part of novelWriter +Copyright 2018–2022, 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 logging +import novelwriter + +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QLineEdit, QLabel, QDialogButtonBox, QHBoxLayout +) + +logger = logging.getLogger(__name__) + + +class GuiEditLabel(QDialog): + + def __init__(self, parent, text=""): + QDialog.__init__(self, parent=parent) + + self.setObjectName("GuiEditLabel") + self.setWindowTitle(self.tr("Item Label")) + + mVd = novelwriter.CONFIG.pxInt(220) + mSp = novelwriter.CONFIG.pxInt(12) + + # Item Label + self.labelValue = QLineEdit() + self.labelValue.setMinimumWidth(mVd) + self.labelValue.setMaxLength(200) + self.labelValue.setText(text) + self.labelValue.selectAll() + + # Buttons + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + # Assemble + self.innerBox = QHBoxLayout() + self.innerBox.addWidget(QLabel(self.tr("Label")), 0) + self.innerBox.addWidget(self.labelValue, 1) + self.innerBox.setSpacing(mSp) + + self.outerBox = QVBoxLayout() + self.outerBox.setSpacing(mSp) + self.outerBox.addLayout(self.innerBox, 1) + self.outerBox.addWidget(self.buttonBox, 0) + + self.setLayout(self.outerBox) + + return + + @property + def itemLabel(self): + return self.labelValue.text() + + @classmethod + def getLabel(cls, parent, text): + cls = GuiEditLabel(parent, text=text) + cls.exec_() + return cls.itemLabel, cls.result() == QDialog.Accepted + +# END Class GuiEditLabel diff --git a/novelwriter/dialogs/itemeditor.py b/novelwriter/dialogs/itemeditor.py deleted file mode 100644 index acf07134..00000000 --- a/novelwriter/dialogs/itemeditor.py +++ /dev/null @@ -1,203 +0,0 @@ -""" -novelWriter – GUI Item Editor -============================= -GUI class for the item editor dialog - -File History: -Created: 2019-04-27 [0.0.1] - -This file is a part of novelWriter -Copyright 2018–2022, 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 logging -import novelwriter - -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QGridLayout, QLineEdit, QComboBox, QLabel, - QDialogButtonBox -) - -from novelwriter.enum import nwItemLayout, nwItemType -from novelwriter.constants import trConst, nwLabels -from novelwriter.gui.custom import QSwitch - -logger = logging.getLogger(__name__) - - -class GuiItemEditor(QDialog): - - def __init__(self, theParent, tHandle): - QDialog.__init__(self, theParent) - - logger.debug("Initialising GuiItemEditor ...") - self.setObjectName("GuiItemEditor") - - self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject - - ## - # Build GUI - ## - - self.theItem = self.theProject.tree[tHandle] - if self.theItem is None: - self.close() - return - - self.setWindowTitle(self.tr("Item Settings")) - - mVd = self.mainConf.pxInt(220) - mSp = self.mainConf.pxInt(16) - vSp = self.mainConf.pxInt(4) - - # Item Label - self.editName = QLineEdit() - self.editName.setMinimumWidth(mVd) - self.editName.setMaxLength(200) - - # Item Status - self.editStatus = QComboBox() - self.editStatus.setMinimumWidth(mVd) - if self.theItem.isNovelLike(): - for key, entry in self.theProject.statusItems.items(): - self.editStatus.addItem(entry["icon"], entry["name"], key) - - index = self.editStatus.findData(self.theItem.itemStatus) - if index != -1: - self.editStatus.setCurrentIndex(index) - - else: - for key, entry in self.theProject.importItems.items(): - self.editStatus.addItem(entry["icon"], entry["name"], key) - - index = self.editStatus.findData(self.theItem.itemImport) - if index != -1: - self.editStatus.setCurrentIndex(index) - - # Item Layout - self.editLayout = QComboBox() - self.editLayout.setMinimumWidth(mVd) - validLayouts = [] - if self.theItem.itemType == nwItemType.FILE: - if self.theItem.documentAllowed(): - validLayouts.append(nwItemLayout.DOCUMENT) - validLayouts.append(nwItemLayout.NOTE) - else: - validLayouts.append(nwItemLayout.NO_LAYOUT) - self.editLayout.setEnabled(False) - - for itemLayout in nwItemLayout: - if itemLayout in validLayouts: - self.editLayout.addItem(trConst(nwLabels.LAYOUT_NAME[itemLayout]), itemLayout) - - index = self.editLayout.findData(self.theItem.itemLayout) - if index != -1: - self.editLayout.setCurrentIndex(index) - - # Export Switch - self.textExport = QLabel(self.tr("Include when building project")) - self.editExport = QSwitch() - if self.theItem.itemType == nwItemType.FILE: - self.editExport.setEnabled(True) - self.editExport.setChecked(self.theItem.isExported) - else: - self.editExport.setEnabled(False) - self.editExport.setChecked(False) - - # Buttons - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - self.buttonBox.accepted.connect(self._doSave) - self.buttonBox.rejected.connect(self._doClose) - - # Set Current Values - self.editName.setText(self.theItem.itemName) - self.editName.selectAll() - - ## - # Assemble - ## - - nameLabel = QLabel(self.tr("Label")) - statusLabel = QLabel(self.tr("Status")) - layoutLabel = QLabel(self.tr("Layout")) - - self.mainForm = QGridLayout() - self.mainForm.setVerticalSpacing(vSp) - self.mainForm.setHorizontalSpacing(mSp) - self.mainForm.addWidget(nameLabel, 0, 0, 1, 1) - self.mainForm.addWidget(self.editName, 0, 1, 1, 2) - self.mainForm.addWidget(statusLabel, 1, 0, 1, 1) - self.mainForm.addWidget(self.editStatus, 1, 1, 1, 2) - self.mainForm.addWidget(layoutLabel, 2, 0, 1, 1) - self.mainForm.addWidget(self.editLayout, 2, 1, 1, 2) - self.mainForm.addWidget(self.textExport, 3, 0, 1, 2) - self.mainForm.addWidget(self.editExport, 3, 2, 1, 1) - self.mainForm.setColumnStretch(0, 0) - self.mainForm.setColumnStretch(1, 1) - self.mainForm.setColumnStretch(2, 0) - - self.outerBox = QVBoxLayout() - self.outerBox.setSpacing(mSp) - self.outerBox.addLayout(self.mainForm) - self.outerBox.addStretch(1) - self.outerBox.addWidget(self.buttonBox) - self.setLayout(self.outerBox) - - self.rejected.connect(self._doClose) - - logger.debug("GuiItemEditor initialisation complete") - - return - - ## - # Slots - ## - - @pyqtSlot() - def _doSave(self): - """Save the setting to the item. - """ - logger.verbose("ItemEditor save button clicked") - - itemName = self.editName.text() - itemStatus = self.editStatus.currentData() - itemLayout = self.editLayout.currentData() - isExported = self.editExport.isChecked() - - self.theItem.setName(itemName) - self.theItem.setImportStatus(itemStatus) - self.theItem.setLayout(itemLayout) - self.theItem.setExported(isExported) - - self.theProject.setProjectChanged(True) - - self.accept() - self.close() - - return - - @pyqtSlot() - def _doClose(self): - """Close the dialog without saving the settings. - """ - logger.verbose("ItemEditor cancel button clicked") - self.close() - return - -# END Class GuiItemEditor diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index d38a4ab6..f807fde1 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -43,25 +43,25 @@ logger = logging.getLogger(__name__) class GuiPreferences(PagedDialog): - def __init__(self, theParent): - PagedDialog.__init__(self, theParent) + def __init__(self, mainGui): + PagedDialog.__init__(self, mainGui) logger.debug("Initialising GuiPreferences ...") self.setObjectName("GuiPreferences") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theProject = mainGui.theProject self.setWindowTitle(self.tr("Preferences")) - self.tabGeneral = GuiPreferencesGeneral(self.theParent) - self.tabProjects = GuiPreferencesProjects(self.theParent) - self.tabDocs = GuiPreferencesDocuments(self.theParent) - self.tabEditor = GuiPreferencesEditor(self.theParent) - self.tabSyntax = GuiPreferencesSyntax(self.theParent) - self.tabAuto = GuiPreferencesAutomation(self.theParent) - self.tabQuote = GuiPreferencesQuotes(self.theParent) + self.tabGeneral = GuiPreferencesGeneral(self.mainGui) + self.tabProjects = GuiPreferencesProjects(self.mainGui) + self.tabDocs = GuiPreferencesDocuments(self.mainGui) + self.tabEditor = GuiPreferencesEditor(self.mainGui) + self.tabSyntax = GuiPreferencesSyntax(self.mainGui) + self.tabAuto = GuiPreferencesAutomation(self.mainGui) + self.tabQuote = GuiPreferencesQuotes(self.mainGui) self.addTab(self.tabGeneral, self.tr("General")) self.addTab(self.tabProjects, self.tr("Projects")) @@ -102,12 +102,12 @@ class GuiPreferences(PagedDialog): self.tabQuote.saveValues() if needsRestart: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Some changes will not be applied until novelWriter has been restarted." ), nwAlert.INFO) if refreshTree: - self.theParent.treeView.buildTree() + self.mainGui.projView.populateTree() self._saveWindowSize() self.accept() @@ -138,16 +138,16 @@ class GuiPreferences(PagedDialog): class GuiPreferencesGeneral(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) # Look and Feel @@ -174,7 +174,7 @@ class GuiPreferencesGeneral(QWidget): # Select Theme self.guiTheme = QComboBox() self.guiTheme.setMinimumWidth(minWidth) - self.theThemes = self.theTheme.listThemes() + self.theThemes = self.mainTheme.listThemes() for themeDir, themeName in self.theThemes: self.guiTheme.addItem(themeName, themeDir) themeIdx = self.guiTheme.findData(self.mainConf.guiTheme) @@ -190,8 +190,8 @@ class GuiPreferencesGeneral(QWidget): # Select Icon Theme self.guiIcons = QComboBox() self.guiIcons.setMinimumWidth(minWidth) - self.theIcons = self.theTheme.theIcons.listThemes() - for iconDir, iconName in self.theIcons: + self.iconCache = self.mainTheme.iconCache.listThemes() + for iconDir, iconName in self.iconCache: self.guiIcons.addItem(iconName, iconDir) iconIdx = self.guiIcons.findData(self.mainConf.guiIcons) if iconIdx != -1: @@ -206,7 +206,7 @@ class GuiPreferencesGeneral(QWidget): # Editor Theme self.guiSyntax = QComboBox() self.guiSyntax.setMinimumWidth(self.mainConf.pxInt(200)) - self.theSyntaxes = self.theTheme.listSyntax() + self.theSyntaxes = self.mainTheme.listSyntax() for syntaxFile, syntaxName in self.theSyntaxes: self.guiSyntax.addItem(syntaxName, syntaxFile) syntaxIdx = self.guiSyntax.findData(self.mainConf.guiSyntax) @@ -225,7 +225,7 @@ class GuiPreferencesGeneral(QWidget): self.guiFont.setFixedWidth(self.mainConf.pxInt(162)) self.guiFont.setText(self.mainConf.guiFont) self.fontButton = QPushButton("...") - self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) + self.fontButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("..."))) self.fontButton.clicked.connect(self._selectFont) self.mainForm.addRow( self.tr("Font family"), @@ -344,16 +344,16 @@ class GuiPreferencesGeneral(QWidget): class GuiPreferencesProjects(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) # Automatic Save @@ -505,16 +505,16 @@ class GuiPreferencesProjects(QWidget): class GuiPreferencesDocuments(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) # Text Style @@ -527,7 +527,7 @@ class GuiPreferencesDocuments(QWidget): self.textFont.setFixedWidth(self.mainConf.pxInt(162)) self.textFont.setText(self.mainConf.textFont) self.fontButton = QPushButton("...") - self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) + self.fontButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("..."))) self.fontButton.clicked.connect(self._selectFont) self.mainForm.addRow( self.tr("Font family"), @@ -666,16 +666,16 @@ class GuiPreferencesDocuments(QWidget): class GuiPreferencesEditor(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) mW = self.mainConf.pxInt(250) @@ -688,7 +688,7 @@ class GuiPreferencesEditor(QWidget): self.spellLanguage = QComboBox(self) self.spellLanguage.setMaximumWidth(mW) - langAvail = self.theParent.docEditor.spEnchant.listDictionaries() + langAvail = self.mainGui.docEditor.spEnchant.listDictionaries() if self.mainConf.hasEnchant: if langAvail: for spTag, spProv in langAvail: @@ -840,16 +840,16 @@ class GuiPreferencesEditor(QWidget): class GuiPreferencesSyntax(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) # Quotes & Dialogue @@ -943,16 +943,16 @@ class GuiPreferencesSyntax(QWidget): class GuiPreferencesAutomation(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) # Automatic Features @@ -1100,16 +1100,16 @@ class GuiPreferencesAutomation(QWidget): class GuiPreferencesQuotes(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainTheme.helpText) self.setLayout(self.mainForm) # Quotation Style @@ -1117,7 +1117,7 @@ class GuiPreferencesQuotes(QWidget): self.mainForm.addGroupLabel(self.tr("Quotation Style")) qWidth = self.mainConf.pxInt(40) - bWidth = int(2.5*self.theTheme.getTextWidth("...")) + bWidth = int(2.5*self.mainTheme.getTextWidth("...")) self.quoteSym = {} # Single Quote Style diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py index 258b82e4..e83fc0a4 100644 --- a/novelwriter/dialogs/projdetails.py +++ b/novelwriter/dialogs/projdetails.py @@ -43,15 +43,15 @@ logger = logging.getLogger(__name__) class GuiProjectDetails(PagedDialog): - def __init__(self, theParent): - PagedDialog.__init__(self, theParent) + def __init__(self, mainGui): + PagedDialog.__init__(self, mainGui) logger.debug("Initialising GuiProjectDetails ...") self.setObjectName("GuiProjectDetails") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theProject = mainGui.theProject self.setWindowTitle(self.tr("Project Details")) @@ -66,8 +66,8 @@ class GuiProjectDetails(PagedDialog): self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "winHeight", wH)) ) - self.tabMain = GuiProjectDetailsMain(self.theParent, self.theProject) - self.tabContents = GuiProjectDetailsContents(self.theParent, self.theProject) + self.tabMain = GuiProjectDetailsMain(self.mainGui, self.theProject) + self.tabContents = GuiProjectDetailsContents(self.mainGui, self.theProject) self.addTab(self.tabMain, self.tr("Overview")) self.addTab(self.tabContents, self.tr("Contents")) @@ -139,16 +139,16 @@ class GuiProjectDetails(PagedDialog): class GuiProjectDetailsMain(QWidget): - def __init__(self, theParent, theProject): - QWidget.__init__(self, theParent) + def __init__(self, mainGui, theProject): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent self.theProject = theProject - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme - fPx = self.theTheme.fontPixelSize - fPt = self.theTheme.fontPointSize + fPx = self.mainTheme.fontPixelSize + fPt = self.mainTheme.fontPointSize vPx = self.mainConf.pxInt(4) hPx = self.mainConf.pxInt(12) @@ -271,18 +271,18 @@ class GuiProjectDetailsContents(QWidget): C_PAGE = 3 C_PROG = 4 - def __init__(self, theParent, theProject): - QWidget.__init__(self, theParent) + def __init__(self, mainGui, theProject): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent self.theProject = theProject - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # Internal self._theToC = [] - iPx = self.theTheme.baseIconSize + iPx = self.mainTheme.baseIconSize hPx = self.mainConf.pxInt(12) vPx = self.mainConf.pxInt(4) pOptions = self.theProject.options @@ -469,7 +469,7 @@ class GuiProjectDetailsContents(QWidget): if tTitle.strip() == "": tTitle = self.tr("Untitled") - newItem.setIcon(self.C_TITLE, self.theTheme.getIcon("doc_h%d" % tLevel)) + newItem.setIcon(self.C_TITLE, self.mainTheme.getIcon("doc_h%d" % tLevel)) newItem.setText(self.C_TITLE, tTitle) newItem.setText(self.C_WORDS, f"{wCount:n}") newItem.setText(self.C_PAGES, f"{pCount:n}") diff --git a/novelwriter/dialogs/projload.py b/novelwriter/dialogs/projload.py index bbae4920..c7229f44 100644 --- a/novelwriter/dialogs/projload.py +++ b/novelwriter/dialogs/projload.py @@ -53,21 +53,21 @@ class GuiProjectLoad(QDialog): C_COUNT = 1 C_TIME = 2 - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiProjectLoad ...") self.setObjectName("GuiProjectLoad") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme self.openState = self.NONE_STATE self.openPath = None sPx = self.mainConf.pxInt(16) nPx = self.mainConf.pxInt(96) - iPx = self.theTheme.baseIconSize + iPx = self.mainTheme.baseIconSize self.outerBox = QVBoxLayout() self.innerBox = QHBoxLayout() @@ -80,7 +80,7 @@ class GuiProjectLoad(QDialog): self.setModal(True) self.nwIcon = QLabel() - self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx))) + self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx))) self.innerBox.addWidget(self.nwIcon, 0, Qt.AlignTop) self.projectForm = QGridLayout() @@ -110,7 +110,7 @@ class GuiProjectLoad(QDialog): self.selPath.setReadOnly(True) self.browseButton = QPushButton("...") - self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) + self.browseButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("..."))) self.browseButton.clicked.connect(self._doBrowse) self.projectForm.addWidget(self.lblRecent, 0, 0, 1, 3) @@ -225,7 +225,7 @@ class GuiProjectLoad(QDialog): selList = self.listBox.selectedItems() if selList: projName = selList[0].text(self.C_NAME) - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("Remove Entry"), self.tr( "Remove '{0}' from the recent projects list? " @@ -280,7 +280,7 @@ class GuiProjectLoad(QDialog): sortList = sorted(dataList, key=lambda x: x[1], reverse=True) for theTitle, theTime, theWords, projPath in sortList: newItem = QTreeWidgetItem([""]*4) - newItem.setIcon(self.C_NAME, self.theParent.theTheme.getIcon("proj_nwx")) + newItem.setIcon(self.C_NAME, self.mainGui.mainTheme.getIcon("proj_nwx")) newItem.setText(self.C_NAME, theTitle) newItem.setData(self.C_NAME, Qt.UserRole, projPath) newItem.setText(self.C_COUNT, formatInt(theWords)) @@ -288,7 +288,7 @@ class GuiProjectLoad(QDialog): newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter) newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter) newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter) - newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed) + newItem.setFont(self.C_TIME, self.mainTheme.guiFontFixed) self.listBox.addTopLevelItem(newItem) if self.listBox.topLevelItemCount() > 0: diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 8bbdcee3..11a640b1 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -43,15 +43,15 @@ logger = logging.getLogger(__name__) class GuiProjectSettings(PagedDialog): - def __init__(self, theParent): - PagedDialog.__init__(self, theParent) + def __init__(self, mainGui): + PagedDialog.__init__(self, mainGui) logger.debug("Initialising GuiProjectSettings ...") self.setObjectName("GuiProjectSettings") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theProject = mainGui.theProject self.theProject.countStatus() self.setWindowTitle(self.tr("Project Settings")) @@ -67,10 +67,10 @@ class GuiProjectSettings(PagedDialog): self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH)) ) - self.tabMain = GuiProjectEditMain(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.tabMain = GuiProjectEditMain(self.mainGui, self.theProject) + self.tabStatus = GuiProjectEditStatus(self.mainGui, self.theProject, True) + self.tabImport = GuiProjectEditStatus(self.mainGui, self.theProject, False) + self.tabReplace = GuiProjectEditReplace(self.mainGui, self.theProject) self.addTab(self.tabMain, self.tr("Settings")) self.addTab(self.tabStatus, self.tr("Status")) @@ -121,7 +121,7 @@ class GuiProjectSettings(PagedDialog): self.theProject.setImportColours(newList, delList) if self.tabStatus.colChanged or self.tabImport.colChanged: - self.theParent.rebuildTrees() + self.mainGui.rebuildTrees() if self.tabReplace.arChanged: newList = self.tabReplace.getNewList() @@ -166,22 +166,22 @@ class GuiProjectSettings(PagedDialog): class GuiProjectEditMain(QWidget): - def __init__(self, theParent, theProject): - QWidget.__init__(self, theParent) + def __init__(self, mainGui, theProject): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent + self.mainGui = mainGui self.theProject = theProject # The Form self.mainForm = QConfigLayout() - self.mainForm.setHelpTextStyle(self.theParent.theTheme.helpText) + self.mainForm.setHelpTextStyle(self.mainGui.mainTheme.helpText) self.setLayout(self.mainForm) self.mainForm.addGroupLabel(self.tr("Project Settings")) xW = self.mainConf.pxInt(250) - xH = round(4.8*self.theParent.theTheme.fontPixelSize) + xH = round(4.8*self.mainGui.mainTheme.fontPixelSize) self.editName = QLineEdit() self.editName.setMaxLength(200) @@ -216,7 +216,7 @@ class GuiProjectEditMain(QWidget): self.spellLang = QComboBox(self) self.spellLang.setMaximumWidth(xW) self.spellLang.addItem(self.tr("Default"), "None") - langAvail = self.theParent.docEditor.spEnchant.listDictionaries() + langAvail = self.mainGui.docEditor.spEnchant.listDictionaries() for spTag, spProv in langAvail: qLocal = QLocale(spTag) spLang = qLocal.nativeLanguageName().title() @@ -256,13 +256,13 @@ class GuiProjectEditStatus(QWidget): COL_ROLE = Qt.UserRole + 1 NUM_ROLE = Qt.UserRole + 2 - def __init__(self, theParent, theProject, isStatus): - QWidget.__init__(self, theParent) + def __init__(self, mainGui, theProject, isStatus): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent + self.mainGui = mainGui self.theProject = theProject - self.theTheme = theParent.theTheme + self.mainTheme = mainGui.mainTheme if isStatus: self.theStatus = self.theProject.statusItems @@ -281,7 +281,7 @@ class GuiProjectEditStatus(QWidget): self.colChanged = False self.selColour = QColor(100, 100, 100) - self.iPx = self.theTheme.baseIconSize + self.iPx = self.mainTheme.baseIconSize # The List # ======== @@ -300,16 +300,16 @@ class GuiProjectEditStatus(QWidget): # List Controls # ============= - self.addButton = QPushButton(self.theTheme.getIcon("add"), "") + self.addButton = QPushButton(self.mainTheme.getIcon("add"), "") self.addButton.clicked.connect(self._newItem) - self.delButton = QPushButton(self.theTheme.getIcon("remove"), "") + self.delButton = QPushButton(self.mainTheme.getIcon("remove"), "") self.delButton.clicked.connect(self._delItem) - self.upButton = QPushButton(self.theTheme.getIcon("up"), "") + self.upButton = QPushButton(self.mainTheme.getIcon("up"), "") self.upButton.clicked.connect(lambda: self._moveItem(-1)) - self.dnButton = QPushButton(self.theTheme.getIcon("down"), "") + self.dnButton = QPushButton(self.mainTheme.getIcon("down"), "") self.dnButton.clicked.connect(lambda: self._moveItem(1)) # Edit Form @@ -411,7 +411,7 @@ class GuiProjectEditStatus(QWidget): if selItem is not None: iRow = self.listBox.indexOfTopLevelItem(selItem) if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot delete a status item that is in use." ), nwAlert.ERROR) else: @@ -527,12 +527,12 @@ class GuiProjectEditReplace(QWidget): COL_KEY = 0 COL_REPL = 1 - def __init__(self, theParent, theProject): - QWidget.__init__(self, theParent) + def __init__(self, mainGui, theProject): + QWidget.__init__(self, mainGui) self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme self.theProject = theProject self.arChanged = False @@ -563,10 +563,10 @@ class GuiProjectEditReplace(QWidget): # List Controls # ============= - self.addButton = QPushButton(self.theTheme.getIcon("add"), "") + self.addButton = QPushButton(self.mainTheme.getIcon("add"), "") self.addButton.clicked.connect(self._addEntry) - self.delButton = QPushButton(self.theTheme.getIcon("remove"), "") + self.delButton = QPushButton(self.mainTheme.getIcon("remove"), "") self.delButton.clicked.connect(self._delEntry) # Edit Form diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py index e8426adf..299386d8 100644 --- a/novelwriter/dialogs/quotes.py +++ b/novelwriter/dialogs/quotes.py @@ -42,8 +42,8 @@ class GuiQuoteSelect(QDialog): selectedQuote = "" - def __init__(self, theParent=None, currentQuote='"'): - QDialog.__init__(self, parent=theParent) + def __init__(self, parent=None, currentQuote='"'): + QDialog.__init__(self, parent=parent) self.mainConf = novelwriter.CONFIG diff --git a/novelwriter/dialogs/updates.py b/novelwriter/dialogs/updates.py index fccc49c2..c4782e6d 100644 --- a/novelwriter/dialogs/updates.py +++ b/novelwriter/dialogs/updates.py @@ -43,14 +43,14 @@ logger = logging.getLogger(__name__) class GuiUpdates(QDialog): - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiUpdates ...") self.setObjectName("GuiUpdates") - self.mainConf = novelwriter.CONFIG - self.theParent = theParent + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui self.setWindowTitle(self.tr("Check for Updates")) @@ -61,7 +61,7 @@ class GuiUpdates(QDialog): # Left Box self.nwIcon = QLabel() - self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx))) + self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx))) self.leftBox = QVBoxLayout() self.leftBox.addWidget(self.nwIcon) diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index 77a4fb5a..9a17eed8 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -42,16 +42,16 @@ logger = logging.getLogger(__name__) class GuiWordList(QDialog): - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiWordList ...") self.setObjectName("GuiWordList") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme + self.theProject = mainGui.theProject self.setWindowTitle(self.tr("Project Word List")) @@ -78,10 +78,10 @@ class GuiWordList(QDialog): self.newEntry = QLineEdit() - self.addButton = QPushButton(self.theTheme.getIcon("add"), "") + self.addButton = QPushButton(self.mainTheme.getIcon("add"), "") self.addButton.clicked.connect(self._doAdd) - self.delButton = QPushButton(self.theTheme.getIcon("remove"), "") + self.delButton = QPushButton(self.mainTheme.getIcon("remove"), "") self.delButton.clicked.connect(self._doDelete) self.editBox = QHBoxLayout() @@ -121,13 +121,13 @@ class GuiWordList(QDialog): """ newWord = self.newEntry.text().strip() if newWord == "": - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot add a blank word." ), nwAlert.ERROR) return False if self.listBox.findItems(newWord, Qt.MatchExactly): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "The word '{0}' is already in the word list." ).format(newWord), nwAlert.ERROR) return False diff --git a/novelwriter/gui/__init__.py b/novelwriter/gui/__init__.py index e3560a99..17699942 100644 --- a/novelwriter/gui/__init__.py +++ b/novelwriter/gui/__init__.py @@ -23,9 +23,9 @@ from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.docviewer import GuiDocViewer, GuiDocViewDetails from novelwriter.gui.itemdetails import GuiItemDetails from novelwriter.gui.mainmenu import GuiMainMenu -from novelwriter.gui.noveltree import GuiNovelTree -from novelwriter.gui.outline import GuiOutline -from novelwriter.gui.projtree import GuiProjectTree +from novelwriter.gui.noveltree import GuiNovelView +from novelwriter.gui.outline import GuiOutlineView +from novelwriter.gui.projtree import GuiProjectView from novelwriter.gui.statusbar import GuiMainStatus from novelwriter.gui.theme import GuiTheme from novelwriter.gui.viewsbar import GuiViewsBar @@ -37,9 +37,9 @@ __all__ = [ "GuiItemDetails", "GuiMainMenu", "GuiMainStatus", - "GuiNovelTree", - "GuiOutline", - "GuiProjectTree", + "GuiNovelView", + "GuiOutlineView", + "GuiProjectView", "GuiTheme", "GuiViewsBar", ] diff --git a/novelwriter/gui/custom.py b/novelwriter/gui/custom.py index 62dd3c91..e571a5a1 100644 --- a/novelwriter/gui/custom.py +++ b/novelwriter/gui/custom.py @@ -376,8 +376,8 @@ class QSwitch(QAbstractButton): class PagedDialog(QDialog): - def __init__(self, theParent=None): - QDialog.__init__(self, parent=theParent) + def __init__(self, parent=None): + QDialog.__init__(self, parent=parent) self._tabBar = VerticalTabBar(self) self._tabBar.setExpanding(False) @@ -426,8 +426,8 @@ class PagedDialog(QDialog): class VerticalTabBar(QTabBar): - def __init__(self, theParent=None): - QTabBar.__init__(self, parent=theParent) + def __init__(self, parent=None): + QTabBar.__init__(self, parent=parent) self._mW = novelwriter.CONFIG.pxInt(150) return diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 5ede17f1..0d32689e 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -72,16 +72,16 @@ class GuiDocEditor(QTextEdit): docCountsChanged = pyqtSignal(str, int, int, int) loadDocumentTagRequest = pyqtSignal(str, Enum) - def __init__(self, theParent): - QTextEdit.__init__(self, theParent) + def __init__(self, mainGui): + QTextEdit.__init__(self, mainGui) logger.debug("Initialising GuiDocEditor ...") # Class Variables self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme + self.theProject = mainGui.theProject self._nwDocument = None self._nwItem = None @@ -124,7 +124,7 @@ class GuiDocEditor(QTextEdit): # Syntax self.spEnchant = NWSpellEnchant() - self.highLight = GuiDocHighlighter(qDoc, self.theParent, self.spEnchant) + self.highLight = GuiDocHighlighter(qDoc, self.mainGui, self.spEnchant) # Context Menu self.setContextMenuPolicy(Qt.CustomContextMenu) @@ -239,10 +239,10 @@ class GuiDocEditor(QTextEdit): if self.mainConf.textFont is None: # If none is defined, set a default font theFont = QFont() - if self.mainConf.osWindows and "Arial" in self.theTheme.guiFontDB.families(): + if self.mainConf.osWindows and "Arial" in self.mainTheme.guiFontDB.families(): theFont.setFamily("Arial") theFont.setPointSize(12) - elif self.mainConf.osDarwin and "Courier" in self.theTheme.guiFontDB.families(): + elif self.mainConf.osDarwin and "Courier" in self.mainTheme.guiFontDB.families(): theFont.setFamily("Courier") theFont.setPointSize(12) else: @@ -257,14 +257,14 @@ class GuiDocEditor(QTextEdit): # Set the widget colours to match syntax theme mainPalette = self.palette() - mainPalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) - mainPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack)) - mainPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + mainPalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack)) + mainPalette.setColor(QPalette.Base, QColor(*self.mainTheme.colBack)) + mainPalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.setPalette(mainPalette) docPalette = self.viewport().palette() - docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack)) - docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + docPalette.setColor(QPalette.Base, QColor(*self.mainTheme.colBack)) + docPalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.viewport().setPalette(docPalette) self.docHeader.matchColours() @@ -341,7 +341,7 @@ class GuiDocEditor(QTextEdit): docSize = len(theDoc) if docSize > nwConst.MAX_DOCSIZE: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "The document you are trying to open is too big. " "The document size is {0} MB. " "The maximum size allowed is {1} MB." @@ -417,7 +417,7 @@ class GuiDocEditor(QTextEdit): # Update the status bar if self._nwItem is not None: - self.theParent.setStatus( + self.mainGui.setStatus( self.tr("Opened Document: {0}").format(self._nwItem.itemName) ) @@ -442,7 +442,7 @@ class GuiDocEditor(QTextEdit): """ docSize = len(theText) if docSize > nwConst.MAX_DOCSIZE: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "The text you are trying to add is too big. " "The text size is {0} MB. " "The maximum size allowed is {1} MB." @@ -488,7 +488,7 @@ class GuiDocEditor(QTextEdit): if not self._nwDocument.writeDocument(docText): saveOk = False if self._nwDocument._currHash != self._nwDocument._prevHash: - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("File Changed on Disk"), self.tr( "This document has been changed outside of novelWriter " @@ -499,7 +499,7 @@ class GuiDocEditor(QTextEdit): saveOk = self._nwDocument.writeDocument(docText, forceWrite=True) if not saveOk: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Could not save document."), self._nwDocument.getError() ], nwAlert.ERROR) @@ -511,18 +511,20 @@ class GuiDocEditor(QTextEdit): self.theProject.index.scanText(tHandle, docText) newHeader = self.theProject.index.getHandleHeaderLevel(tHandle) + # ToDo: This should be a signal if self._updateHeaders(checkLevel=True): - self.theParent.requestNovelTreeRefresh() + self.mainGui.requestNovelTreeRefresh() else: - self.theParent.novelView.updateWordCounts(tHandle) + self.mainGui.novelView.updateWordCounts(tHandle) + # ToDo: This should be a signal if oldHeader != newHeader: - self.theParent.treeView.setTreeItemValues(tHandle) - self.theParent.treeMeta.updateViewBox(tHandle) + self.mainGui.projView.setTreeItemValues(tHandle) + self.mainGui.itemDetails.updateViewBox(tHandle) self.docFooter.updateInfo() # Update the status bar - self.theParent.setStatus( + self.mainGui.setStatus( self.tr("Saved Document: {0}").format(self._nwItem.itemName) ) @@ -544,8 +546,8 @@ class GuiDocEditor(QTextEdit): sH = hBar.height() if hBar.isVisible() else 0 tM = cM - if self.mainConf.textWidth > 0 or self.theParent.isFocusMode: - tW = self.mainConf.getTextWidth(self.theParent.isFocusMode) + if self.mainConf.textWidth > 0 or self.mainGui.isFocusMode: + tW = self.mainConf.getTextWidth(self.mainGui.isFocusMode) tM = max((wW - sW - tW)//2, cM) tB = self.frameWidth() @@ -704,7 +706,7 @@ class GuiDocEditor(QTextEdit): if not self.mainConf.hasEnchant: if theMode: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Spell checking requires the package PyEnchant. " "It does not appear to be installed." ), nwAlert.INFO) @@ -714,7 +716,7 @@ class GuiDocEditor(QTextEdit): theMode = False self._spellCheck = theMode - self.theParent.mainMenu.setSpellCheck(theMode) + self.mainGui.mainMenu.setSpellCheck(theMode) self.theProject.setSpellCheck(theMode) self.highLight.setSpellCheck(theMode) if not self._bigDoc: @@ -742,7 +744,7 @@ class GuiDocEditor(QTextEdit): qApp.restoreOverrideCursor() afTime = time() logger.debug("Document highlighted in %.3f ms", 1000*(afTime-bfTime)) - self.theParent.statusBar.setStatus(self.tr("Spell check complete")) + self.mainGui.statusBar.setStatus(self.tr("Spell check complete")) return True @@ -1085,7 +1087,7 @@ class GuiDocEditor(QTextEdit): self._lastFind = None if self.document().characterCount() > nwConst.MAX_DOCSIZE: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "The document has grown too big and you cannot add more text to it. " "The maximum size of a single novelWriter document is {0} MB." ).format( @@ -1245,7 +1247,7 @@ class GuiDocEditor(QTextEdit): if time() - self._lastEdit < 5 * self.wcInterval: logger.verbose("Running word counter") - self.theParent.threadPool.start(self.wCounterDoc) + self.mainGui.threadPool.start(self.wCounterDoc) return @@ -1302,7 +1304,7 @@ class GuiDocEditor(QTextEdit): logger.verbose("Selection word counter is busy") return - self.theParent.threadPool.start(self.wCounterSel) + self.mainGui.threadPool.start(self.wCounterSel) return @@ -1381,7 +1383,7 @@ class GuiDocEditor(QTextEdit): self.docSearch.setResultCount(0, 0) self._lastFind = None if self.docSearch.doNextFile and not goBack: - self.theParent.openNextDocument( + self.mainGui.openNextDocument( self._docHandle, wrapAround=self.docSearch.doLoop ) self.beginSearch() @@ -1401,7 +1403,7 @@ class GuiDocEditor(QTextEdit): if resIdx > maxIdx: if self.docSearch.doNextFile and not goBack: - self.theParent.openNextDocument( + self.mainGui.openNextDocument( self._docHandle, wrapAround=self.docSearch.doLoop ) self.beginSearch() @@ -1645,7 +1647,7 @@ class GuiDocEditor(QTextEdit): """ theCursor = self.textCursor() if not theCursor.hasSelection(): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Please select some text before calling replace quotes." ), nwAlert.ERROR) return False @@ -2187,9 +2189,9 @@ class GuiDocEditSearch(QFrame): self.mainConf = novelwriter.CONFIG self.docEditor = docEditor - self.theParent = docEditor.theParent + self.mainGui = docEditor.mainGui self.theProject = docEditor.theProject - self.theTheme = docEditor.theTheme + self.mainTheme = docEditor.mainTheme self.repVisible = False self.isCaseSense = self.mainConf.searchCase @@ -2200,9 +2202,9 @@ class GuiDocEditSearch(QFrame): self.doMatchCap = self.mainConf.searchMatchCap mPx = self.mainConf.pxInt(6) - tPx = int(0.8*self.theTheme.fontPixelSize) - self.boxFont = self.theTheme.guiFont - self.boxFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + tPx = int(0.8*self.mainTheme.fontPixelSize) + self.boxFont = self.mainTheme.guiFont + self.boxFont.setPointSizeF(0.9*self.mainTheme.fontPointSize) self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) @@ -2236,38 +2238,38 @@ class GuiDocEditSearch(QFrame): self.resultLabel = QLabel("?/?") self.resultLabel.setFont(self.boxFont) - self.resultLabel.setMinimumWidth(self.theTheme.getTextWidth("?/?", self.boxFont)) + self.resultLabel.setMinimumWidth(self.mainTheme.getTextWidth("?/?", self.boxFont)) self.toggleCase = QAction(self.tr("Case Sensitive"), self) - self.toggleCase.setIcon(self.theTheme.getIcon("search_case")) + self.toggleCase.setIcon(self.mainTheme.getIcon("search_case")) self.toggleCase.setCheckable(True) self.toggleCase.setChecked(self.isCaseSense) self.toggleCase.toggled.connect(self._doToggleCase) self.searchOpt.addAction(self.toggleCase) self.toggleWord = QAction(self.tr("Whole Words Only"), self) - self.toggleWord.setIcon(self.theTheme.getIcon("search_word")) + self.toggleWord.setIcon(self.mainTheme.getIcon("search_word")) self.toggleWord.setCheckable(True) self.toggleWord.setChecked(self.isWholeWord) self.toggleWord.toggled.connect(self._doToggleWord) self.searchOpt.addAction(self.toggleWord) self.toggleRegEx = QAction(self.tr("RegEx Mode"), self) - self.toggleRegEx.setIcon(self.theTheme.getIcon("search_regex")) + self.toggleRegEx.setIcon(self.mainTheme.getIcon("search_regex")) self.toggleRegEx.setCheckable(True) self.toggleRegEx.setChecked(self.isRegEx) self.toggleRegEx.toggled.connect(self._doToggleRegEx) self.searchOpt.addAction(self.toggleRegEx) self.toggleLoop = QAction(self.tr("Loop Search"), self) - self.toggleLoop.setIcon(self.theTheme.getIcon("search_loop")) + self.toggleLoop.setIcon(self.mainTheme.getIcon("search_loop")) self.toggleLoop.setCheckable(True) self.toggleLoop.setChecked(self.doLoop) self.toggleLoop.toggled.connect(self._doToggleLoop) self.searchOpt.addAction(self.toggleLoop) self.toggleProject = QAction(self.tr("Search Next File"), self) - self.toggleProject.setIcon(self.theTheme.getIcon("search_project")) + self.toggleProject.setIcon(self.mainTheme.getIcon("search_project")) self.toggleProject.setCheckable(True) self.toggleProject.setChecked(self.doNextFile) self.toggleProject.toggled.connect(self._doToggleProject) @@ -2276,7 +2278,7 @@ class GuiDocEditSearch(QFrame): self.searchOpt.addSeparator() self.toggleMatchCap = QAction(self.tr("Preserve Case"), self) - self.toggleMatchCap.setIcon(self.theTheme.getIcon("search_preserve")) + self.toggleMatchCap.setIcon(self.mainTheme.getIcon("search_preserve")) self.toggleMatchCap.setCheckable(True) self.toggleMatchCap.setChecked(self.doMatchCap) self.toggleMatchCap.toggled.connect(self._doToggleMatchCap) @@ -2285,7 +2287,7 @@ class GuiDocEditSearch(QFrame): self.searchOpt.addSeparator() self.cancelSearch = QAction(self.tr("Close Search"), self) - self.cancelSearch.setIcon(self.theTheme.getIcon("search_cancel")) + self.cancelSearch.setIcon(self.mainTheme.getIcon("search_cancel")) self.cancelSearch.triggered.connect(self._doClose) self.searchOpt.addAction(self.cancelSearch) @@ -2300,12 +2302,12 @@ class GuiDocEditSearch(QFrame): self.showReplace.setStyleSheet("QToolButton {border: none; background: transparent;}") self.showReplace.toggled.connect(self._doToggleReplace) - self.searchButton = QPushButton(self.theTheme.getIcon("search"), "") + self.searchButton = QPushButton(self.mainTheme.getIcon("search"), "") self.searchButton.setFixedSize(QSize(bPx, bPx)) self.searchButton.setToolTip(self.tr("Find in current document")) self.searchButton.clicked.connect(self._doSearch) - self.replaceButton = QPushButton(self.theTheme.getIcon("search_replace"), "") + self.replaceButton = QPushButton(self.mainTheme.getIcon("search_replace"), "") self.replaceButton.setFixedSize(QSize(bPx, bPx)) self.replaceButton.setToolTip(self.tr("Find and replace in current document")) self.replaceButton.clicked.connect(self._doReplace) @@ -2424,7 +2426,7 @@ class GuiDocEditSearch(QFrame): """ currRes = "?" if currRes is None else currRes resCount = "?" if resCount is None else "1000+" if resCount > 1000 else resCount - minWidth = self.theTheme.getTextWidth(f"{resCount}//{resCount}", self.boxFont) + minWidth = self.mainTheme.getTextWidth(f"{resCount}//{resCount}", self.boxFont) self.resultLabel.setText(f"{currRes}/{resCount}") self.resultLabel.setMinimumWidth(minWidth) self.adjustSize() @@ -2575,13 +2577,13 @@ class GuiDocEditHeader(QWidget): self.mainConf = novelwriter.CONFIG self.docEditor = docEditor - self.theParent = docEditor.theParent + self.mainGui = docEditor.mainGui self.theProject = docEditor.theProject - self.theTheme = docEditor.theTheme + self.mainTheme = docEditor.mainTheme self._docHandle = None - fPx = int(0.9*self.theTheme.fontPixelSize) + fPx = int(0.9*self.mainTheme.fontPixelSize) hSp = self.mainConf.pxInt(6) # Main Widget Settings @@ -2598,17 +2600,17 @@ class GuiDocEditHeader(QWidget): self.theTitle.setFixedHeight(fPx) lblFont = self.theTitle.font() - lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize) self.theTitle.setFont(lblFont) buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(*self.theTheme.colText) + ).format(*self.mainTheme.colText) # Buttons self.editButton = QToolButton(self) - self.editButton.setIcon(self.theTheme.getIcon("edit")) + self.editButton.setIcon(self.mainTheme.getIcon("edit")) self.editButton.setContentsMargins(0, 0, 0, 0) self.editButton.setIconSize(QSize(fPx, fPx)) self.editButton.setFixedSize(fPx, fPx) @@ -2619,7 +2621,7 @@ class GuiDocEditHeader(QWidget): self.editButton.clicked.connect(self._editDocument) self.searchButton = QToolButton(self) - self.searchButton.setIcon(self.theTheme.getIcon("search")) + self.searchButton.setIcon(self.mainTheme.getIcon("search")) self.searchButton.setContentsMargins(0, 0, 0, 0) self.searchButton.setIconSize(QSize(fPx, fPx)) self.searchButton.setFixedSize(fPx, fPx) @@ -2630,7 +2632,7 @@ class GuiDocEditHeader(QWidget): self.searchButton.clicked.connect(self._searchDocument) self.minmaxButton = QToolButton(self) - self.minmaxButton.setIcon(self.theTheme.getIcon("maximise")) + self.minmaxButton.setIcon(self.mainTheme.getIcon("maximise")) self.minmaxButton.setContentsMargins(0, 0, 0, 0) self.minmaxButton.setIconSize(QSize(fPx, fPx)) self.minmaxButton.setFixedSize(fPx, fPx) @@ -2641,7 +2643,7 @@ class GuiDocEditHeader(QWidget): self.minmaxButton.clicked.connect(self._minmaxDocument) self.closeButton = QToolButton(self) - self.closeButton.setIcon(self.theTheme.getIcon("close")) + self.closeButton.setIcon(self.mainTheme.getIcon("close")) self.closeButton.setContentsMargins(0, 0, 0, 0) self.closeButton.setIconSize(QSize(fPx, fPx)) self.closeButton.setFixedSize(fPx, fPx) @@ -2684,9 +2686,9 @@ class GuiDocEditHeader(QWidget): theme rather than the main GUI. """ thePalette = QPalette() - thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) - thePalette.setColor(QPalette.WindowText, QColor(*self.theTheme.colText)) - thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + thePalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack)) + thePalette.setColor(QPalette.WindowText, QColor(*self.mainTheme.colText)) + thePalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.setPalette(thePalette) self.theTitle.setPalette(thePalette) @@ -2733,10 +2735,10 @@ class GuiDocEditHeader(QWidget): This function is called by the GuiMain class via the toggleFocusMode function and should not be activated directly. """ - if self.theParent.isFocusMode: - self.minmaxButton.setIcon(self.theTheme.getIcon("minimise")) + if self.mainGui.isFocusMode: + self.minmaxButton.setIcon(self.mainTheme.getIcon("minimise")) else: - self.minmaxButton.setIcon(self.theTheme.getIcon("maximise")) + self.minmaxButton.setIcon(self.mainTheme.getIcon("maximise")) return ## @@ -2746,7 +2748,7 @@ class GuiDocEditHeader(QWidget): def _editDocument(self): """Open the edit item dialog from the main GUI. """ - self.theParent.editItem(self._docHandle) + self.mainGui.editItemLabel(self._docHandle) return def _searchDocument(self): @@ -2758,7 +2760,7 @@ class GuiDocEditHeader(QWidget): def _closeDocument(self): """Trigger the close editor on the main window. """ - self.theParent.closeDocEditor() + self.mainGui.closeDocEditor() self.editButton.setVisible(False) self.searchButton.setVisible(False) self.closeButton.setVisible(False) @@ -2768,7 +2770,7 @@ class GuiDocEditHeader(QWidget): def _minmaxDocument(self): """Switch on or off Focus Mode. """ - self.theParent.toggleFocusMode() + self.mainGui.toggleFocusMode() return ## @@ -2779,7 +2781,7 @@ class GuiDocEditHeader(QWidget): """Capture a click on the title and ensure that the item is selected in the project tree. """ - self.theParent.treeView.setSelectedHandle(self._docHandle, doScroll=True) + self.mainGui.projView.setSelectedHandle(self._docHandle, doScroll=True) return # END Class GuiDocEditHeader @@ -2799,22 +2801,22 @@ class GuiDocEditFooter(QWidget): self.mainConf = novelwriter.CONFIG self.docEditor = docEditor - self.theParent = docEditor.theParent + self.mainGui = docEditor.mainGui self.theProject = docEditor.theProject - self.theTheme = docEditor.theTheme + self.mainTheme = docEditor.mainTheme self._theItem = None self._docHandle = None self._docSelection = False - self.sPx = int(round(0.9*self.theTheme.baseIconSize)) - fPx = int(0.9*self.theTheme.fontPixelSize) + self.sPx = int(round(0.9*self.mainTheme.baseIconSize)) + fPx = int(0.9*self.mainTheme.fontPixelSize) bSp = self.mainConf.pxInt(4) hSp = self.mainConf.pxInt(6) lblFont = self.font() - lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize) # Main Widget Settings self.setContentsMargins(0, 0, 0, 0) @@ -2837,7 +2839,7 @@ class GuiDocEditFooter(QWidget): # Lines self.linesIcon = QLabel("") - self.linesIcon.setPixmap(self.theTheme.getPixmap("status_lines", (self.sPx, self.sPx))) + self.linesIcon.setPixmap(self.mainTheme.getPixmap("status_lines", (self.sPx, self.sPx))) self.linesIcon.setContentsMargins(0, 0, 0, 0) self.linesIcon.setFixedHeight(self.sPx) self.linesIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop) @@ -2853,7 +2855,7 @@ class GuiDocEditFooter(QWidget): # Words self.wordsIcon = QLabel("") - self.wordsIcon.setPixmap(self.theTheme.getPixmap("status_stats", (self.sPx, self.sPx))) + self.wordsIcon.setPixmap(self.mainTheme.getPixmap("status_stats", (self.sPx, self.sPx))) self.wordsIcon.setContentsMargins(0, 0, 0, 0) self.wordsIcon.setFixedHeight(self.sPx) self.wordsIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop) @@ -2905,9 +2907,9 @@ class GuiDocEditFooter(QWidget): theme rather than the main GUI. """ thePalette = QPalette() - thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) - thePalette.setColor(QPalette.WindowText, QColor(*self.theTheme.colText)) - thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + thePalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack)) + thePalette.setColor(QPalette.WindowText, QColor(*self.mainTheme.colText)) + thePalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.setPalette(thePalette) self.statusText.setPalette(thePalette) diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index bd2d78eb..8b7ec1a5 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -46,16 +46,16 @@ class GuiDocHighlighter(QSyntaxHighlighter): BLOCK_META = 2 BLOCK_TITLE = 4 - def __init__(self, theDoc, theParent, spEnchant): + def __init__(self, theDoc, mainGui, spEnchant): QSyntaxHighlighter.__init__(self, theDoc) logger.debug("Initialising GuiDocHighlighter ...") self.mainConf = novelwriter.CONFIG self.theDoc = theDoc self.spEnchant = spEnchant - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme + self.theProject = mainGui.theProject self.theHandle = None self.spellCheck = False self.spellRx = None @@ -87,24 +87,24 @@ class GuiDocHighlighter(QSyntaxHighlighter): """ logger.debug("Setting up highlighting rules") - self.colHead = QColor(*self.theTheme.colHead) - self.colHeadH = QColor(*self.theTheme.colHeadH) - self.colDialN = QColor(*self.theTheme.colDialN) - self.colDialD = QColor(*self.theTheme.colDialD) - self.colDialS = QColor(*self.theTheme.colDialS) - self.colHidden = QColor(*self.theTheme.colHidden) - self.colKey = QColor(*self.theTheme.colKey) - self.colVal = QColor(*self.theTheme.colVal) - self.colSpell = QColor(*self.theTheme.colSpell) - self.colError = QColor(*self.theTheme.colError) - self.colRepTag = QColor(*self.theTheme.colRepTag) - self.colMod = QColor(*self.theTheme.colMod) - self.colBreak = QColor(*self.theTheme.colEmph) + self.colHead = QColor(*self.mainTheme.colHead) + self.colHeadH = QColor(*self.mainTheme.colHeadH) + self.colDialN = QColor(*self.mainTheme.colDialN) + self.colDialD = QColor(*self.mainTheme.colDialD) + self.colDialS = QColor(*self.mainTheme.colDialS) + self.colHidden = QColor(*self.mainTheme.colHidden) + self.colKey = QColor(*self.mainTheme.colKey) + self.colVal = QColor(*self.mainTheme.colVal) + self.colSpell = QColor(*self.mainTheme.colSpell) + self.colError = QColor(*self.mainTheme.colError) + self.colRepTag = QColor(*self.mainTheme.colRepTag) + self.colMod = QColor(*self.mainTheme.colMod) + self.colBreak = QColor(*self.mainTheme.colEmph) self.colBreak.setAlpha(64) self.colEmph = None if self.mainConf.highlightEmph: - self.colEmph = QColor(*self.theTheme.colEmph) + self.colEmph = QColor(*self.mainTheme.colEmph) self.hStyles = { "header1": self._makeFormat(self.colHead, "bold", 1.8), @@ -288,7 +288,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): if theText.startswith("@"): # Keywords and commands self.setCurrentBlockState(self.BLOCK_META) pIndex = self.theProject.index - tItem = self.theParent.theProject.tree[self.theHandle] + tItem = self.mainGui.theProject.tree[self.theHandle] isValid, theBits, thePos = pIndex.scanThis(theText) isGood = pIndex.checkThese(theBits, tItem) if isValid: diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 89ae8ea2..6ce275a3 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -53,16 +53,16 @@ class GuiDocViewer(QTextBrowser): loadDocumentTagRequest = pyqtSignal(str, Enum) - def __init__(self, theParent): - QTextBrowser.__init__(self, theParent) + def __init__(self, mainGui): + QTextBrowser.__init__(self, mainGui) logger.debug("Initialising GuiDocViewer ...") # Class Variables self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme + self.theProject = mainGui.theProject # Internal Variables self._docHandle = None @@ -118,14 +118,14 @@ class GuiDocViewer(QTextBrowser): # Set the widget colours to match syntax theme mainPalette = self.palette() - mainPalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) - mainPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack)) - mainPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + mainPalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack)) + mainPalette.setColor(QPalette.Base, QColor(*self.mainTheme.colBack)) + mainPalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.setPalette(mainPalette) docPalette = self.viewport().palette() - docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack)) - docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + docPalette.setColor(QPalette.Base, QColor(*self.mainTheme.colBack)) + docPalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.viewport().setPalette(docPalette) self.docHeader.matchColours() @@ -221,7 +221,7 @@ class GuiDocViewer(QTextBrowser): self.updateDocMargins() # Make sure the main GUI knows we changed the content - self.theParent.viewMeta.refreshReferences(tHandle) + self.mainGui.viewMeta.refreshReferences(tHandle) # Since we change the content while it may still be rendering, we mark # the document dirty again to make sure it's re-rendered properly. @@ -539,27 +539,27 @@ class GuiDocViewer(QTextBrowser): " text-align: center;" "}}\n" ).format( - tColR=self.theTheme.colText[0], - tColG=self.theTheme.colText[1], - tColB=self.theTheme.colText[2], - hColR=self.theTheme.colHead[0], - hColG=self.theTheme.colHead[1], - hColB=self.theTheme.colHead[2], - aColR=self.theTheme.colVal[0], - aColG=self.theTheme.colVal[1], - aColB=self.theTheme.colVal[2], - eColR=self.theTheme.colEmph[0], - eColG=self.theTheme.colEmph[1], - eColB=self.theTheme.colEmph[2], - kColR=self.theTheme.colKey[0], - kColG=self.theTheme.colKey[1], - kColB=self.theTheme.colKey[2], - cColR=self.theTheme.colHidden[0], - cColG=self.theTheme.colHidden[1], - cColB=self.theTheme.colHidden[2], - mColR=self.theTheme.colMod[0], - mColG=self.theTheme.colMod[1], - mColB=self.theTheme.colMod[2], + tColR=self.mainTheme.colText[0], + tColG=self.mainTheme.colText[1], + tColB=self.mainTheme.colText[2], + hColR=self.mainTheme.colHead[0], + hColG=self.mainTheme.colHead[1], + hColB=self.mainTheme.colHead[2], + aColR=self.mainTheme.colVal[0], + aColG=self.mainTheme.colVal[1], + aColB=self.mainTheme.colVal[2], + eColR=self.mainTheme.colEmph[0], + eColG=self.mainTheme.colEmph[1], + eColB=self.mainTheme.colEmph[2], + kColR=self.mainTheme.colKey[0], + kColG=self.mainTheme.colKey[1], + kColB=self.mainTheme.colKey[2], + cColR=self.mainTheme.colHidden[0], + cColG=self.mainTheme.colHidden[1], + cColB=self.mainTheme.colHidden[2], + mColR=self.mainTheme.colMod[0], + mColG=self.mainTheme.colMod[1], + mColB=self.mainTheme.colMod[2], ) self.document().setDefaultStyleSheet(styleSheet) @@ -714,14 +714,14 @@ class GuiDocViewHeader(QWidget): self.mainConf = novelwriter.CONFIG self.docViewer = docViewer - self.theParent = docViewer.theParent + self.mainGui = docViewer.mainGui self.theProject = docViewer.theProject - self.theTheme = docViewer.theTheme + self.mainTheme = docViewer.mainTheme # Internal Variables self._docHandle = None - fPx = int(0.9*self.theTheme.fontPixelSize) + fPx = int(0.9*self.mainTheme.fontPixelSize) hSp = self.mainConf.pxInt(6) # Main Widget Settings @@ -738,17 +738,17 @@ class GuiDocViewHeader(QWidget): self.theTitle.setFixedHeight(fPx) lblFont = self.theTitle.font() - lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize) self.theTitle.setFont(lblFont) buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(*self.theTheme.colText) + ).format(*self.mainTheme.colText) # Buttons self.backButton = QToolButton(self) - self.backButton.setIcon(self.theTheme.getIcon("backward")) + self.backButton.setIcon(self.mainTheme.getIcon("backward")) self.backButton.setContentsMargins(0, 0, 0, 0) self.backButton.setIconSize(QSize(fPx, fPx)) self.backButton.setFixedSize(fPx, fPx) @@ -759,7 +759,7 @@ class GuiDocViewHeader(QWidget): self.backButton.clicked.connect(self.docViewer.navBackward) self.forwardButton = QToolButton(self) - self.forwardButton.setIcon(self.theTheme.getIcon("forward")) + self.forwardButton.setIcon(self.mainTheme.getIcon("forward")) self.forwardButton.setContentsMargins(0, 0, 0, 0) self.forwardButton.setIconSize(QSize(fPx, fPx)) self.forwardButton.setFixedSize(fPx, fPx) @@ -770,7 +770,7 @@ class GuiDocViewHeader(QWidget): self.forwardButton.clicked.connect(self.docViewer.navForward) self.refreshButton = QToolButton(self) - self.refreshButton.setIcon(self.theTheme.getIcon("refresh")) + self.refreshButton.setIcon(self.mainTheme.getIcon("refresh")) self.refreshButton.setContentsMargins(0, 0, 0, 0) self.refreshButton.setIconSize(QSize(fPx, fPx)) self.refreshButton.setFixedSize(fPx, fPx) @@ -781,7 +781,7 @@ class GuiDocViewHeader(QWidget): self.refreshButton.clicked.connect(self._refreshDocument) self.closeButton = QToolButton(self) - self.closeButton.setIcon(self.theTheme.getIcon("close")) + self.closeButton.setIcon(self.mainTheme.getIcon("close")) self.closeButton.setContentsMargins(0, 0, 0, 0) self.closeButton.setIconSize(QSize(fPx, fPx)) self.closeButton.setFixedSize(fPx, fPx) @@ -824,9 +824,9 @@ class GuiDocViewHeader(QWidget): theme rather than the main GUI. """ thePalette = QPalette() - thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) - thePalette.setColor(QPalette.WindowText, QColor(*self.theTheme.colText)) - thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + thePalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack)) + thePalette.setColor(QPalette.WindowText, QColor(*self.mainTheme.colText)) + thePalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.setPalette(thePalette) self.theTitle.setPalette(thePalette) @@ -882,14 +882,14 @@ class GuiDocViewHeader(QWidget): def _closeDocument(self): """Trigger the close editor/viewer on the main window. """ - self.theParent.closeDocViewer() + self.mainGui.closeDocViewer() return def _refreshDocument(self): """Reload the content of the document. """ - if self.docViewer.docHandle() == self.theParent.docEditor.docHandle(): - self.theParent.saveDocument() + if self.docViewer.docHandle() == self.mainGui.docEditor.docHandle(): + self.mainGui.saveDocument() self.docViewer.reloadText() return @@ -901,7 +901,7 @@ class GuiDocViewHeader(QWidget): """Capture a click on the title and ensure that the item is selected in the project tree. """ - self.theParent.treeView.setSelectedHandle(self._docHandle, doScroll=True) + self.mainGui.projView.setSelectedHandle(self._docHandle, doScroll=True) return # END Class GuiDocViewHeader @@ -921,26 +921,26 @@ class GuiDocViewFooter(QWidget): self.mainConf = novelwriter.CONFIG self.docViewer = docViewer - self.theParent = docViewer.theParent - self.theTheme = docViewer.theTheme - self.viewMeta = docViewer.theParent.viewMeta + self.mainGui = docViewer.mainGui + self.mainTheme = docViewer.mainTheme + self.viewMeta = docViewer.mainGui.viewMeta # Internal Variables self._docHandle = None - fPx = int(0.9*self.theTheme.fontPixelSize) + fPx = int(0.9*self.mainTheme.fontPixelSize) bSp = self.mainConf.pxInt(2) hSp = self.mainConf.pxInt(8) # Icons - stickyOn = self.theTheme.getPixmap("sticky-on", (fPx, fPx)) - stickyOff = self.theTheme.getPixmap("sticky-off", (fPx, fPx)) + stickyOn = self.mainTheme.getPixmap("sticky-on", (fPx, fPx)) + stickyOff = self.mainTheme.getPixmap("sticky-off", (fPx, fPx)) stickyIcon = QIcon() stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On) stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off) - bulletOn = self.theTheme.getPixmap("bullet-on", (fPx, fPx)) - bulletOff = self.theTheme.getPixmap("bullet-off", (fPx, fPx)) + bulletOn = self.mainTheme.getPixmap("bullet-on", (fPx, fPx)) + bulletOff = self.mainTheme.getPixmap("bullet-off", (fPx, fPx)) bulletIcon = QIcon() bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On) bulletIcon.addPixmap(bulletOff, QIcon.Normal, QIcon.Off) @@ -952,13 +952,13 @@ class GuiDocViewFooter(QWidget): buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(*self.theTheme.colText) + ).format(*self.mainTheme.colText) # Show/Hide Details self.showHide = QToolButton(self) self.showHide.setToolButtonStyle(Qt.ToolButtonIconOnly) self.showHide.setStyleSheet(buttonStyle) - self.showHide.setIcon(self.theTheme.getIcon("reference")) + self.showHide.setIcon(self.mainTheme.getIcon("reference")) self.showHide.setIconSize(QSize(fPx, fPx)) self.showHide.setFixedSize(QSize(fPx, fPx)) self.showHide.clicked.connect(self._doShowHide) @@ -1039,7 +1039,7 @@ class GuiDocViewFooter(QWidget): self.lblSynopsis.setAlignment(Qt.AlignLeft | Qt.AlignTop) lblFont = self.font() - lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize) self.lblRefs.setFont(lblFont) self.lblSticky.setFont(lblFont) self.lblComments.setFont(lblFont) @@ -1084,9 +1084,9 @@ class GuiDocViewFooter(QWidget): theme rather than the main GUI. """ thePalette = QPalette() - thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) - thePalette.setColor(QPalette.WindowText, QColor(*self.theTheme.colText)) - thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) + thePalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack)) + thePalette.setColor(QPalette.WindowText, QColor(*self.mainTheme.colText)) + thePalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText)) self.setPalette(thePalette) self.lblRefs.setPalette(thePalette) @@ -1140,14 +1140,14 @@ class GuiDocViewFooter(QWidget): class GuiDocViewDetails(QScrollArea): - def __init__(self, theParent): - QScrollArea.__init__(self, theParent) + def __init__(self, mainGui): + QScrollArea.__init__(self, mainGui) logger.debug("Initialising GuiDocViewDetails ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.theProject = mainGui.theProject + self.mainTheme = mainGui.mainTheme self.refList = QLabel("") self.refList.setWordWrap(True) @@ -1156,7 +1156,7 @@ class GuiDocViewDetails(QScrollArea): self.refList.linkActivated.connect(self._linkClicked) self.linkStyle = "style='color: rgb({0},{1},{2})'".format( - *self.theTheme.colLink + *self.mainTheme.colLink ) # Assemble @@ -1181,7 +1181,7 @@ class GuiDocViewDetails(QScrollArea): """Update the current list of document references from the project index. """ - if self.theParent.docViewer.stickyRef: + if self.mainGui.docViewer.stickyRef: return theRefs = self.theProject.index.getBackReferenceList(tHandle) @@ -1209,7 +1209,7 @@ class GuiDocViewDetails(QScrollArea): if len(theLink) == 21: tHandle = theLink[:13] tAnchor = theLink[13:] - self.theParent.viewDocument(tHandle, tAnchor) + self.mainGui.viewDocument(tHandle, tAnchor) return # END Class GuiDocViewDetails diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 643479a4..859e58f2 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -38,14 +38,14 @@ logger = logging.getLogger(__name__) class GuiItemDetails(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) logger.debug("Initialising GuiItemDetails ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.theProject = mainGui.theProject + self.mainTheme = mainGui.mainTheme # Internal Variables self._itemHandle = None @@ -54,11 +54,11 @@ class GuiItemDetails(QWidget): hSp = self.mainConf.pxInt(6) vSp = self.mainConf.pxInt(1) mPx = self.mainConf.pxInt(6) - iPx = self.theTheme.baseIconSize - fPt = self.theTheme.fontPointSize + iPx = self.mainTheme.baseIconSize + fPt = self.mainTheme.fontPointSize - self._expCheck = self.theTheme.getPixmap("check", (iPx, iPx)) - self._expCross = self.theTheme.getPixmap("cross", (iPx, iPx)) + self._expCheck = self.mainTheme.getPixmap("check", (iPx, iPx)) + self._expCross = self.mainTheme.getPixmap("cross", (iPx, iPx)) fntLabel = QFont() fntLabel.setBold(True) @@ -181,8 +181,8 @@ class GuiItemDetails(QWidget): self.setLayout(self.mainBox) # Make sure the columns for flags and counts don't resize too often - flagWidth = self.theTheme.getTextWidth("Mm", fntValue) - countWidth = self.theTheme.getTextWidth("99,999", fntValue) + flagWidth = self.mainTheme.getTextWidth("Mm", fntValue) + countWidth = self.mainTheme.getTextWidth("99,999", fntValue) self.mainBox.setColumnMinimumWidth(1, flagWidth) self.mainBox.setColumnMinimumWidth(4, countWidth) @@ -238,7 +238,7 @@ class GuiItemDetails(QWidget): return self._itemHandle = tHandle - iPx = int(round(0.8*self.theTheme.baseIconSize)) + iPx = int(round(0.8*self.mainTheme.baseIconSize)) # Label # ===== @@ -267,7 +267,7 @@ class GuiItemDetails(QWidget): # Class # ===== - classIcon = self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]) + classIcon = self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]) self.classIcon.setPixmap(classIcon.pixmap(iPx, iPx)) self.classData.setText(trConst(nwLabels.CLASS_NAME[nwItem.itemClass])) @@ -275,7 +275,7 @@ class GuiItemDetails(QWidget): # ====== hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) - usageIcon = self.theTheme.getItemIcon( + usageIcon = self.mainTheme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel ) self.usageIcon.setPixmap(usageIcon.pixmap(iPx, iPx)) @@ -296,7 +296,7 @@ class GuiItemDetails(QWidget): return @pyqtSlot(str, int, int, int) - def doUpdateCounts(self, tHandle, cC, wC, pC): + def updateCounts(self, tHandle, cC, wC, pC): """Update the counts if the handle is the same as the one we're already showing. Otherwise, do nothing. """ diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index bd8d1edd..59af7691 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -33,7 +33,7 @@ from PyQt5.QtCore import QUrl from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMenuBar, QAction -from novelwriter.enum import nwItemType, nwItemClass, nwDocAction, nwDocInsert, nwWidget +from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget from novelwriter.constants import trConst, nwKeyWords, nwLabels, nwUnicode logger = logging.getLogger(__name__) @@ -41,13 +41,13 @@ logger = logging.getLogger(__name__) class GuiMainMenu(QMenuBar): - def __init__(self, theParent): - QMenuBar.__init__(self, theParent) + def __init__(self, mainGui): + QMenuBar.__init__(self, mainGui) logger.debug("Initialising GuiMainMenu ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theProject = theParent.theProject + self.mainGui = mainGui + self.theProject = mainGui.theProject # Build Menu self._buildProjectMenu() @@ -61,11 +61,9 @@ class GuiMainMenu(QMenuBar): self._buildHelpMenu() # Function Pointers - self._docAction = self.theParent.passDocumentAction - self._moveTreeItem = self.theParent.treeView.moveTreeItem - self._newTreeItem = self.theParent.treeView.newTreeItem - self._docInsert = self.theParent.docEditor.insertText - self._insertKeyWord = self.theParent.docEditor.insertKeyWord + self._docAction = self.mainGui.passDocumentAction + self._docInsert = self.mainGui.docEditor.insertText + self._insertKeyWord = self.mainGui.docEditor.insertKeyWord logger.debug("GuiMainMenu initialisation complete") @@ -96,7 +94,7 @@ class GuiMainMenu(QMenuBar): flag is handled by the document editor class, so we make no decision, just pass a None to the function and let it decide. """ - self.theParent.docEditor.toggleSpellCheck(None) + self.mainGui.docEditor.toggleSpellCheck(None) return True def _openWebsite(self, theUrl): @@ -125,25 +123,25 @@ class GuiMainMenu(QMenuBar): # Project > New Project self.aNewProject = QAction(self.tr("New Project"), self) - self.aNewProject.triggered.connect(lambda: self.theParent.newProject(None)) + self.aNewProject.triggered.connect(lambda: self.mainGui.newProject(None)) self.projMenu.addAction(self.aNewProject) # Project > Open Project self.aOpenProject = QAction(self.tr("Open Project"), self) self.aOpenProject.setShortcut("Ctrl+Shift+O") - self.aOpenProject.triggered.connect(lambda: self.theParent.showProjectLoadDialog()) + self.aOpenProject.triggered.connect(lambda: self.mainGui.showProjectLoadDialog()) self.projMenu.addAction(self.aOpenProject) # Project > Save Project self.aSaveProject = QAction(self.tr("Save Project"), self) self.aSaveProject.setShortcut("Ctrl+Shift+S") - self.aSaveProject.triggered.connect(lambda: self.theParent.saveProject()) + self.aSaveProject.triggered.connect(lambda: self.mainGui.saveProject()) self.projMenu.addAction(self.aSaveProject) # Project > Close Project self.aCloseProject = QAction(self.tr("Close Project"), self) self.aCloseProject.setShortcut("Ctrl+Shift+W") - self.aCloseProject.triggered.connect(lambda: self.theParent.closeProject(False)) + self.aCloseProject.triggered.connect(lambda: self.mainGui.closeProject(False)) self.projMenu.addAction(self.aCloseProject) # Project > Separator @@ -152,78 +150,33 @@ class GuiMainMenu(QMenuBar): # Project > Project Settings self.aProjectSettings = QAction(self.tr("Project Settings"), self) self.aProjectSettings.setShortcut("Ctrl+Shift+,") - self.aProjectSettings.triggered.connect(lambda: self.theParent.showProjectSettingsDialog()) + self.aProjectSettings.triggered.connect(lambda: self.mainGui.showProjectSettingsDialog()) self.projMenu.addAction(self.aProjectSettings) # Project > Project Details self.aProjectDetails = QAction(self.tr("Project Details"), self) self.aProjectDetails.setShortcut("Shift+F6") - self.aProjectDetails.triggered.connect(lambda: self.theParent.showProjectDetailsDialog()) + self.aProjectDetails.triggered.connect(lambda: self.mainGui.showProjectDetailsDialog()) self.projMenu.addAction(self.aProjectDetails) # Project > Separator self.projMenu.addSeparator() - # Project > New Root - self.rootMenu = self.projMenu.addMenu(self.tr("Create Root Folder")) - self.rootItems = {} - self.rootItems[nwItemClass.NOVEL] = QAction(self.tr("Novel Root"), self.rootMenu) - self.rootItems[nwItemClass.PLOT] = QAction(self.tr("Plot Root"), self.rootMenu) - self.rootItems[nwItemClass.CHARACTER] = QAction(self.tr("Character Root"), self.rootMenu) - self.rootItems[nwItemClass.WORLD] = QAction(self.tr("Location Root"), self.rootMenu) - self.rootItems[nwItemClass.TIMELINE] = QAction(self.tr("Timeline Root"), self.rootMenu) - self.rootItems[nwItemClass.OBJECT] = QAction(self.tr("Object Root"), self.rootMenu) - self.rootItems[nwItemClass.ENTITY] = QAction(self.tr("Entity Root"), self.rootMenu) - self.rootItems[nwItemClass.CUSTOM] = QAction(self.tr("Custom Root"), self.rootMenu) - self.rootItems[nwItemClass.ARCHIVE] = QAction(self.tr("Archive Root"), self.rootMenu) - for n, itemClass in enumerate(self.rootItems.keys()): - self.rootItems[itemClass].triggered.connect( - lambda n, itemClass=itemClass: self._newTreeItem(nwItemType.ROOT, itemClass) - ) - self.rootMenu.addAction(self.rootItems[itemClass]) - - # Project > New Folder - self.aCreateFolder = QAction(self.tr("Create Folder"), self) - self.aCreateFolder.setShortcut("Ctrl+Shift+N") - self.aCreateFolder.triggered.connect(lambda: self._newTreeItem(nwItemType.FOLDER)) - self.projMenu.addAction(self.aCreateFolder) - - # Project > Separator - self.projMenu.addSeparator() - # Project > Edit - self.aEditItem = QAction(self.tr("Edit Item"), self) - self.aEditItem.setShortcuts(["Ctrl+E", "F2"]) - self.aEditItem.triggered.connect(lambda: self.theParent.editItem(None)) + self.aEditItem = QAction(self.tr("Rename Item"), self) + self.aEditItem.setShortcuts(["F2"]) + self.aEditItem.triggered.connect(lambda: self.mainGui.editItemLabel(None)) self.projMenu.addAction(self.aEditItem) # Project > Delete self.aDeleteItem = QAction(self.tr("Delete Item"), self) self.aDeleteItem.setShortcut("Ctrl+Shift+Del") - self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None)) + self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.deleteItem(None)) self.projMenu.addAction(self.aDeleteItem) - # Project > Move Up - self.aMoveUp = QAction(self.tr("Move Item Up"), self) - self.aMoveUp.setShortcut("Ctrl+Up") - self.aMoveUp.triggered.connect(lambda: self._moveTreeItem(-1)) - self.projMenu.addAction(self.aMoveUp) - - # Project > Move Down - self.aMoveDown = QAction(self.tr("Move Item Down"), self) - self.aMoveDown.setShortcut("Ctrl+Down") - self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1)) - self.projMenu.addAction(self.aMoveDown) - - # Project > Undo Last Action - self.aMoveUndo = QAction(self.tr("Undo Last Move"), self) - self.aMoveUndo.setShortcut("Ctrl+Shift+Z") - self.aMoveUndo.triggered.connect(lambda: self.theParent.treeView.undoLastMove()) - self.projMenu.addAction(self.aMoveUndo) - # Project > Empty Trash self.aEmptyTrash = QAction(self.tr("Empty Trash"), self) - self.aEmptyTrash.triggered.connect(lambda: self.theParent.treeView.emptyTrash()) + self.aEmptyTrash.triggered.connect(lambda: self.mainGui.projView.emptyTrash()) self.projMenu.addAction(self.aEmptyTrash) # Project > Separator @@ -233,7 +186,7 @@ class GuiMainMenu(QMenuBar): self.aExitNW = QAction(self.tr("Exit"), self) self.aExitNW.setShortcut("Ctrl+Q") self.aExitNW.setMenuRole(QAction.QuitRole) - self.aExitNW.triggered.connect(lambda: self.theParent.closeMain()) + self.aExitNW.triggered.connect(lambda: self.mainGui.closeMain()) self.projMenu.addAction(self.aExitNW) return @@ -244,28 +197,22 @@ class GuiMainMenu(QMenuBar): # Document self.docuMenu = self.addMenu(self.tr("&Document")) - # Document > New - self.aNewDoc = QAction(self.tr("New Document"), self) - self.aNewDoc.setShortcut("Ctrl+N") - self.aNewDoc.triggered.connect(lambda: self._newTreeItem(nwItemType.FILE)) - self.docuMenu.addAction(self.aNewDoc) - # Document > Open self.aOpenDoc = QAction(self.tr("Open Document"), self) self.aOpenDoc.setShortcut("Ctrl+O") - self.aOpenDoc.triggered.connect(lambda: self.theParent.openSelectedItem()) + self.aOpenDoc.triggered.connect(lambda: self.mainGui.openSelectedItem()) self.docuMenu.addAction(self.aOpenDoc) # Document > Save self.aSaveDoc = QAction(self.tr("Save Document"), self) self.aSaveDoc.setShortcut("Ctrl+S") - self.aSaveDoc.triggered.connect(lambda: self.theParent.saveDocument()) + self.aSaveDoc.triggered.connect(lambda: self.mainGui.saveDocument()) self.docuMenu.addAction(self.aSaveDoc) # Document > Close self.aCloseDoc = QAction(self.tr("Close Document"), self) self.aCloseDoc.setShortcut("Ctrl+W") - self.aCloseDoc.triggered.connect(lambda: self.theParent.closeDocEditor()) + self.aCloseDoc.triggered.connect(lambda: self.mainGui.closeDocEditor()) self.docuMenu.addAction(self.aCloseDoc) # Document > Separator @@ -274,13 +221,13 @@ class GuiMainMenu(QMenuBar): # Document > Preview self.aViewDoc = QAction(self.tr("View Document"), self) self.aViewDoc.setShortcut("Ctrl+R") - self.aViewDoc.triggered.connect(lambda: self.theParent.viewDocument(None)) + self.aViewDoc.triggered.connect(lambda: self.mainGui.viewDocument(None)) self.docuMenu.addAction(self.aViewDoc) # Document > Close Preview self.aCloseView = QAction(self.tr("Close Document View"), self) self.aCloseView.setShortcut("Ctrl+Shift+R") - self.aCloseView.triggered.connect(lambda: self.theParent.closeDocViewer()) + self.aCloseView.triggered.connect(lambda: self.mainGui.closeDocViewer()) self.docuMenu.addAction(self.aCloseView) # Document > Separator @@ -288,23 +235,23 @@ class GuiMainMenu(QMenuBar): # Document > Show File Details self.aFileDetails = QAction(self.tr("Show File Details"), self) - self.aFileDetails.triggered.connect(lambda: self.theParent.docEditor.revealLocation()) + self.aFileDetails.triggered.connect(lambda: self.mainGui.docEditor.revealLocation()) self.docuMenu.addAction(self.aFileDetails) # Document > Import From File self.aImportFile = QAction(self.tr("Import Text from File"), self) self.aImportFile.setShortcut("Ctrl+Shift+I") - self.aImportFile.triggered.connect(lambda: self.theParent.importDocument()) + self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument()) self.docuMenu.addAction(self.aImportFile) # Document > Merge Documents self.aMergeDocs = QAction(self.tr("Merge Folder to Document"), self) - self.aMergeDocs.triggered.connect(lambda: self.theParent.mergeDocuments()) + self.aMergeDocs.triggered.connect(lambda: self.mainGui.mergeDocuments()) self.docuMenu.addAction(self.aMergeDocs) # Document > Split Document self.aSplitDoc = QAction(self.tr("Split Document to Folder"), self) - self.aSplitDoc.triggered.connect(lambda: self.theParent.splitDocument()) + self.aSplitDoc.triggered.connect(lambda: self.mainGui.splitDocument()) self.docuMenu.addAction(self.aSplitDoc) return @@ -377,7 +324,7 @@ class GuiMainMenu(QMenuBar): self.aFocusTree.setShortcut("Ctrl+Alt+1") else: self.aFocusTree.setShortcut("Alt+1") - self.aFocusTree.triggered.connect(lambda: self.theParent.switchFocus(nwWidget.TREE)) + self.aFocusTree.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.TREE)) self.viewMenu.addAction(self.aFocusTree) # View > Document Pane 1 @@ -386,7 +333,7 @@ class GuiMainMenu(QMenuBar): self.aFocusEditor.setShortcut("Ctrl+Alt+2") else: self.aFocusEditor.setShortcut("Alt+2") - self.aFocusEditor.triggered.connect(lambda: self.theParent.switchFocus(nwWidget.EDITOR)) + self.aFocusEditor.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.EDITOR)) self.viewMenu.addAction(self.aFocusEditor) # View > Document Pane 2 @@ -395,7 +342,7 @@ class GuiMainMenu(QMenuBar): self.aFocusView.setShortcut("Ctrl+Alt+3") else: self.aFocusView.setShortcut("Alt+3") - self.aFocusView.triggered.connect(lambda: self.theParent.switchFocus(nwWidget.VIEWER)) + self.aFocusView.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.VIEWER)) self.viewMenu.addAction(self.aFocusView) # View > Outline @@ -404,7 +351,7 @@ class GuiMainMenu(QMenuBar): self.aFocusOutline.setShortcut("Ctrl+Alt+4") else: self.aFocusOutline.setShortcut("Alt+4") - self.aFocusOutline.triggered.connect(lambda: self.theParent.switchFocus(nwWidget.OUTLINE)) + self.aFocusOutline.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.OUTLINE)) self.viewMenu.addAction(self.aFocusOutline) # View > Separator @@ -413,13 +360,13 @@ class GuiMainMenu(QMenuBar): # View > Go Backward self.aViewPrev = QAction(self.tr("Navigate Backward"), self) self.aViewPrev.setShortcut("Alt+Left") - self.aViewPrev.triggered.connect(lambda: self.theParent.docViewer.navBackward()) + self.aViewPrev.triggered.connect(lambda: self.mainGui.docViewer.navBackward()) self.viewMenu.addAction(self.aViewPrev) # View > Go Forward self.aViewNext = QAction(self.tr("Navigate Forward"), self) self.aViewNext.setShortcut("Alt+Right") - self.aViewNext.triggered.connect(lambda: self.theParent.docViewer.navForward()) + self.aViewNext.triggered.connect(lambda: self.mainGui.docViewer.navForward()) self.viewMenu.addAction(self.aViewNext) # View > Separator @@ -429,14 +376,14 @@ class GuiMainMenu(QMenuBar): self.aFocusMode = QAction(self.tr("Focus Mode"), self) self.aFocusMode.setShortcut("F8") self.aFocusMode.setCheckable(True) - self.aFocusMode.setChecked(self.theParent.isFocusMode) - self.aFocusMode.triggered.connect(lambda: self.theParent.toggleFocusMode()) + self.aFocusMode.setChecked(self.mainGui.isFocusMode) + self.aFocusMode.triggered.connect(lambda: self.mainGui.toggleFocusMode()) self.viewMenu.addAction(self.aFocusMode) # View > Toggle Full Screen self.aFullScreen = QAction(self.tr("Full Screen Mode"), self) self.aFullScreen.setShortcut("F11") - self.aFullScreen.triggered.connect(lambda: self.theParent.toggleFullScreenMode()) + self.aFullScreen.triggered.connect(lambda: self.mainGui.toggleFullScreenMode()) self.viewMenu.addAction(self.aFullScreen) return @@ -641,7 +588,7 @@ class GuiMainMenu(QMenuBar): # Insert > Placeholder Text self.aLipsumText = QAction(self.tr("Placeholder Text"), self) - self.aLipsumText.triggered.connect(lambda: self.theParent.showLoremIpsumDialog()) + self.aLipsumText.triggered.connect(lambda: self.mainGui.showLoremIpsumDialog()) self.insertMenu.addAction(self.aLipsumText) return @@ -805,7 +752,7 @@ class GuiMainMenu(QMenuBar): # Search > Find self.aFind = QAction(self.tr("Find"), self) self.aFind.setShortcut("Ctrl+F") - self.aFind.triggered.connect(lambda: self.theParent.docEditor.beginSearch()) + self.aFind.triggered.connect(lambda: self.mainGui.docEditor.beginSearch()) self.srcMenu.addAction(self.aFind) # Search > Replace @@ -814,7 +761,7 @@ class GuiMainMenu(QMenuBar): self.aReplace.setShortcut("Ctrl+=") else: self.aReplace.setShortcut("Ctrl+H") - self.aReplace.triggered.connect(lambda: self.theParent.docEditor.beginReplace()) + self.aReplace.triggered.connect(lambda: self.mainGui.docEditor.beginReplace()) self.srcMenu.addAction(self.aReplace) # Search > Find Next @@ -823,7 +770,7 @@ class GuiMainMenu(QMenuBar): self.aFindNext.setShortcuts(["Ctrl+G", "F3"]) else: self.aFindNext.setShortcuts(["F3", "Ctrl+G"]) - self.aFindNext.triggered.connect(lambda: self.theParent.docEditor.findNext()) + self.aFindNext.triggered.connect(lambda: self.mainGui.docEditor.findNext()) self.srcMenu.addAction(self.aFindNext) # Search > Find Prev @@ -832,13 +779,13 @@ class GuiMainMenu(QMenuBar): self.aFindPrev.setShortcuts(["Ctrl+Shift+G", "Shift+F3"]) else: self.aFindPrev.setShortcuts(["Shift+F3", "Ctrl+Shift+G"]) - self.aFindPrev.triggered.connect(lambda: self.theParent.docEditor.findNext(goBack=True)) + self.aFindPrev.triggered.connect(lambda: self.mainGui.docEditor.findNext(goBack=True)) self.srcMenu.addAction(self.aFindPrev) # Search > Replace Next self.aReplaceNext = QAction(self.tr("Replace Next"), self) self.aReplaceNext.setShortcut("Ctrl+Shift+1") - self.aReplaceNext.triggered.connect(lambda: self.theParent.docEditor.replaceNext()) + self.aReplaceNext.triggered.connect(lambda: self.mainGui.docEditor.replaceNext()) self.srcMenu.addAction(self.aReplaceNext) return @@ -860,12 +807,12 @@ class GuiMainMenu(QMenuBar): # Tools > Re-Run Spell Check self.aReRunSpell = QAction(self.tr("Re-Run Spell Check"), self) self.aReRunSpell.setShortcut("F7") - self.aReRunSpell.triggered.connect(lambda: self.theParent.docEditor.spellCheckDocument()) + self.aReRunSpell.triggered.connect(lambda: self.mainGui.docEditor.spellCheckDocument()) self.toolsMenu.addAction(self.aReRunSpell) # Tools > Project Word List self.aEditWordList = QAction(self.tr("Project Word List"), self) - self.aEditWordList.triggered.connect(lambda: self.theParent.showProjectWordListDialog()) + self.aEditWordList.triggered.connect(lambda: self.mainGui.showProjectWordListDialog()) self.toolsMenu.addAction(self.aEditWordList) # Tools > Separator @@ -874,7 +821,7 @@ class GuiMainMenu(QMenuBar): # Tools > Rebuild Indices self.aRebuildIndex = QAction(self.tr("Rebuild Index"), self) self.aRebuildIndex.setShortcut("F9") - self.aRebuildIndex.triggered.connect(lambda: self.theParent.rebuildIndex()) + self.aRebuildIndex.triggered.connect(lambda: self.mainGui.rebuildIndex()) self.toolsMenu.addAction(self.aRebuildIndex) # Tools > Separator @@ -888,20 +835,20 @@ class GuiMainMenu(QMenuBar): # Tools > Export Project self.aBuildProject = QAction(self.tr("Build Novel Project"), self) self.aBuildProject.setShortcut("F5") - self.aBuildProject.triggered.connect(lambda: self.theParent.showBuildProjectDialog()) + self.aBuildProject.triggered.connect(lambda: self.mainGui.showBuildProjectDialog()) self.toolsMenu.addAction(self.aBuildProject) # Tools > Writing Stats self.aWritingStats = QAction(self.tr("Writing Statistics"), self) self.aWritingStats.setShortcut("F6") - self.aWritingStats.triggered.connect(lambda: self.theParent.showWritingStatsDialog()) + self.aWritingStats.triggered.connect(lambda: self.mainGui.showWritingStatsDialog()) self.toolsMenu.addAction(self.aWritingStats) # Tools > Settings self.aPreferences = QAction(self.tr("Preferences"), self) self.aPreferences.setShortcut("Ctrl+,") self.aPreferences.setMenuRole(QAction.PreferencesRole) - self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog()) + self.aPreferences.triggered.connect(lambda: self.mainGui.showPreferencesDialog()) self.toolsMenu.addAction(self.aPreferences) return @@ -915,13 +862,13 @@ class GuiMainMenu(QMenuBar): # Help > About self.aAboutNW = QAction(self.tr("About novelWriter"), self) self.aAboutNW.setMenuRole(QAction.AboutRole) - self.aAboutNW.triggered.connect(lambda: self.theParent.showAboutNWDialog()) + self.aAboutNW.triggered.connect(lambda: self.mainGui.showAboutNWDialog()) self.helpMenu.addAction(self.aAboutNW) # Help > About Qt5 self.aAboutQt = QAction(self.tr("About Qt5"), self) self.aAboutQt.setMenuRole(QAction.AboutQtRole) - self.aAboutQt.triggered.connect(lambda: self.theParent.showAboutQtDialog()) + self.aAboutQt.triggered.connect(lambda: self.mainGui.showAboutQtDialog()) self.helpMenu.addAction(self.aAboutQt) # Help > Separator @@ -968,7 +915,7 @@ class GuiMainMenu(QMenuBar): # Document > Check for Updates self.aUpdates = QAction(self.tr("Check for New Release"), self) - self.aUpdates.triggered.connect(lambda: self.theParent.showUpdatesDialog()) + self.aUpdates.triggered.connect(lambda: self.mainGui.showUpdatesDialog()) self.helpMenu.addAction(self.aUpdates) return diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 9ac2ea2b..8711dc8d 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -4,7 +4,9 @@ novelWriter – GUI Novel Tree GUI classe for the main window novel tree File History: -Created: 2020-12-20 [1.1a0] +Created: 2020-12-20 [1.1a0] GuiNovelTree +Created: 2022-06-12 [1.7b1] GuiNovelView +Created: 2022-06-12 [1.7b1] GuiNovelToolBar This file is a part of novelWriter Copyright 2018–2020, Veronica Berglyd Olsen @@ -26,84 +28,376 @@ along with this program. If not, see . import logging import novelwriter +from enum import Enum from time import time -from PyQt5.QtCore import Qt, QSize +from PyQt5.QtGui import QPalette +from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal from PyQt5.QtWidgets import ( - QTreeWidget, QTreeWidgetItem, QAbstractItemView, QFrame + QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel, + QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + QWidget ) +from novelwriter.enum import nwDocMode, nwItemClass from novelwriter.common import checkInt -from novelwriter.constants import nwKeyWords +from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst logger = logging.getLogger(__name__) +class NovelTreeColumn(Enum): + + HIDDEN = 0 + POV = 1 + FOCUS = 2 + PLOT = 3 + +# END Enum NovelTreeColumn + + +class GuiNovelView(QWidget): + + # Signals for user interaction with the novel tree + selectedItemChanged = pyqtSignal(str) + openDocumentRequest = pyqtSignal(str, Enum, int, str) + + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) + + self.mainGui = mainGui + self.theProject = mainGui.theProject + + # Build GUI + self.novelTree = GuiNovelTree(self) + self.novelBar = GuiNovelToolBar(self) + + # Assemble + self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.novelBar, 0) + self.outerBox.addWidget(self.novelTree, 1) + self.outerBox.setContentsMargins(0, 0, 0, 0) + self.outerBox.setSpacing(0) + + self.setLayout(self.outerBox) + + # Function Mappings + self.updateWordCounts = self.novelTree.updateWordCounts + self.getSelectedHandle = self.novelTree.getSelectedHandle + + return + + ## + # Methods + ## + + def initSettings(self): + self.novelTree.initSettings() + return + + def refreshTree(self): + """Refresh the current tree. + """ + self.novelTree.refreshTree(rootHandle=self.theProject.lastNovel) + return + + def clearProject(self): + """Clear project-related GUI content. + """ + self.novelTree.clearContent() + self.novelBar.clearContent() + return + + def openProjectTasks(self): + """Run opening project tasks. + """ + lastNovel = self.theProject.lastNovel + if lastNovel not in self.theProject.tree: + lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL) + + logger.debug("Setting novel tree to root item '%s'", lastNovel) + + lastCol = self.theProject.options.getEnum( + "GuiNovelView", "lastCol", NovelTreeColumn, NovelTreeColumn.HIDDEN + ) + + self.clearProject() + self.novelBar.buildNovelRootMenu() + self.novelBar.setLastColType(lastCol, doRefresh=False) + self.novelBar.setCurrentRoot(lastNovel) + + return + + def closeProjectTasks(self): + """Run closing project tasks. + """ + lastColType = self.novelTree.lastColType + self.theProject.options.setValue("GuiNovelView", "lastCol", lastColType) + return + + def setFocus(self): + """Forward the set focus call to the tree widget. + """ + self.novelTree.setFocus() + return + + def treeHasFocus(self): + """Check if the novel tree has focus. + """ + return self.novelTree.hasFocus() + + ## + # Public Slots + ## + + @pyqtSlot(str) + def updateRootItem(self, tHandle): + """If any root item changes, rebuild the novel root menu. + """ + self.novelBar.buildNovelRootMenu() + return + +# END Class GuiNovelView + + +class GuiNovelToolBar(QWidget): + + def __init__(self, novelView): + QTreeWidget.__init__(self, novelView) + + logger.debug("Initialising GuiNovelToolBar ...") + + self.mainConf = novelwriter.CONFIG + self.novelView = novelView + self.theProject = novelView.mainGui.theProject + self.mainTheme = novelView.mainGui.mainTheme + + iPx = self.mainTheme.baseIconSize + mPx = self.mainConf.pxInt(3) + + self.setContentsMargins(0, 0, 0, 0) + self.setAutoFillBackground(True) + + qPalette = self.palette() + qPalette.setBrush(QPalette.Window, qPalette.base()) + self.setPalette(qPalette) + + fadeCol = qPalette.text().color() + buttonStyle = ( + "QToolButton {{padding: {0}px; border: none; background: transparent;}} " + "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" + ).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue()) + + # Widget Label + self.viewLabel = QLabel("%s" % self.tr("Novel Outline")) + self.viewLabel.setContentsMargins(0, 0, 0, 0) + self.viewLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Refresh Button + self.tbRefresh = QToolButton(self) + self.tbRefresh.setToolTip(self.tr("Refresh")) + self.tbRefresh.setIcon(self.mainTheme.getIcon("refresh")) + self.tbRefresh.setIconSize(QSize(iPx, iPx)) + self.tbRefresh.setStyleSheet(buttonStyle) + self.tbRefresh.clicked.connect(self._refreshNovelTree) + + # Novel Root Menu + self.mRoot = QMenu() + self.gRoot = QActionGroup(self.mRoot) + self.aRoot = {} + + self.tbRoot = QToolButton(self) + self.tbRoot.setToolTip(self.tr("Novel Root")) + self.tbRoot.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])) + self.tbRoot.setIconSize(QSize(iPx, iPx)) + self.tbRoot.setStyleSheet(buttonStyle) + self.tbRoot.setMenu(self.mRoot) + self.tbRoot.setPopupMode(QToolButton.InstantPopup) + + # More Options Menu + self.mMore = QMenu() + + self.mLastCol = self.mMore.addMenu(self.tr("Last Column")) + self.gLastCol = QActionGroup(self.mMore) + self.aLastCol = {} + self._addLastColAction(NovelTreeColumn.HIDDEN, self.tr("Hidden")) + self._addLastColAction(NovelTreeColumn.POV, self.tr("Point of View Character")) + self._addLastColAction(NovelTreeColumn.FOCUS, self.tr("Focus Character")) + self._addLastColAction(NovelTreeColumn.PLOT, self.tr("Novel Plot")) + + self.tbMore = QToolButton(self) + self.tbMore.setToolTip(self.tr("More Options")) + self.tbMore.setIcon(self.mainTheme.getIcon("menu")) + self.tbMore.setIconSize(QSize(iPx, iPx)) + self.tbMore.setStyleSheet(buttonStyle) + self.tbMore.setMenu(self.mMore) + self.tbMore.setPopupMode(QToolButton.InstantPopup) + + # Assemble + self.outerBox = QHBoxLayout() + self.outerBox.addWidget(self.viewLabel) + self.outerBox.addWidget(self.tbRefresh) + self.outerBox.addWidget(self.tbRoot) + self.outerBox.addWidget(self.tbMore) + self.outerBox.setContentsMargins(mPx, mPx, 0, mPx) + self.outerBox.setSpacing(0) + + self.setLayout(self.outerBox) + + logger.debug("GuiNovelToolBar initialisation complete") + + return + + ## + # Methods + ## + + def clearContent(self): + """Run clearing project tasks. + """ + self.mRoot.clear() + self.aRoot = {} + return + + def buildNovelRootMenu(self): + """Build the novel root menu. + """ + self.mRoot.clear() + self.aRoot = {} + for n, (tHandle, nwItem) in enumerate(self.theProject.tree.iterRoots(nwItemClass.NOVEL)): + aRoot = self.mRoot.addAction(nwItem.itemName) + aRoot.setData(tHandle) + aRoot.setCheckable(True) + aRoot.triggered.connect(lambda n, tHandle=tHandle: self.setCurrentRoot(tHandle)) + self.gRoot.addAction(aRoot) + self.aRoot[tHandle] = aRoot + + return + + def setCurrentRoot(self, rootHandle): + """Set the current active root handle. + """ + if rootHandle in self.aRoot: + self.aRoot[rootHandle].setChecked(True) + self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) + return + + def setLastColType(self, colType, doRefresh=True): + """Set the last column type. + """ + self.aLastCol[colType].setChecked(True) + self.novelView.novelTree.setLastColType(colType, doRefresh=doRefresh) + return + + ## + # Private Slots + ## + + @pyqtSlot() + def _refreshNovelTree(self): + """Rebuild the current tree. + """ + rootHandle = self.theProject.lastNovel + self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) + return + + ## + # Internal Functions + ## + + def _addLastColAction(self, colType, actionLabel): + """Add a column selection entry to the last column menu. + """ + aLast = self.mLastCol.addAction(actionLabel) + aLast.setCheckable(True) + aLast.setActionGroup(self.gLastCol) + aLast.triggered.connect(lambda: self.setLastColType(colType)) + self.aLastCol[colType] = aLast + return + +# END Class GuiNovelToolBar + + class GuiNovelTree(QTreeWidget): C_TITLE = 0 C_WORDS = 1 - C_POV = 2 + C_LAST = 2 - def __init__(self, theParent): - QTreeWidget.__init__(self, theParent) + def __init__(self, novelView): + QTreeWidget.__init__(self, novelView) logger.debug("Initialising GuiNovelTree ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.novelView = novelView + self.mainGui = novelView.mainGui + self.mainTheme = novelView.mainGui.mainTheme + self.theProject = novelView.mainGui.theProject # Internal Variables self._treeMap = {} self._lastBuild = 0 + self._lastCol = NovelTreeColumn.POV + + # Cached Strings + self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]) + self._focLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]) + self._pltLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]) # Build GUI - iPx = self.theTheme.baseIconSize - self.setFrameStyle(QFrame.NoFrame) + # ========= + + iPx = self.mainTheme.baseIconSize + cMg = self.mainConf.pxInt(6) + self.setIconSize(QSize(iPx, iPx)) - self.setIndentation(iPx) + self.setFrameStyle(QFrame.NoFrame) + self.setHeaderHidden(True) + self.setIndentation(0) self.setColumnCount(3) - self.setHeaderLabels([ - self.tr("Novel Outline"), - self.tr("Words"), - self.tr("POV") - ]) - self.itemDoubleClicked.connect(self._treeDoubleClick) - self.itemSelectionChanged.connect(self._itemSelected) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setExpandsOnDoubleClick(False) self.setDragEnabled(False) - treeHeadItem = self.headerItem() - treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) - treeHeadItem.setToolTip(self.C_TITLE, self.tr("Section title")) - treeHeadItem.setToolTip(self.C_WORDS, self.tr("Word count")) - treeHeadItem.setToolTip(self.C_POV, self.tr("Point-of-view character")) - + # Lock the column sizes treeHeader = self.header() - treeHeader.setStretchLastSection(True) - treeHeader.setMinimumSectionSize(iPx + 6) + treeHeader.setStretchLastSection(False) + treeHeader.setMinimumSectionSize(iPx + cMg) + treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch) + treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents) + treeHeader.setSectionResizeMode(self.C_LAST, QHeaderView.ResizeToContents) - # Get user's column width preferences for NAME and COUNT - treeColWidth = self.mainConf.getNovelColWidths() - if len(treeColWidth) <= 3: - for colN, colW in enumerate(treeColWidth): - self.setColumnWidth(colN, colW) + # Pre-Generate Tree Formatting + fH1 = self.font() + fH1.setBold(True) + fH1.setUnderline(True) - # The last column should just auto-scale - self.resizeColumnToContents(self.C_POV) + fH2 = self.font() + fH2.setBold(True) + + self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()] + self._pIndent = [ + self.mainTheme.loadDecoration("deco_doc_h0", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h1", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h2", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h3", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx), + ] + + # Connect signals + self.itemDoubleClicked.connect(self._treeDoubleClick) + self.itemSelectionChanged.connect(self._treeSelectionChange) # Set custom settings - self.initTree() + self.initSettings() logger.debug("GuiNovelTree initialisation complete") return - def initTree(self): + def initSettings(self): """Set or update tree widget settings. """ # Scroll bars @@ -119,11 +413,19 @@ class GuiNovelTree(QTreeWidget): return + ## + # Properties + ## + + @property + def lastColType(self): + return self._lastCol + ## # Class Methods ## - def clearTree(self): + def clearContent(self): """Clear the GUI content and the related maps. """ self.clear() @@ -131,12 +433,15 @@ class GuiNovelTree(QTreeWidget): self._lastBuild = 0 return - def refreshTree(self, overRide=False): + def refreshTree(self, rootHandle=None, overRide=False): """Called whenever the Novel tab is activated. """ logger.verbose("Requesting refresh of the novel tree") - treeChanged = self.theParent.treeView.changedSince(self._lastBuild) - indexChanged = self.theProject.index.indexChangedSince(self._lastBuild) + if rootHandle is None: + rootHandle = self.theProject.tree.findRoot(nwItemClass.NOVEL) + + treeChanged = self.mainGui.projView.changedSince(self._lastBuild) + indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild) if not (treeChanged or indexChanged or overRide): logger.verbose("No changes have been made to the novel index") return @@ -146,7 +451,8 @@ class GuiNovelTree(QTreeWidget): if selItem: titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2] - self._populateTree() + self._populateTree(rootHandle) + self.theProject.setLastNovelViewed(rootHandle) if titleKey is not None and titleKey in self._treeMap: self._treeMap[titleKey].setSelected(True) @@ -162,15 +468,6 @@ class GuiNovelTree(QTreeWidget): self._treeMap[titleKey].setText(self.C_WORDS, f"{wCount:n}") return - def getColumnSizes(self): - """Return the column widths for the tree columns. - """ - retVals = [ - self.columnWidth(0), - self.columnWidth(1), - ] - return retVals - def getSelectedHandle(self): """Get the currently selected handle. If multiple items are selected, return the first. @@ -184,6 +481,17 @@ class GuiNovelTree(QTreeWidget): return tHandle, tLine + def setLastColType(self, colType, doRefresh=True): + """Change the content type of the last column and rebuild. + """ + if self._lastCol != colType: + logger.debug("Changing last column to %s", colType.name) + self._lastCol = colType + self.setColumnHidden(self.C_LAST, colType == NovelTreeColumn.HIDDEN) + if doRefresh: + self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True) + return + ## # Events ## @@ -209,113 +517,94 @@ class GuiNovelTree(QTreeWidget): if tHandle is None: return - self.theParent.viewDocument(tHandle) + self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") return ## - # Slots + # Private Slots ## - def _treeDoubleClick(self, tItem, tCol): + @pyqtSlot() + def _treeSelectionChange(self): + """Extract the handle and line number of the currently selected + title, and send it to the tree meta panel. + """ + tHandle, _ = self.getSelectedHandle() + if tHandle is not None: + self.novelView.selectedItemChanged.emit(tHandle) + return + + @pyqtSlot("QTreeWidgetItem*", int) + def _treeDoubleClick(self, tItem, colNo): """Extract the handle and line number of the title double- clicked, and send it to the main gui class for opening in the document editor. """ tHandle, tLine = self.getSelectedHandle() - self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True) - return - - def _itemSelected(self): - """Extract the handle and line number of the currently selected - title, and send it to the tree meta panel. - """ - selItems = self.selectedItems() - if selItems: - tHandle = selItems[0].data(self.C_TITLE, Qt.UserRole)[0] - self.theParent.treeMeta.updateViewBox(tHandle) - + self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, tLine, "") return ## # Internal Functions ## - def _populateTree(self): + def _populateTree(self, rootHandle): """Build the tree based on the project index. """ - self.clearTree() + self.clearContent() + tStart = time() + logger.verbose("Building novel tree for root item '%s'", rootHandle) - currTitle = None - currChapter = None - currScene = None + novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True) + for tKey, tHandle, sTitle, novIdx in novStruct: - for tKey, tHandle, sTitle, novIdx in self.theProject.index.novelStructure(skipExcl=True): + iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0) + if iLevel == 0: + continue - tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx) - self._treeMap[tKey] = tItem + newItem = QTreeWidgetItem() + theData = (tHandle, sTitle[1:].lstrip("0"), tKey) - tLevel = novIdx.level - if tLevel == "H1": - self.addTopLevelItem(tItem) - currTitle = tItem - currChapter = None - currScene = None + newItem.setData(self.C_TITLE, Qt.DecorationRole, self._pIndent[iLevel]) + newItem.setText(self.C_TITLE, novIdx.title) + newItem.setData(self.C_TITLE, Qt.UserRole, theData) + newItem.setFont(self.C_TITLE, self._hFonts[iLevel]) + newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}") + newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) - elif tLevel == "H2": - if currTitle is None: - self.addTopLevelItem(tItem) - else: - currTitle.addChild(tItem) - currChapter = tItem - currScene = None + lastText, toolTip = self._getLastColumnText(tHandle, sTitle) + newItem.setText(self.C_LAST, lastText) + if lastText: + newItem.setToolTip(self.C_LAST, toolTip) - elif tLevel == "H3": - if currChapter is None: - if currTitle is None: - self.addTopLevelItem(tItem) - else: - currTitle.addChild(tItem) - else: - currChapter.addChild(tItem) - currScene = tItem - - elif tLevel == "H4": - if currScene is None: - if currChapter is None: - if currTitle is None: - self.addTopLevelItem(tItem) - else: - currTitle.addChild(tItem) - else: - currChapter.addChild(tItem) - else: - currScene.addChild(tItem) - - tItem.setExpanded(True) + self._treeMap[tKey] = newItem + self.addTopLevelItem(newItem) + logger.verbose("Novel Tree built in %.3f ms", (time() - tStart)*1000) self._lastBuild = time() return - def _createTreeItem(self, tHandle, sTitle, titleKey, novIdx): - """Populate a tree item with all the column values. + def _getLastColumnText(self, tHandle, sTitle): + """Generate the text for the last column based on user settings. """ - newItem = QTreeWidgetItem() - hIcon = "doc_%s" % novIdx.level.lower() - theData = (tHandle, sTitle[1:].lstrip("0"), titleKey) - - wC = int(novIdx.wordCount) - - newItem.setText(self.C_TITLE, novIdx.title) - newItem.setData(self.C_TITLE, Qt.UserRole, theData) - newItem.setIcon(self.C_TITLE, self.theTheme.getIcon(hIcon)) - newItem.setText(self.C_WORDS, f"{wC:n}") - newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) + if self._lastCol == NovelTreeColumn.HIDDEN: + return "", "" theRefs = self.theProject.index.getReferences(tHandle, sTitle) - newItem.setText(self.C_POV, ", ".join(theRefs[nwKeyWords.POV_KEY])) + if self._lastCol == NovelTreeColumn.POV: + newText = ", ".join(theRefs[nwKeyWords.POV_KEY]) + return newText, f"{self._povLabel}: {newText}" - return newItem + elif self._lastCol == NovelTreeColumn.FOCUS: + newText = ", ".join(theRefs[nwKeyWords.FOCUS_KEY]) + return newText, f"{self._focLabel}: {newText}" + + elif self._lastCol == NovelTreeColumn.PLOT: + newText = ", ".join(theRefs[nwKeyWords.PLOT_KEY]) + return newText, f"{self._pltLabel}: {newText}" + + return "", "" # END Class GuiNovelTree diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 34cb35da..4b2077f6 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -4,9 +4,9 @@ novelWriter – GUI Project Outline GUI class for the project outline view File History: -Created: 2022-05-15 [1.7b1] GuiOutline +Created: 2022-05-15 [1.7b1] GuiOutlineView Created: 2022-05-22 [1.7b1] GuiOutlineToolBar -Created: 2019-11-16 [0.4.1] GuiOutlineView +Created: 2019-11-16 [0.4.1] GuiOutlineTree Created: 2019-11-16 [0.4.1] GuiOutlineHeaderMenu Created: 2020-06-02 [0.7.0] GuiOutlineDetails @@ -52,22 +52,23 @@ from novelwriter.constants import trConst, nwKeyWords, nwLabels logger = logging.getLogger(__name__) -class GuiOutline(QWidget): +class GuiOutlineView(QWidget): loadDocumentTagRequest = pyqtSignal(str, Enum) - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) - self.mainConf = novelwriter.CONFIG - self.theParent = theParent + self.mainConf = novelwriter.CONFIG + self.mainGui = mainGui + # Build GUI self.outlineBar = GuiOutlineToolBar(self) - self.outlineView = GuiOutlineView(self) + self.outlineTree = GuiOutlineTree(self) self.outlineData = GuiOutlineDetails(self) self.splitOutline = QSplitter(Qt.Vertical) - self.splitOutline.addWidget(self.outlineView) + self.splitOutline.addWidget(self.outlineTree) self.splitOutline.addWidget(self.outlineData) self.splitOutline.setSizes(self.mainConf.getOutlinePanePos()) @@ -80,14 +81,14 @@ class GuiOutline(QWidget): self.setLayout(self.outerBox) # Connect Signals - self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns) - self.outlineView.activeItemChanged.connect(self.outlineData.showItem) + self.outlineTree.hiddenStateChanged.connect(self._updateMenuColumns) + self.outlineTree.activeItemChanged.connect(self.outlineData.showItem) self.outlineData.itemTagClicked.connect(self._tagClicked) self.outlineBar.loadNovelRootRequest.connect(self._rootItemChanged) - self.outlineBar.viewColumnToggled.connect(self.outlineView.menuColumnToggled) + self.outlineBar.viewColumnToggled.connect(self.outlineTree.menuColumnToggled) # Function Mappings - self.getSelectedHandle = self.outlineView.getSelectedHandle + self.getSelectedHandle = self.outlineTree.getSelectedHandle return @@ -103,24 +104,24 @@ class GuiOutline(QWidget): return def initOutline(self): - self.outlineView.initOutline() + self.outlineTree.initOutline() self.outlineData.initDetails() return def closeOutline(self): - self.outlineView.closeOutline() + self.outlineTree.closeOutline() self.outlineData.updateClasses() return def refreshView(self, overRide=False, novelChanged=False): - self.outlineView.refreshTree(overRide=overRide, novelChanged=novelChanged) + self.outlineTree.refreshTree(overRide=overRide, novelChanged=novelChanged) return - def treeFocus(self): - return self.outlineView.hasFocus() + def treeHasFocus(self): + return self.outlineTree.hasFocus() def setTreeFocus(self): - return self.outlineView.setFocus() + return self.outlineTree.setFocus() ## # Public Slots @@ -144,7 +145,7 @@ class GuiOutline(QWidget): checkboxes whenever a signal is received that the hidden state of columns has changed. """ - self.outlineBar.setColumnHiddenState(self.outlineView.hiddenColumns) + self.outlineBar.setColumnHiddenState(self.outlineTree.hiddenColumns) return @pyqtSlot(str) @@ -159,10 +160,10 @@ class GuiOutline(QWidget): def _rootItemChanged(self, handle): """The root novel handle has changed or needs to be refreshed. """ - self.outlineView.refreshTree(rootHandle=(handle or None), overRide=True) + self.outlineTree.refreshTree(rootHandle=(handle or None), overRide=True) return -# END Class GuiOutline +# END Class GuiOutlineView class GuiOutlineToolBar(QToolBar): @@ -176,9 +177,9 @@ class GuiOutlineToolBar(QToolBar): logger.debug("Initialising GuiOutlineToolBar ...") self.mainConf = novelwriter.CONFIG - self.theParent = theOutline.theParent - self.theProject = theOutline.theParent.theProject - self.theTheme = theOutline.theParent.theTheme + self.mainGui = theOutline.mainGui + self.theProject = theOutline.mainGui.theProject + self.mainTheme = theOutline.mainGui.mainTheme iPx = self.mainConf.pxInt(22) mPx = self.mainConf.pxInt(12) @@ -201,7 +202,7 @@ class GuiOutlineToolBar(QToolBar): # Actions self.aRefresh = QAction(self.tr("Refresh"), self) - self.aRefresh.setIcon(self.theTheme.getIcon("refresh")) + self.aRefresh.setIcon(self.mainTheme.getIcon("refresh")) self.aRefresh.triggered.connect(self._refreshRequested) # Column Menu @@ -211,7 +212,7 @@ class GuiOutlineToolBar(QToolBar): ) self.tbColumns = QToolButton(self) - self.tbColumns.setIcon(self.theTheme.getIcon("menu")) + self.tbColumns.setIcon(self.mainTheme.getIcon("menu")) self.tbColumns.setMenu(self.mColumns) self.tbColumns.setPopupMode(QToolButton.InstantPopup) @@ -225,6 +226,8 @@ class GuiOutlineToolBar(QToolBar): logger.debug("GuiOutlineToolBar initialisation complete") + return + ## # Methods ## @@ -233,7 +236,7 @@ class GuiOutlineToolBar(QToolBar): """Fill the novel combo box with a list of all novel folders. """ self.novelValue.clear() - tIcon = self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) + tIcon = self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) for tHandle, nwItem in self.theProject.tree.iterRoots(nwItemClass.NOVEL): self.novelValue.addItem(tIcon, nwItem.itemName, tHandle) self.novelValue.insertSeparator(self.novelValue.count()) @@ -268,7 +271,7 @@ class GuiOutlineToolBar(QToolBar): # END Class GuiOutlineToolBar -class GuiOutlineView(QTreeWidget): +class GuiOutlineTree(QTreeWidget): DEF_WIDTH = { nwOutline.TITLE: 200, @@ -316,12 +319,12 @@ class GuiOutlineView(QTreeWidget): def __init__(self, theOutline): QTreeWidget.__init__(self, theOutline) - logger.debug("Initialising GuiOutlineView ...") + logger.debug("Initialising GuiOutlineTree ...") self.mainConf = novelwriter.CONFIG - self.theParent = theOutline.theParent - self.theProject = theOutline.theParent.theProject - self.theTheme = theOutline.theParent.theTheme + self.mainGui = theOutline.mainGui + self.theProject = theOutline.mainGui.theProject + self.mainTheme = theOutline.mainGui.mainTheme self.setFrameStyle(QFrame.NoFrame) self.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -331,7 +334,7 @@ class GuiOutlineView(QTreeWidget): self.itemDoubleClicked.connect(self._treeDoubleClick) self.itemSelectionChanged.connect(self._itemSelected) - iPx = self.theTheme.baseIconSize + iPx = self.mainTheme.baseIconSize self.setIconSize(QSize(iPx, iPx)) self.setIndentation(iPx) @@ -352,7 +355,7 @@ class GuiOutlineView(QTreeWidget): self.hiddenStateChanged.emit() - logger.debug("GuiOutlineView initialisation complete") + logger.debug("GuiOutlineTree initialisation complete") return @@ -461,7 +464,7 @@ class GuiOutlineView(QTreeWidget): document editor. """ tHandle, tLine = self.getSelectedHandle() - self.theParent.openDocument(tHandle, tLine=tLine - 1, doScroll=True) + self.mainGui.openDocument(tHandle, tLine=tLine - 1, doScroll=True) return @pyqtSlot() @@ -679,7 +682,7 @@ class GuiOutlineView(QTreeWidget): hIcon = "doc_%s" % novIdx.level.lower() hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) - dIcon = self.theTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, hLevel) + dIcon = self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, hLevel) cC = int(novIdx.charCount) wC = int(novIdx.wordCount) @@ -687,7 +690,7 @@ class GuiOutlineView(QTreeWidget): newItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title) newItem.setData(self._colIdx[nwOutline.TITLE], Qt.UserRole, tHandle) - newItem.setIcon(self._colIdx[nwOutline.TITLE], self.theTheme.getIcon(hIcon)) + newItem.setIcon(self._colIdx[nwOutline.TITLE], self.mainTheme.getIcon(hIcon)) newItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level) newItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName) newItem.setIcon(self._colIdx[nwOutline.LABEL], dIcon) @@ -714,7 +717,7 @@ class GuiOutlineView(QTreeWidget): return newItem -# END Class GuiOutlineView +# END Class GuiOutlineTree class GuiOutlineHeaderMenu(QMenu): @@ -779,14 +782,14 @@ class GuiOutlineDetails(QScrollArea): self.mainConf = novelwriter.CONFIG self.theOutline = theOutline - self.theParent = theOutline.theParent - self.theProject = theOutline.theParent.theProject - self.theTheme = theOutline.theParent.theTheme + self.mainGui = theOutline.mainGui + self.theProject = theOutline.mainGui.theProject + self.mainTheme = theOutline.mainGui.mainTheme # Sizes - minTitle = 30*self.theTheme.textNWidth - maxTitle = 40*self.theTheme.textNWidth - wCount = self.theTheme.getTextWidth("999,999") + minTitle = 30*self.mainTheme.textNWidth + maxTitle = 40*self.mainTheme.textNWidth + wCount = self.mainTheme.getTextWidth("999,999") hSpace = int(self.mainConf.pxInt(10)) vSpace = int(self.mainConf.pxInt(4)) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index e0a8ef62..08b24232 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -5,7 +5,8 @@ GUI classes for the main window project tree File History: Created: 2018-09-29 [0.0.1] GuiProjectTree -Created: 2020-06-04 [0.7] GuiProjectTreeMenu +Created: 2022-06-06 [1.7b1] GuiProjectView +Created: 2022-06-06 [1.7b1] GuiProjectToolBar This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -27,23 +28,292 @@ along with this program. If not, see . import logging import novelwriter +from enum import Enum from time import time +from PyQt5.QtGui import QPalette from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( - QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame, - QDialog + QAbstractItemView, QFrame, QHBoxLayout, QHeaderView, QLabel, + QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, + QVBoxLayout, QWidget ) from novelwriter.core import NWDoc -from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.common import minmax -from novelwriter.dialogs.itemeditor import GuiItemEditor +from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert +from novelwriter.constants import trConst, nwLabels +from novelwriter.dialogs.editlabel import GuiEditLabel logger = logging.getLogger(__name__) +class GuiProjectView(QWidget): + """This is a wrapper class holding all the elements of the project + tree. The core object is the project tree itself. Most methods + available are mapped through to the project tree class. + """ + + # Signals triggered when the meta data values of items change + treeItemChanged = pyqtSignal(str) + novelItemChanged = pyqtSignal(str) + rootFolderChanged = pyqtSignal(str) + wordCountsChanged = pyqtSignal() + + # Signals for user interaction with the project tree + selectedItemChanged = pyqtSignal(str) + openDocumentRequest = pyqtSignal(str, Enum, int, str) + + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) + + self.mainGui = mainGui + + # Build GUI + self.projTree = GuiProjectTree(self) + self.projBar = GuiProjectToolBar(self) + + # Assemble + self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.projBar, 0) + self.outerBox.addWidget(self.projTree, 1) + self.outerBox.setContentsMargins(0, 0, 0, 0) + self.outerBox.setSpacing(0) + + self.setLayout(self.outerBox) + + # Keyboard Shortcuts + self.keyMoveUp = QShortcut(self.projTree) + self.keyMoveUp.setKey("Ctrl+Up") + self.keyMoveUp.setContext(Qt.WidgetShortcut) + self.keyMoveUp.activated.connect(lambda: self.projTree.moveTreeItem(-1)) + + self.keyMoveDn = QShortcut(self.projTree) + self.keyMoveDn.setKey("Ctrl+Down") + self.keyMoveDn.setContext(Qt.WidgetShortcut) + self.keyMoveDn.activated.connect(lambda: self.projTree.moveTreeItem(1)) + + self.keyUndoMv = QShortcut(self.projTree) + self.keyUndoMv.setKey("Ctrl+Shift+Z") + self.keyUndoMv.setContext(Qt.WidgetShortcut) + self.keyUndoMv.activated.connect(lambda: self.projTree.undoLastMove()) + + self.keyContext = QShortcut(self.projTree) + self.keyContext.setKey("Ctrl+.") + self.keyContext.setContext(Qt.WidgetShortcut) + self.keyContext.activated.connect(lambda: self.projTree.openContextOnSelected()) + + # Function Mappings + self.revealNewTreeItem = self.projTree.revealNewTreeItem + self.renameTreeItem = self.projTree.renameTreeItem + self.getTreeFromHandle = self.projTree.getTreeFromHandle + self.emptyTrash = self.projTree.emptyTrash + self.deleteItem = self.projTree.deleteItem + self.setTreeItemValues = self.projTree.setTreeItemValues + self.propagateCount = self.projTree.propagateCount + self.getSelectedHandle = self.projTree.getSelectedHandle + self.setSelectedHandle = self.projTree.setSelectedHandle + self.changedSince = self.projTree.changedSince + + return + + ## + # Methods + ## + + def initSettings(self): + self.projTree.initSettings() + return + + def clearProject(self): + self.projTree.clearTree() + return + + def saveProjectTree(self): + self.projTree.saveTreeOrder() + return + + def populateTree(self): + self.projTree.buildTree() + return + + def setFocus(self): + """Forward the set focus call to the tree widget. + """ + self.projTree.setFocus() + return + + def treeHasFocus(self): + """Check if the project tree has focus. + """ + return self.projTree.hasFocus() + + ## + # Public Slots + ## + + @pyqtSlot(str, int, int, int) + def updateCounts(self, tHandle, cCount, wCount, pCount): + """Slot for updating the word count of a specific item. + """ + self.projTree.propagateCount(tHandle, wCount, countChildren=True) + self.wordCountsChanged.emit() + return + +# END Class GuiProjectView + + +class GuiProjectToolBar(QWidget): + + def __init__(self, projView): + QTreeWidget.__init__(self, projView) + + logger.debug("Initialising GuiProjectToolBar ...") + + self.mainConf = novelwriter.CONFIG + self.projView = projView + self.projTree = projView.projTree + self.mainGui = projView.mainGui + self.theProject = projView.mainGui.theProject + self.mainTheme = projView.mainGui.mainTheme + + iPx = self.mainTheme.baseIconSize + mPx = self.mainConf.pxInt(3) + + self.setContentsMargins(0, 0, 0, 0) + self.setAutoFillBackground(True) + + qPalette = self.palette() + qPalette.setBrush(QPalette.Window, qPalette.base()) + self.setPalette(qPalette) + + fadeCol = qPalette.text().color() + buttonStyle = ( + "QToolButton {{padding: {0}px; border: none; background: transparent;}} " + "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" + ).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue()) + + # Widget Label + self.viewLabel = QLabel("%s" % self.tr("Project Content")) + self.viewLabel.setContentsMargins(0, 0, 0, 0) + self.viewLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Move Buttons + self.tbMoveU = QToolButton(self) + self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up")) + self.tbMoveU.setIcon(self.mainTheme.getIcon("up")) + self.tbMoveU.setIconSize(QSize(iPx, iPx)) + self.tbMoveU.setStyleSheet(buttonStyle) + self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1)) + + self.tbMoveD = QToolButton(self) + self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down")) + self.tbMoveD.setIcon(self.mainTheme.getIcon("down")) + self.tbMoveD.setIconSize(QSize(iPx, iPx)) + self.tbMoveD.setStyleSheet(buttonStyle) + self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1)) + + # Add Item Menu + self.mAdd = QMenu() + + self.aAddEmpty = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["document"])) + self.aAddEmpty.setIcon(self.mainTheme.getIcon("proj_document")) + self.aAddEmpty.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) + ) + + self.aAddChap = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h2"])) + self.aAddChap.setIcon(self.mainTheme.getIcon("proj_chapter")) + self.aAddChap.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) + ) + + self.aAddScene = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h3"])) + self.aAddScene.setIcon(self.mainTheme.getIcon("proj_scene")) + self.aAddScene.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) + ) + + self.aAddNote = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["note"])) + self.aAddNote.setIcon(self.mainTheme.getIcon("proj_note")) + self.aAddNote.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) + ) + + self.aAddFolder = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["folder"])) + self.aAddFolder.setIcon(self.mainTheme.getIcon("proj_folder")) + self.aAddFolder.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FOLDER) + ) + + self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"])) + self._addRootFolderEntry(nwItemClass.NOVEL) + self._addRootFolderEntry(nwItemClass.ARCHIVE) + self.mAddRoot.addSeparator() + self._addRootFolderEntry(nwItemClass.PLOT) + self._addRootFolderEntry(nwItemClass.CHARACTER) + self._addRootFolderEntry(nwItemClass.WORLD) + self._addRootFolderEntry(nwItemClass.TIMELINE) + self._addRootFolderEntry(nwItemClass.OBJECT) + self._addRootFolderEntry(nwItemClass.ENTITY) + self._addRootFolderEntry(nwItemClass.CUSTOM) + + self.tbAdd = QToolButton(self) + self.tbAdd.setToolTip("%s [Ctrl+N]" % self.tr("Add Item")) + self.tbAdd.setShortcut("Ctrl+N") + self.tbAdd.setIcon(self.mainTheme.getIcon("add")) + self.tbAdd.setIconSize(QSize(iPx, iPx)) + self.tbAdd.setStyleSheet(buttonStyle) + self.tbAdd.setMenu(self.mAdd) + self.tbAdd.setPopupMode(QToolButton.InstantPopup) + + # More Options Menu + self.mMore = QMenu() + + self.aMoreUndo = self.mMore.addAction(self.tr("Undo Move")) + self.aMoreUndo.triggered.connect(lambda: self.projTree.undoLastMove()) + + self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash")) + self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash()) + + self.tbMore = QToolButton(self) + self.tbMore.setToolTip(self.tr("More Options")) + self.tbMore.setIcon(self.mainTheme.getIcon("menu")) + self.tbMore.setIconSize(QSize(iPx, iPx)) + self.tbMore.setStyleSheet(buttonStyle) + self.tbMore.setMenu(self.mMore) + self.tbMore.setPopupMode(QToolButton.InstantPopup) + + # Assemble + self.outerBox = QHBoxLayout() + self.outerBox.addWidget(self.viewLabel) + self.outerBox.addWidget(self.tbMoveU) + self.outerBox.addWidget(self.tbMoveD) + self.outerBox.addWidget(self.tbAdd) + self.outerBox.addWidget(self.tbMore) + self.outerBox.setContentsMargins(mPx, mPx, 0, mPx) + self.outerBox.setSpacing(0) + + self.setLayout(self.outerBox) + + logger.debug("GuiProjectToolBar initialisation complete") + + return + + ## + # Internal Functions + ## + + def _addRootFolderEntry(self, itemClass): + """Add a menu entry for a root folder of a given class. + """ + aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass])) + aNew.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[itemClass])) + aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass)) + self.mAddRoot.addAction(aNew) + +# END Class GuiProjectToolBar + + class GuiProjectTree(QTreeWidget): C_NAME = 0 @@ -51,59 +321,50 @@ class GuiProjectTree(QTreeWidget): C_EXPORT = 2 C_STATUS = 3 - treeItemChanged = pyqtSignal(str) - novelItemChanged = pyqtSignal(str) - rootFolderChanged = pyqtSignal(str) - wordCountsChanged = pyqtSignal() - - def __init__(self, theParent): - QTreeWidget.__init__(self, theParent) + def __init__(self, projView): + QTreeWidget.__init__(self, projView) logger.debug("Initialising GuiProjectTree ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.projView = projView + self.mainGui = projView.mainGui + self.mainTheme = projView.mainGui.mainTheme + self.theProject = projView.mainGui.theProject # Internal Variables self._treeMap = {} self._lastMove = {} self._timeChanged = 0 - ## - # Build GUI - ## + # Build GUI + # ========= # Context Menu - self.ctxMenu = GuiProjectTreeMenu(self) self.setContextMenuPolicy(Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self._rightClickMenu) + self.customContextMenuRequested.connect(self._openContextMenu) # Tree Settings - iPx = self.theTheme.baseIconSize + iPx = self.mainTheme.baseIconSize + cMg = self.mainConf.pxInt(6) + self.setIconSize(QSize(iPx, iPx)) self.setFrameStyle(QFrame.NoFrame) self.setExpandsOnDoubleClick(False) + self.setHeaderHidden(True) self.setIndentation(iPx) self.setColumnCount(4) - self.setHeaderLabels([ - self.tr("Project Tree"), self.tr("Words"), "", "" - ]) - treeHeadItem = self.headerItem() - treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) - treeHeadItem.setToolTip(self.C_NAME, self.tr("Item label")) - treeHeadItem.setToolTip(self.C_COUNT, self.tr("Word count")) - treeHeadItem.setToolTip(self.C_EXPORT, self.tr("Include in build")) - treeHeadItem.setToolTip(self.C_STATUS, self.tr("Item status")) - - # Let the last column stretch, and set the minimum size to the - # size of the icon as the default Qt font metrics approach fails - # for some fonts like the Ubuntu font. + # Lock the column sizes treeHeader = self.header() - treeHeader.setStretchLastSection(True) - treeHeader.setMinimumSectionSize(iPx + 6) + treeHeader.setStretchLastSection(False) + treeHeader.setMinimumSectionSize(iPx + cMg) + treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.Stretch) + treeHeader.setSectionResizeMode(self.C_COUNT, QHeaderView.ResizeToContents) + treeHeader.setSectionResizeMode(self.C_EXPORT, QHeaderView.Fixed) + treeHeader.setSectionResizeMode(self.C_STATUS, QHeaderView.Fixed) + treeHeader.resizeSection(self.C_EXPORT, iPx + cMg) + treeHeader.resizeSection(self.C_STATUS, iPx + cMg) # Allow Move by Drag & Drop self.setDragEnabled(True) @@ -118,23 +379,18 @@ class GuiProjectTree(QTreeWidget): # self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) - # Get user's column width preferences for NAME and COUNT - treeColWidth = self.mainConf.getTreeColWidths() - if len(treeColWidth) <= 4: - for colN, colW in enumerate(treeColWidth): - self.setColumnWidth(colN, colW) - - # The last column should just auto-scale - self.resizeColumnToContents(self.C_STATUS) + # Connect signals + self.itemDoubleClicked.connect(self._treeDoubleClick) + self.itemSelectionChanged.connect(self._treeSelectionChange) # Set custom settings - self.initTree() + self.initSettings() logger.debug("GuiProjectTree initialisation complete") return - def initTree(self): + def initSettings(self): """Set or update tree widget settings. """ # Scroll bars @@ -163,13 +419,13 @@ class GuiProjectTree(QTreeWidget): self._timeChanged = 0 return - def newTreeItem(self, itemType, itemClass=None): + def newTreeItem(self, itemType, itemClass=None, hLevel=1, isNote=False): """Add new item to the tree, with a given itemType (and itemClass if Root), and attach it to the selected handle. Also make sure the item is added in a place it can be added, and that other meta data is set correctly to ensure a valid project tree. """ - if not self.theParent.hasProject: + if not self.mainGui.hasProject: logger.error("No project open") return False @@ -184,7 +440,7 @@ class GuiProjectTree(QTreeWidget): sHandle = self.getSelectedHandle() if sHandle is None or sHandle not in self.theProject.tree: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Did not find anywhere to add the file or folder!" ), nwAlert.ERROR) return False @@ -201,60 +457,54 @@ class GuiProjectTree(QTreeWidget): return False if self.theProject.tree.isTrash(sHandle): - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot add new files or folders to the Trash folder." ), nwAlert.ERROR) return False + # Ask for label + if itemType == nwItemType.FILE: + if isNote: + newLabel = self.tr("New Note") + elif hLevel == 2: + newLabel = self.tr("New Chapter") + elif hLevel == 3: + newLabel = self.tr("New Scene") + else: + newLabel = self.tr("New Document") + else: + newLabel = self.tr("New Folder") + + newLabel, dlgOk = GuiEditLabel.getLabel(self, text=newLabel) + if not dlgOk: + logger.info("New item creation cancelled by user") + return False + # Add the file or folder if itemType == nwItemType.FILE: - if pItem.isNovelLike(): - tHandle = self.theProject.newFile(self.tr("New Document"), sHandle) - else: - tHandle = self.theProject.newFile(self.tr("New Note"), sHandle) - elif itemType == nwItemType.FOLDER: - tHandle = self.theProject.newFolder(self.tr("New Folder"), sHandle) + tHandle = self.theProject.newFile(newLabel, sHandle) + else: + tHandle = self.theProject.newFolder(newLabel, sHandle) else: logger.error("Failed to add new item") return False - # If there is no handle set, return here. This is a bug + # If there is no handle set, return here. This is a bug. if tHandle is None: # pragma: no cover + logger.error("Internal error") return True - # Add the new item to the tree - self.revealNewTreeItem(tHandle, nHandle) - self.theParent.editItem(tHandle) - # Handle new file creation - nwItem = self.theProject.tree[tHandle] - if nwItem.itemType != nwItemType.FILE: - return True + if itemType == nwItemType.FILE and hLevel > 0: + if self.theProject.writeNewFile(tHandle, hLevel, not isNote): + # If successful, update word count + wC = self.theProject.index.getCounts(tHandle)[1] + self.propagateCount(tHandle, wC) + self.projView.wordCountsChanged.emit() - # This is a new file, so let's add some content - newDoc = NWDoc(self.theProject, tHandle) - if not newDoc.readDocument(): - if nwItem.itemLayout == nwItemLayout.DOCUMENT: - iLvl = self.theProject.index.getHandleHeaderIntLevel(sHandle) - hLvl = "#"*minmax(iLvl + 1, 2, 4) - newText = f"{hLvl} {nwItem.itemName}\n\n" - else: - newText = f"# {nwItem.itemName}\n\n" - - pIndex = self.theProject.index - - # Save the text and index it - newDoc.writeDocument(newText) - pIndex.scanText(tHandle, newText) - - # Get Word Counts - cC, wC, pC = pIndex.getCounts(tHandle) - nwItem.setCharCount(cC) - nwItem.setWordCount(wC) - nwItem.setParaCount(pC) - self.propagateCount(tHandle, wC) - self.wordCountsChanged.emit() + # Add the new item to the project tree + self.revealNewTreeItem(tHandle, nHandle) return True @@ -280,68 +530,60 @@ class GuiProjectTree(QTreeWidget): return True def moveTreeItem(self, nStep): - """Move an item up or down in the tree, but only if the treeView - has focus. This also applies when the menu is used. + """Move an item up or down in the tree. """ - if not self.theParent.hasProject: - logger.error("No project open") - return False - - if not self.hasFocus(): - return False - tHandle = self.getSelectedHandle() - tItem = self._getTreeItem(tHandle) - if tItem is None: + trItem = self._getTreeItem(tHandle) + if trItem is None: + logger.verbose("No item selected") return False - pItem = tItem.parent() + pItem = trItem.parent() + isExp = trItem.isExpanded() if pItem is None: - tIndex = self.indexOfTopLevelItem(tItem) + tIndex = self.indexOfTopLevelItem(trItem) nChild = self.topLevelItemCount() + nIndex = tIndex + nStep if nIndex < 0 or nIndex >= nChild: return False + cItem = self.takeTopLevelItem(tIndex) self.insertTopLevelItem(nIndex, cItem) else: - tIndex = pItem.indexOfChild(tItem) + tIndex = pItem.indexOfChild(trItem) nChild = pItem.childCount() + nIndex = tIndex + nStep if nIndex < 0 or nIndex >= nChild: return False + cItem = pItem.takeChild(tIndex) pItem.insertChild(nIndex, cItem) self._recordLastMove(cItem, pItem, tIndex) self._alertTreeChange(tHandle=tHandle, flush=True) self.clearSelection() - cItem.setSelected(True) + trItem.setSelected(True) + trItem.setExpanded(isExp) return True - def editTreeItem(self, tHandle=None): - """Open the edit item dialog. + def renameTreeItem(self, tHandle): + """Open a dialog to edit the label of an item. """ - if tHandle is None: - logger.warning("No item selected") - return False - tItem = self.theProject.tree[tHandle] if tItem is None: return False - if tItem.itemType == nwItemType.NO_TYPE: - return False - logger.verbose("Requesting change to item '%s'", tHandle) - dlgProj = GuiItemEditor(self, tHandle) - dlgProj.exec_() - if dlgProj.result() == QDialog.Accepted: + newLabel, dlgOk = GuiEditLabel.getLabel(self, text=tItem.itemName) + if dlgOk: + tItem.setName(newLabel) self.setTreeItemValues(tHandle) self._alertTreeChange(tHandle=tHandle, flush=False) - return True + return def saveTreeOrder(self): """Build a list of the items in the project tree and send them @@ -357,8 +599,8 @@ class GuiProjectTree(QTreeWidget): return True def getTreeFromHandle(self, tHandle): - """Recursively return all the children items starting from a - given item handle. + """Recursively return all the child items starting from a given + item handle. """ theList = [] theItem = self._getTreeItem(tHandle) @@ -366,30 +608,12 @@ class GuiProjectTree(QTreeWidget): theList = self._scanChildren(theList, theItem, 0) return theList - def toggleExpanded(self, tHandle): - """Expand an item based on its handle. - """ - trItem = self._getTreeItem(tHandle) - if trItem is not None: - trItem.setExpanded(not trItem.isExpanded()) - return - - def getColumnSizes(self): - """Return the column widths for the tree columns. - """ - retVals = [ - self.columnWidth(0), - self.columnWidth(1), - self.columnWidth(2), - ] - return retVals - def emptyTrash(self): """Permanently delete all documents in the Trash folder. This function only asks for confirmation once, and calls the regular deleteItem function for each document in the Trash folder. """ - if not self.theParent.hasProject: + if not self.mainGui.hasProject: logger.error("No project open") return False @@ -397,7 +621,7 @@ class GuiProjectTree(QTreeWidget): logger.debug("Emptying Trash folder") if trashHandle is None: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "There is currently no Trash folder in this project." ), nwAlert.INFO) return False @@ -408,12 +632,12 @@ class GuiProjectTree(QTreeWidget): nTrash = len(theTrash) if nTrash == 0: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "The Trash folder is already empty." ), nwAlert.INFO) return False - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("Empty Trash"), self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash) ) @@ -438,7 +662,7 @@ class GuiProjectTree(QTreeWidget): delete the files on disk. Root folders are deleted if they're empty only, and the deletion is always permanent. """ - if not self.theParent.hasProject: + if not self.mainGui.hasProject: logger.error("No project open") return False @@ -471,7 +695,7 @@ class GuiProjectTree(QTreeWidget): self._deleteTreeItem(tHandle) self._alertTreeChange(tHandle=tHandle, flush=True) else: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Cannot delete root folder. It is not empty. " "Recursive deletion is not supported. " "Please delete the content first." @@ -501,7 +725,7 @@ class GuiProjectTree(QTreeWidget): # user if they want to permanently delete the file. doPermanent = False if not alreadyAsked: - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("Delete"), self.tr("Permanently delete '{0}'?").format(nwItemS.itemName) ) @@ -517,17 +741,17 @@ class GuiProjectTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) trItemC = trItemP.takeChild(tIndex) for dHandle in reversed(self.getTreeFromHandle(tHandle)): - if self.theParent.docEditor.docHandle() == dHandle: - self.theParent.closeDocument() + if self.mainGui.docEditor.docHandle() == dHandle: + self.mainGui.closeDocument() self._deleteTreeItem(dHandle) self._alertTreeChange(tHandle=tHandle, flush=autoFlush) - self.wordCountsChanged.emit() + self.projView.wordCountsChanged.emit() else: # The item is not already in the trash folder, so we # move it there. - msgYes = self.theParent.askQuestion( + msgYes = self.mainGui.askQuestion( self.tr("Delete"), self.tr("Move '{0}' to Trash?").format(nwItemS.itemName), ) @@ -554,33 +778,26 @@ class GuiProjectTree(QTreeWidget): if trItem is None or nwItem is None: return - expIcon = QIcon() - if nwItem.itemType == nwItemType.FILE: - if nwItem.isExported: - expIcon = self.theTheme.getIcon("check") - else: - expIcon = self.theTheme.getIcon("cross") - itemStatus, statusIcon = nwItem.getImportStatus() hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) - itemIcon = self.theTheme.getItemIcon( + itemIcon = self.mainTheme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel ) trItem.setIcon(self.C_NAME, itemIcon) trItem.setText(self.C_NAME, nwItem.itemName) - trItem.setIcon(self.C_EXPORT, expIcon) trItem.setIcon(self.C_STATUS, statusIcon) trItem.setToolTip(self.C_STATUS, itemStatus) + if nwItem.itemType == nwItemType.FILE: + trItem.setIcon( + self.C_EXPORT, self.mainTheme.getIcon("check" if nwItem.isExported else "cross") + ) + if self.mainConf.emphLabels and nwItem.itemLayout == nwItemLayout.DOCUMENT: trFont = trItem.font(self.C_NAME) - if hLevel in ("H1", "H2"): - trFont.setBold(True) - trFont.setUnderline(True) - else: - trFont.setBold(False) - trFont.setUnderline(False) + trFont.setBold(hLevel == "H1" or hLevel == "H2") + trFont.setUnderline(hLevel == "H1") trItem.setFont(self.C_NAME, trFont) return @@ -648,9 +865,6 @@ class GuiProjectTree(QTreeWidget): dstItem = self._lastMove.get("parent", None) dstIndex = self._lastMove.get("index", None) - if not self.hasFocus(): - return False - if srcItem is None or dstItem is None or dstIndex is None: logger.verbose("No tree move to undo") return False @@ -697,9 +911,6 @@ class GuiProjectTree(QTreeWidget): def setSelectedHandle(self, tHandle, doScroll=False): """Set a specific handle as the selected item. """ - if tHandle not in self._treeMap: - return False - tItem = self._getTreeItem(tHandle) if tItem is None: return False @@ -713,39 +924,159 @@ class GuiProjectTree(QTreeWidget): return True + def openContextOnSelected(self): + """Open the context menu on the current selected item. + """ + selItem = self.selectedItems() + if selItem: + pos = self.visualItemRect(selItem[0]).center() + return self._openContextMenu(pos) + return False + def changedSince(self, checkTime): """Check if the tree has changed since a given time. """ return self._timeChanged > checkTime ## - # Slots + # Private Slots ## + @pyqtSlot() + def _treeSelectionChange(self): + """The user changed which item is selected. + """ + tHandle = self.getSelectedHandle() + if tHandle is not None: + self.projView.selectedItemChanged.emit(tHandle) + return + + @pyqtSlot("QTreeWidgetItem*", int) + def _treeDoubleClick(self, tItem, colNo): + """Capture a double-click event and either request the document + for editing if it is a file, or expand/close the node it is not. + """ + tHandle = self.getSelectedHandle() + if tHandle is None: + return + + tItem = self.theProject.tree[tHandle] + if tItem is None: + return + + if tItem.itemType == nwItemType.FILE: + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "") + else: + trItem = self._getTreeItem(tHandle) + if trItem is not None: + trItem.setExpanded(not trItem.isExpanded()) + + return + @pyqtSlot("QPoint") - def _rightClickMenu(self, clickPos): + def _openContextMenu(self, clickPos): """The user right clicked an element in the project tree, so we open a context menu in-place. """ + tItem = None selItem = self.itemAt(clickPos) if isinstance(selItem, QTreeWidgetItem): tHandle = selItem.data(self.C_NAME, Qt.UserRole) - self.setSelectedHandle(tHandle) # Just to be safe tItem = self.theProject.tree[tHandle] - if tItem is not None: - if self.ctxMenu.filterActions(tItem): - # Only open menu if any actions remain after filter - self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) - return + if tItem is None: + logger.debug("No item found") + return False - @pyqtSlot(str, int, int, int) - def doUpdateCounts(self, tHandle, cCount, wCount, pCount): - """Slot for updating the word count of a specific item. - """ - self.propagateCount(tHandle, wCount, countChildren=True) - self.wordCountsChanged.emit() - return + ctxMenu = QMenu() + + # Trash Folder + # ============ + + trashHandle = self.theProject.tree.trashRoot() + if tItem.itemHandle == trashHandle and trashHandle is not None: + # The trash folder only has one option + ctxMenu.addAction( + self.tr("Empty Trash"), lambda: self.emptyTrash() + ) + ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) + return True + + # Document Actions + # ================ + + isFile = tItem.itemType == nwItemType.FILE + if isFile: + ctxMenu.addAction( + self.tr("Open Document"), + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "") + ) + ctxMenu.addAction( + self.tr("View Document"), + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") + ) + ctxMenu.addSeparator() + + # Edit Item Settings + # ================== + + ctxMenu.addAction( + self.tr("Change Label"), lambda: self.renameTreeItem(tHandle) + ) + + if isFile: + ctxMenu.addAction( + self.tr("Toggle Exported"), lambda: self._toggleItemExported(tHandle) + ) + + if tItem.isNovelLike(): + mStatus = ctxMenu.addMenu(self.tr("Change Status")) + for n, (key, entry) in enumerate(self.theProject.statusItems.items()): + aStatus = mStatus.addAction(entry["icon"], entry["name"]) + aStatus.triggered.connect( + lambda n, key=key: self._changeItemStatus(tHandle, key) + ) + else: + mImport = ctxMenu.addMenu(self.tr("Change Importance")) + for n, (key, entry) in enumerate(self.theProject.importItems.items()): + aImport = mImport.addAction(entry["icon"], entry["name"]) + aImport.triggered.connect( + lambda n, key=key: self._changeItemImport(tHandle, key) + ) + + if isFile and tItem.documentAllowed(): + if tItem.itemLayout == nwItemLayout.NOTE: + ctxMenu.addAction( + self.tr("Change to {0}").format( + trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT]) + ), + lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT) + ) + else: + ctxMenu.addAction( + self.tr("Change to {0}").format( + trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE]) + ), + lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE) + ) + + ctxMenu.addSeparator() + + # Delete Item + # =========== + + if tItem.itemClass == nwItemClass.TRASH or tItem.itemType == nwItemType.ROOT: + ctxMenu.addAction( + self.tr("Delete Permanently"), lambda: self.deleteItem(tHandle) + ) + else: + ctxMenu.addAction( + self.tr("Move to Trash"), lambda: self.deleteItem(tHandle) + ) + + ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) + + return True ## # Events @@ -774,7 +1105,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.theParent.viewDocument(tHandle) + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") return @@ -869,7 +1200,7 @@ class GuiProjectTree(QTreeWidget): if self.theProject.tree.checkType(tHandle, nwItemType.FILE): delDoc = NWDoc(self.theProject, tHandle) if not delDoc.deleteDocument(): - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Could not delete document file."), delDoc.getError() ], nwAlert.ERROR) return False @@ -880,6 +1211,46 @@ class GuiProjectTree(QTreeWidget): return True + def _toggleItemExported(self, tHandle): + """Toggle the exported status of an item. + """ + tItem = self.theProject.tree[tHandle] + if tItem is not None: + tItem.setExported(not tItem.isExported) + self.setTreeItemValues(tItem.itemHandle) + return + + def _changeItemStatus(self, tHandle, tStatus): + """Set a new status value of an item. + """ + tItem = self.theProject.tree[tHandle] + if tItem is not None: + tItem.setStatus(tStatus) + self.setTreeItemValues(tItem.itemHandle) + return + + def _changeItemImport(self, tHandle, tImport): + """Set a new importance value of an item. + """ + tItem = self.theProject.tree[tHandle] + if tItem is not None: + tItem.setImport(tImport) + self.setTreeItemValues(tItem.itemHandle) + return + + def _changeItemLayout(self, tHandle, itemLayout): + """Set a new item layout value of an item. + """ + tItem = self.theProject.tree[tHandle] + if tItem is not None: + if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed(): + tItem.setLayout(nwItemLayout.DOCUMENT) + self.setTreeItemValues(tItem.itemHandle) + elif itemLayout == nwItemLayout.NOTE: + tItem.setLayout(nwItemLayout.NOTE) + self.setTreeItemValues(tItem.itemHandle) + return + def _scanChildren(self, theList, tItem, tIndex): """This is a recursive function returning all items in a tree starting at a given QTreeWidgetItem. @@ -925,7 +1296,7 @@ class GuiProjectTree(QTreeWidget): newItem.setFlags(newItem.flags() ^ Qt.ItemIsDragEnabled) self.addTopLevelItem(newItem) else: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "There is nowhere to add item with name '{0}'." ).format(nwItem.itemName), nwAlert.ERROR) del self._treeMap[tHandle] @@ -981,11 +1352,11 @@ class GuiProjectTree(QTreeWidget): itemType = tItem.itemType if itemType == nwItemType.ROOT: - self.rootFolderChanged.emit(tHandle) + self.projView.rootFolderChanged.emit(tHandle) elif itemType == nwItemType.FILE and tItem.isNovelLike(): - self.novelItemChanged.emit(tHandle) + self.projView.novelItemChanged.emit(tHandle) - self.treeItemChanged.emit(tHandle) + self.projView.treeItemChanged.emit(tHandle) return @@ -1003,167 +1374,3 @@ class GuiProjectTree(QTreeWidget): return # END Class GuiProjectTree - - -class GuiProjectTreeMenu(QMenu): - - def __init__(self, theTree): - QMenu.__init__(self, theTree) - - self.theTree = theTree - self.theItem = None - - self.editItem = QAction(self.tr("Edit Project Item"), self) - self.editItem.triggered.connect(self._doEditItem) - self.addAction(self.editItem) - - self.openItem = QAction(self.tr("Open Document"), self) - self.openItem.triggered.connect(self._doOpenItem) - self.addAction(self.openItem) - - self.viewItem = QAction(self.tr("View Document"), self) - self.viewItem.triggered.connect(self._doViewItem) - self.addAction(self.viewItem) - - self.toggleExp = QAction(self.tr("Toggle Included Flag"), self) - self.toggleExp.triggered.connect(self._doToggleExported) - self.addAction(self.toggleExp) - - self.newFile = QAction(self.tr("New File"), self) - self.newFile.triggered.connect(self._doMakeFile) - self.addAction(self.newFile) - - self.newFolder = QAction(self.tr("New Folder"), self) - self.newFolder.triggered.connect(self._doMakeFolder) - self.addAction(self.newFolder) - - self.deleteItem = QAction(self.tr("Delete Item"), self) - self.deleteItem.triggered.connect(self._doDeleteItem) - self.addAction(self.deleteItem) - - self.emptyTrash = QAction(self.tr("Empty Trash"), self) - self.emptyTrash.triggered.connect(self._doEmptyTrash) - self.addAction(self.emptyTrash) - - self.moveUp = QAction(self.tr("Move Item Up"), self) - self.moveUp.triggered.connect(self._doMoveUp) - self.addAction(self.moveUp) - - self.moveDown = QAction(self.tr("Move Item Down"), self) - self.moveDown.triggered.connect(self._doMoveDown) - self.addAction(self.moveDown) - - return - - def filterActions(self, theItem): - """Filter the menu entries available based on the properties of - the item the menu was activated on. - """ - self.theItem = theItem - - if theItem is None: - logger.error("Failed to extract information to build tree context menu") - return False - - trashHandle = self.theTree.theProject.tree.trashRoot() - - inTrash = self.theTree.theProject.tree.isTrash(theItem.itemHandle) - isTrash = theItem.itemHandle == trashHandle and trashHandle is not None - isFile = theItem.itemType == nwItemType.FILE - - allowNew = not (isTrash or inTrash) - - self.editItem.setVisible(not isTrash) - self.openItem.setVisible(isFile) - self.viewItem.setVisible(isFile) - self.toggleExp.setVisible(isFile) - self.newFile.setVisible(allowNew) - self.newFolder.setVisible(allowNew) - self.deleteItem.setVisible(not isTrash) - self.emptyTrash.setVisible(isTrash) - - return True - - ## - # Slots - ## - - @pyqtSlot() - def _doOpenItem(self): - """Forward the open document call to the main GUI window. - """ - if self.theItem is not None: - self.theTree.theParent.openDocument(self.theItem.itemHandle, doScroll=False) - return - - @pyqtSlot() - def _doViewItem(self): - """Forward the view document call to the main GUI window. - """ - if self.theItem is not None: - self.theTree.theParent.viewDocument(self.theItem.itemHandle) - return - - @pyqtSlot() - def _doEditItem(self): - """Forward the edit item call to the main GUI window. - """ - if self.theItem is not None: - self.theTree.theParent.editItem() - return - - @pyqtSlot() - def _doMakeFile(self): - """Forward the new file call to the project tree. - """ - if self.theItem is not None: - self.theTree.newTreeItem(nwItemType.FILE) - return - - @pyqtSlot() - def _doMakeFolder(self): - """Forward the new folder call to the project tree. - """ - if self.theItem is not None: - self.theTree.newTreeItem(nwItemType.FOLDER) - return - - @pyqtSlot() - def _doToggleExported(self): - """Flip the isExported flag of the current item. - """ - if self.theItem is not None: - self.theItem.setExported(not self.theItem.isExported) - self.theTree.setTreeItemValues(self.theItem.itemHandle) - return - - @pyqtSlot() - def _doDeleteItem(self): - """Forward the delete item call to the project tree. - """ - if self.theItem is not None: - self.theTree.deleteItem() - return - - @pyqtSlot() - def _doEmptyTrash(self): - """Forward the empty trash call to the project tree. - """ - self.theTree.emptyTrash() - return - - @pyqtSlot() - def _doMoveUp(self): - """Forward the move item call to the project tree. - """ - self.theTree.moveTreeItem(-1) - return - - @pyqtSlot() - def _doMoveDown(self): - """Forward the move item call to the project tree. - """ - self.theTree.moveTreeItem(1) - return - -# END Class GuiProjectTreeMenu diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index e3891e7e..a0cf9416 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -41,22 +41,22 @@ logger = logging.getLogger(__name__) class GuiMainStatus(QStatusBar): - def __init__(self, theParent): - QStatusBar.__init__(self, theParent) + def __init__(self, mainGui): + QStatusBar.__init__(self, mainGui) logger.debug("Initialising GuiMainStatus ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme self.refTime = None self.userIdle = False - colNone = QColor(*self.theTheme.statNone) - colTrue = QColor(*self.theTheme.statUnsaved) - colFalse = QColor(*self.theTheme.statSaved) + colNone = QColor(*self.mainTheme.statNone) + colTrue = QColor(*self.mainTheme.statUnsaved) + colFalse = QColor(*self.mainTheme.statSaved) - iPx = self.theTheme.baseIconSize + iPx = self.mainTheme.baseIconSize # Permanent Widgets # ================= @@ -66,7 +66,7 @@ class GuiMainStatus(QStatusBar): # The Spell Checker Language self.langIcon = QLabel("") self.langText = QLabel(self.tr("None")) - self.langIcon.setPixmap(self.theTheme.getPixmap("status_lang", (iPx, iPx))) + self.langIcon.setPixmap(self.mainTheme.getPixmap("status_lang", (iPx, iPx))) self.langIcon.setContentsMargins(0, 0, 0, 0) self.langText.setContentsMargins(0, 0, xM, 0) self.addPermanentWidget(self.langIcon) @@ -91,7 +91,7 @@ class GuiMainStatus(QStatusBar): # The Project and Session Stats self.statsIcon = QLabel() self.statsText = QLabel("") - self.statsIcon.setPixmap(self.theTheme.getPixmap("status_stats", (iPx, iPx))) + self.statsIcon.setPixmap(self.mainTheme.getPixmap("status_stats", (iPx, iPx))) self.statsIcon.setContentsMargins(0, 0, 0, 0) self.statsText.setContentsMargins(0, 0, xM, 0) self.addPermanentWidget(self.statsIcon) @@ -99,14 +99,14 @@ class GuiMainStatus(QStatusBar): # The Session Clock # Set the mimimum width so the label doesn't rescale every second - self.timePixmap = self.theTheme.getPixmap("status_time", (iPx, iPx)) - self.idlePixmap = self.theTheme.getPixmap("status_idle", (iPx, iPx)) + self.timePixmap = self.mainTheme.getPixmap("status_time", (iPx, iPx)) + self.idlePixmap = self.mainTheme.getPixmap("status_idle", (iPx, iPx)) self.timeIcon = QLabel() self.timeText = QLabel("") self.timeIcon.setPixmap(self.timePixmap) self.timeText.setToolTip(self.tr("Session Time")) - self.timeText.setMinimumWidth(self.theTheme.getTextWidth("00:00:00:")) + self.timeText.setMinimumWidth(self.mainTheme.getTextWidth("00:00:00:")) self.timeIcon.setContentsMargins(0, 0, 0, 0) self.timeText.setContentsMargins(0, 0, 0, 0) self.addPermanentWidget(self.timeIcon) diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index bb93e0b8..6c4ff50e 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -54,7 +54,7 @@ class GuiTheme: def __init__(self): self.mainConf = novelwriter.CONFIG - self.theIcons = GuiIcons(self) + self.iconCache = GuiIcons(self) # Loaded Theme Settings # ===================== @@ -127,13 +127,13 @@ class GuiTheme: self.updateFont() self.updateTheme() - self.theIcons.updateTheme() + self.iconCache.updateTheme() # Icon Functions - self.getIcon = self.theIcons.getIcon - self.getPixmap = self.theIcons.getPixmap - self.getItemIcon = self.theIcons.getItemIcon - self.loadDecoration = self.theIcons.loadDecoration + self.getIcon = self.iconCache.getIcon + self.getPixmap = self.iconCache.getPixmap + self.getItemIcon = self.iconCache.getItemIcon + self.loadDecoration = self.iconCache.loadDecoration # Extract Other Info self.guiDPI = qApp.primaryScreen().logicalDotsPerInchX() @@ -472,16 +472,19 @@ class GuiIcons: # Switches "sticky-on", "sticky-off", "bullet-on", "bullet-off", + + # Decorations + "deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4", } - DECO_MAP = { + IMAGE_MAP = { "wiz-back": "wizard-back.jpg", } - def __init__(self, theTheme): + def __init__(self, mainTheme): self.mainConf = novelwriter.CONFIG - self.theTheme = theTheme + self.mainTheme = mainTheme # Storage self._qIcons = {} @@ -573,19 +576,22 @@ class GuiIcons: # Access Functions ## - def loadDecoration(self, decoKey, pxW, pxH): + def loadDecoration(self, decoKey, pxW=None, pxH=None): """Load graphical decoration element based on the decoration - map. This function always returns a QSwgWidget. + map or the icon map. This function always returns a QPixmap. """ - if decoKey not in self.DECO_MAP: + if decoKey in self._themeMap: + imgPath = self._themeMap[decoKey] + elif decoKey in self.IMAGE_MAP: + imgPath = os.path.join( + self.mainConf.assetPath, "images", self.IMAGE_MAP[decoKey] + ) + else: logger.error("Decoration with name '%s' does not exist", decoKey) return QPixmap() - imgPath = os.path.join( - self.mainConf.assetPath, "images", self.DECO_MAP[decoKey] - ) if not os.path.isfile(imgPath): - logger.error("Decoration file '%s' not in assets folder", self.DECO_MAP[decoKey]) + logger.error("Asset '%s' not found", self.IMAGE_MAP[decoKey]) return QPixmap() theDeco = QPixmap(imgPath) diff --git a/novelwriter/gui/viewsbar.py b/novelwriter/gui/viewsbar.py index e5b627b2..031f4316 100644 --- a/novelwriter/gui/viewsbar.py +++ b/novelwriter/gui/viewsbar.py @@ -40,21 +40,21 @@ class GuiViewsBar(QToolBar): viewChangeRequested = pyqtSignal(nwView) - def __init__(self, theParent): - QToolBar.__init__(self, theParent) + def __init__(self, mainGui): + QToolBar.__init__(self, mainGui) logger.debug("Initialising GuiViewsBar ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme # Style iPx = self.mainConf.pxInt(22) mPx = self.mainConf.pxInt(60) - lblFont = self.theTheme.guiFont - lblFont.setPointSizeF(0.65*self.theTheme.fontPointSize) + lblFont = self.mainTheme.guiFont + lblFont.setPointSizeF(0.65*self.mainTheme.fontPointSize) self.setMovable(False) self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) @@ -70,54 +70,54 @@ class GuiViewsBar(QToolBar): self.aProject = QAction(self.tr("Project")) self.aProject.setFont(lblFont) self.aProject.setToolTip(self.tr("Show project tree and editor")) - self.aProject.setIcon(self.theTheme.getIcon("view_editor")) + self.aProject.setIcon(self.mainTheme.getIcon("view_editor")) self.aProject.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT)) self.aNovel = QAction(self.tr("Novel")) self.aNovel.setFont(lblFont) self.aNovel.setToolTip(self.tr("Show novel tree and editor")) - self.aNovel.setIcon(self.theTheme.getIcon("view_novel")) + self.aNovel.setIcon(self.mainTheme.getIcon("view_novel")) self.aNovel.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL)) self.aOutline = QAction(self.tr("Outline")) self.aOutline.setFont(lblFont) self.aOutline.setToolTip(self.tr("Show novel outline")) - self.aOutline.setIcon(self.theTheme.getIcon("view_outline")) + self.aOutline.setIcon(self.mainTheme.getIcon("view_outline")) self.aOutline.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE)) self.aBuild = QAction(self.tr("Build")) self.aBuild.setFont(lblFont) self.aBuild.setToolTip(self.tr("Build novel project")) - self.aBuild.setIcon(self.theTheme.getIcon("view_build")) - self.aBuild.triggered.connect(lambda: self.theParent.showBuildProjectDialog()) + self.aBuild.setIcon(self.mainTheme.getIcon("view_build")) + self.aBuild.triggered.connect(lambda: self.mainGui.showBuildProjectDialog()) self.aDetails = QAction(self.tr("Details")) self.aDetails.setFont(lblFont) self.aDetails.setToolTip(self.tr("Show project details")) - self.aDetails.setIcon(self.theTheme.getIcon("proj_details")) - self.aDetails.triggered.connect(lambda: self.theParent.showProjectDetailsDialog()) + self.aDetails.setIcon(self.mainTheme.getIcon("proj_details")) + self.aDetails.triggered.connect(lambda: self.mainGui.showProjectDetailsDialog()) self.aStats = QAction(self.tr("Stats")) self.aStats.setFont(lblFont) self.aStats.setToolTip(self.tr("Show project statistics")) - self.aStats.setIcon(self.theTheme.getIcon("proj_stats")) - self.aStats.triggered.connect(lambda: self.theParent.showWritingStatsDialog()) + self.aStats.setIcon(self.mainTheme.getIcon("proj_stats")) + self.aStats.triggered.connect(lambda: self.mainGui.showWritingStatsDialog()) # Settings Menu self.mSettings = QMenu() self.aPrjSettings = QAction(self.tr("Project Settings")) - self.aPrjSettings.triggered.connect(lambda: self.theParent.showProjectSettingsDialog()) + self.aPrjSettings.triggered.connect(lambda: self.mainGui.showProjectSettingsDialog()) self.mSettings.addAction(self.aPrjSettings) self.aPreferences = QAction(self.tr("Preferences")) - self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog()) + self.aPreferences.triggered.connect(lambda: self.mainGui.showPreferencesDialog()) self.mSettings.addAction(self.aPreferences) self.tbSettings = QToolButton(self) self.tbSettings.setFont(lblFont) self.tbSettings.setText(self.tr("Settings")) - self.tbSettings.setIcon(self.theTheme.getIcon("settings")) + self.tbSettings.setIcon(self.mainTheme.getIcon("settings")) self.tbSettings.setMenu(self.mSettings) self.tbSettings.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.tbSettings.setPopupMode(QToolButton.InstantPopup) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 6a056dca..e9535747 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -40,7 +40,7 @@ from PyQt5.QtWidgets import ( from novelwriter.gui import ( GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, - GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectTree, GuiTheme, + GuiMainStatus, GuiNovelView, GuiOutlineView, GuiProjectView, GuiTheme, GuiViewsBar ) from novelwriter.dialogs import ( @@ -84,7 +84,7 @@ class GuiMain(QMainWindow): # ============ # Core Classes and Settings - self.theTheme = GuiTheme() + self.mainTheme = GuiTheme() self.theProject = NWProject(self) self.hasProject = False self.isFocusMode = False @@ -104,42 +104,20 @@ class GuiMain(QMainWindow): hWd = self.mainConf.pxInt(4) # Main GUI Elements - self.statusBar = GuiMainStatus(self) - self.treeView = GuiProjectTree(self) - self.novelView = GuiNovelTree(self) - self.docEditor = GuiDocEditor(self) - self.viewMeta = GuiDocViewDetails(self) - self.docViewer = GuiDocViewer(self) - self.treeMeta = GuiItemDetails(self) - self.projView = GuiOutline(self) - self.mainMenu = GuiMainMenu(self) - self.viewsBar = GuiViewsBar(self) - - # Connect Signals Between Main Elements - self.viewsBar.viewChangeRequested.connect(self._changeView) - - self.treeView.itemSelectionChanged.connect(self._treeSingleClick) - self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) - self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) - self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) - self.treeView.treeItemChanged.connect(self.docEditor.updateDocInfo) - self.treeView.treeItemChanged.connect(self.docViewer.updateDocInfo) - self.treeView.treeItemChanged.connect(self.treeMeta.updateViewBox) - self.treeView.rootFolderChanged.connect(self.projView.updateRootItem) - - self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) - self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) - self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts) - self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts) - self.docEditor.loadDocumentTagRequest.connect(self._followTag) - - self.docViewer.loadDocumentTagRequest.connect(self._followTag) - - self.projView.loadDocumentTagRequest.connect(self._followTag) + self.statusBar = GuiMainStatus(self) + self.projView = GuiProjectView(self) + self.novelView = GuiNovelView(self) + self.docEditor = GuiDocEditor(self) + self.viewMeta = GuiDocViewDetails(self) + self.docViewer = GuiDocViewer(self) + self.itemDetails = GuiItemDetails(self) + self.outlineView = GuiOutlineView(self) + self.mainMenu = GuiMainMenu(self) + self.viewsBar = GuiViewsBar(self) # Project Tree Stack self.projStack = QStackedWidget() - self.projStack.addWidget(self.treeView) + self.projStack.addWidget(self.projView) self.projStack.addWidget(self.novelView) self.projStack.currentChanged.connect(self._projStackChanged) @@ -149,7 +127,7 @@ class GuiMain(QMainWindow): self.treeBox.setContentsMargins(0, 0, 0, 0) self.treeBox.setSpacing(mPx) self.treeBox.addWidget(self.projStack) - self.treeBox.addWidget(self.treeMeta) + self.treeBox.addWidget(self.itemDetails) self.treePane.setLayout(self.treeBox) # Splitter : Document Viewer / Document Meta @@ -176,7 +154,7 @@ class GuiMain(QMainWindow): # Main Stack : Editor / Outline self.mainStack = QStackedWidget() self.mainStack.addWidget(self.splitMain) - self.mainStack.addWidget(self.projView) + self.mainStack.addWidget(self.outlineView) self.mainStack.currentChanged.connect(self._mainStackChanged) # Indices of Splitter Widgets @@ -189,15 +167,15 @@ class GuiMain(QMainWindow): # Indices of Tab Widgets self.idxEditorView = self.mainStack.indexOf(self.splitMain) - self.idxOutlineView = self.mainStack.indexOf(self.projView) - self.idxTreeView = self.projStack.indexOf(self.treeView) + self.idxOutlineView = self.mainStack.indexOf(self.outlineView) + self.idxProjView = self.projStack.indexOf(self.projView) self.idxNovelView = self.projStack.indexOf(self.novelView) # Splitter Behaviour self.splitMain.setCollapsible(self.idxTree, False) self.splitMain.setCollapsible(self.idxMain, False) self.splitDocs.setCollapsible(self.idxEditor, False) - self.splitDocs.setCollapsible(self.idxViewer, True) + self.splitDocs.setCollapsible(self.idxViewer, False) self.splitView.setCollapsible(self.idxViewDoc, False) self.splitView.setCollapsible(self.idxViewMeta, False) @@ -214,6 +192,34 @@ class GuiMain(QMainWindow): self.setStatusBar(self.statusBar) self.addToolBar(Qt.LeftToolBarArea, self.viewsBar) + # Connect Signals + # =============== + + self.viewsBar.viewChangeRequested.connect(self._changeView) + + self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox) + self.projView.openDocumentRequest.connect(self._openDocument) + self.projView.novelItemChanged.connect(self._treeNovelItemChanged) + self.projView.wordCountsChanged.connect(self._updateStatusWordCount) + self.projView.treeItemChanged.connect(self.docEditor.updateDocInfo) + self.projView.treeItemChanged.connect(self.docViewer.updateDocInfo) + self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox) + self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem) + self.projView.rootFolderChanged.connect(self.novelView.updateRootItem) + + self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox) + self.novelView.openDocumentRequest.connect(self._openDocument) + + self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) + self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) + self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts) + self.docEditor.docCountsChanged.connect(self.projView.updateCounts) + self.docEditor.loadDocumentTagRequest.connect(self._followTag) + + self.docViewer.loadDocumentTagRequest.connect(self._followTag) + + self.outlineView.loadDocumentTagRequest.connect(self._followTag) + # Finalise Initialisation # ======================= @@ -289,15 +295,15 @@ class GuiMain(QMainWindow): """Wrapper function to clear all sub-elements of the main GUI. """ # Project Area - self.treeView.clearTree() - self.novelView.clearTree() - self.treeMeta.clearDetails() + self.projView.clearProject() + self.novelView.clearProject() + self.itemDetails.clearDetails() # Work Area self.docEditor.clearEditor() self.docEditor.setDictionaries() self.closeDocViewer() - self.projView.clearOutline() + self.outlineView.clearOutline() # General self.statusBar.clearStatus() @@ -359,7 +365,8 @@ class GuiMain(QMainWindow): self.rebuildTrees() self.saveProject() self.docEditor.setDictionaries() - self.projView.updateRootItem(None) + self.outlineView.updateRootItem(None) + self.novelView.openProjectTasks() self.rebuildIndex(beQuiet=True) self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setProjectStatus(nwState.GOOD) @@ -415,7 +422,8 @@ class GuiMain(QMainWindow): if saveOK: self.closeDocument() self.docViewer.clearNavHistory() - self.projView.closeOutline() + self.outlineView.closeOutline() + self.novelView.closeProjectTasks() self.theProject.closeProject(self.idleTime) self.idleRefTime = time() @@ -506,7 +514,8 @@ class GuiMain(QMainWindow): self.docEditor.setDictionaries() self.docEditor.toggleSpellCheck(self.theProject.spellCheck) self.statusBar.setRefTime(self.theProject.projOpened) - self.projView.updateRootItem(None) + self.outlineView.updateRootItem(None) + self.novelView.openProjectTasks() self._updateStatusWordCount() # Restore previously open documents, if any @@ -539,7 +548,7 @@ class GuiMain(QMainWindow): logger.error("No project open") return False - self.treeView.saveTreeOrder() + self.projView.saveProjectTree() if self.theProject.saveProject(autoSave=autoSave): self.theProject.index.saveIndex() @@ -584,7 +593,7 @@ class GuiMain(QMainWindow): if changeFocus: self.docEditor.setFocus() self.theProject.setLastEdited(tHandle) - self.treeView.setSelectedHandle(tHandle, doScroll=doScroll) + self.projView.setSelectedHandle(tHandle, doScroll=doScroll) else: return False @@ -650,7 +659,7 @@ class GuiMain(QMainWindow): self.saveDocument() else: logger.verbose("Trying selected document") - tHandle = self.treeView.getSelectedHandle() + tHandle = self.projView.getSelectedHandle() if tHandle is None: logger.verbose("Trying last viewed document") @@ -787,12 +796,12 @@ class GuiMain(QMainWindow): tHandle = None tLine = None - if self.treeView.hasFocus(): - tHandle = self.treeView.getSelectedHandle() - elif self.novelView.hasFocus(): + if self.projView.treeHasFocus(): + tHandle = self.projView.getSelectedHandle() + elif self.novelView.treeHasFocus(): tHandle, tLine = self.novelView.getSelectedHandle() - elif self.projView.treeFocus(): - tHandle, tLine = self.projView.getSelectedHandle() + elif self.outlineView.treeHasFocus(): + tHandle, tLine = self.outlineView.getSelectedHandle() else: logger.warning("No item selected") return False @@ -802,7 +811,7 @@ class GuiMain(QMainWindow): return True - def editItem(self, tHandle=None): + def editItemLabel(self, tHandle=None): """Open the edit item dialog. """ if not self.hasProject: @@ -813,17 +822,17 @@ class GuiMain(QMainWindow): if self.docEditor.anyFocus() or self.isFocusMode: tHandle = self.docEditor.docHandle() else: - tHandle = self.treeView.getSelectedHandle() + tHandle = self.projView.getSelectedHandle() if tHandle: - return self.treeView.editTreeItem(tHandle) + return self.projView.renameTreeItem(tHandle) return False def rebuildTrees(self): """Rebuild the project tree. """ - self.treeView.buildTree() - self.novelView.refreshTree() + self.projView.populateTree() + # self.novelView.refreshTree() return def requestNovelTreeRefresh(self): @@ -845,7 +854,7 @@ class GuiMain(QMainWindow): qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) tStart = time() - self.treeView.saveTreeOrder() + self.projView.saveProjectTree() self.theProject.index.clearIndex() for tItem in self.theProject.tree: @@ -855,8 +864,8 @@ class GuiMain(QMainWindow): logger.verbose("Indexing '%s'", tItem.itemName) if self.theProject.index.reIndexHandle(tItem.itemHandle): # Update Word Counts - self.treeView.propagateCount(tItem.itemHandle, tItem.wordCount, countChildren=True) - self.treeView.setTreeItemValues(tItem.itemHandle) + self.projView.propagateCount(tItem.itemHandle, tItem.wordCount, countChildren=True) + self.projView.setTreeItemValues(tItem.itemHandle) tEnd = time() self.setStatus( @@ -913,13 +922,13 @@ class GuiMain(QMainWindow): if dlgConf.result() == QDialog.Accepted: logger.debug("Applying new preferences") self.initMain() - self.theTheme.updateTheme() + self.mainTheme.updateTheme() self.saveDocument() self.docEditor.initEditor() self.docViewer.initViewer() - self.treeView.initTree() - self.novelView.initTree() - self.projView.initOutline() + self.projView.initSettings() + self.novelView.initSettings() + self.outlineView.initOutline() self._updateStatusWordCount() return @@ -938,7 +947,7 @@ class GuiMain(QMainWindow): logger.debug("Applying new project settings") if dlgProj.spellChanged: self.docEditor.setDictionaries() - self.treeMeta.refreshDetails() + self.itemDetails.refreshDetails() self._updateWindowTitle(self.theProject.projName) return True @@ -1154,13 +1163,11 @@ class GuiMain(QMainWindow): if not self.isFocusMode: self.mainConf.setMainPanePos(self.splitMain.sizes()) self.mainConf.setDocPanePos(self.splitDocs.sizes()) - self.mainConf.setOutlinePanePos(self.projView.splitSizes()) + self.mainConf.setOutlinePanePos(self.outlineView.splitSizes()) if self.viewMeta.isVisible(): self.mainConf.setViewPanePos(self.splitView.sizes()) self.mainConf.setShowRefPanel(self.viewMeta.isVisible()) - self.mainConf.setTreeColWidths(self.treeView.getColumnSizes()) - self.mainConf.setNovelColWidths(self.novelView.getColumnSizes()) if not self.mainConf.isFullScreen: self.mainConf.setWinSize(self.width(), self.height()) @@ -1179,8 +1186,8 @@ class GuiMain(QMainWindow): """ if paneNo == nwWidget.TREE: tabIdx = self.projStack.currentIndex() - if tabIdx == self.idxTreeView: - self.treeView.setFocus() + if tabIdx == self.idxProjView: + self.projView.setFocus() elif tabIdx == self.idxNovelView: self.novelView.setFocus() elif paneNo == nwWidget.EDITOR: @@ -1191,7 +1198,7 @@ class GuiMain(QMainWindow): self.docViewer.setFocus() elif paneNo == nwWidget.OUTLINE: self._changeView(nwView.OUTLINE) - self.projView.setTreeFocus() + self.outlineView.setTreeFocus() return def closeDocEditor(self): @@ -1468,6 +1475,17 @@ class GuiMain(QMainWindow): self.viewDocument(tHandle=tHandle, tAnchor=f"#{sTitle}") return + @pyqtSlot(str, Enum, int, str) + def _openDocument(self, tHandle, tMode, tLine, tAnchor): + """Handle an open document request from one of the tree views. + """ + if tHandle is not None: + if tMode == nwDocMode.EDIT: + self.openDocument(tHandle, tLine=tLine, changeFocus=False) + elif tMode == nwDocMode.VIEW: + self.viewDocument(tHandle=tHandle, tAnchor=(tAnchor or None)) + return + @pyqtSlot(nwView) def _changeView(self, view): """Handle the requested change of view from the GuiViewBar. @@ -1478,14 +1496,14 @@ class GuiMain(QMainWindow): elif view == nwView.PROJECT: self.mainStack.setCurrentWidget(self.splitMain) - self.projStack.setCurrentWidget(self.treeView) + self.projStack.setCurrentWidget(self.projView) elif view == nwView.NOVEL: self.mainStack.setCurrentWidget(self.splitMain) self.projStack.setCurrentWidget(self.novelView) elif view == nwView.OUTLINE: - self.mainStack.setCurrentWidget(self.projView) + self.mainStack.setCurrentWidget(self.outlineView) return @@ -1531,32 +1549,6 @@ class GuiMain(QMainWindow): return - @pyqtSlot() - def _treeSingleClick(self): - """Single click on a project tree item just updates the details - panel below the tree. - """ - tHandle = self.treeView.getSelectedHandle() - if tHandle is not None: - self.treeMeta.updateViewBox(tHandle) - return - - @pyqtSlot("QTreeWidgetItem*", int) - def _treeDoubleClick(self, tItem, colNo): - """The user double-clicked an item in the tree. If it is a file, - we open it. Otherwise, we toggle the expanded status. - """ - tHandle = self.treeView.getSelectedHandle() - if tHandle is not None: - tItem = self.theProject.tree[tHandle] - if tItem is None: - return - if tItem.itemType == nwItemType.FILE: - self.openDocument(tHandle, changeFocus=False, doScroll=False) - else: - self.treeView.toggleExpanded(tHandle) - return - @pyqtSlot() def _treeNovelItemChanged(self): """Triggered when there is a change to a novel item in the @@ -1565,7 +1557,7 @@ class GuiMain(QMainWindow): if self.mainStack.currentIndex() == self.idxOutlineView: logger.verbose("Novel tree changed while Outline tab active") if self.hasProject: - self.projView.refreshView(novelChanged=True) + self.outlineView.refreshView(novelChanged=True) return @@ -1590,35 +1582,35 @@ class GuiMain(QMainWindow): return @pyqtSlot(int) - def _mainStackChanged(self, tabIndex): + def _mainStackChanged(self, stIndex): """Activated when the main window tab is changed. """ - if tabIndex == self.idxEditorView: - logger.verbose("Editor tab activated") - elif tabIndex == self.idxOutlineView: - logger.verbose("Project outline tab activated") + if stIndex == self.idxEditorView: + logger.verbose("Editor View activated") + elif stIndex == self.idxOutlineView: + logger.verbose("Outline View activated") if self.hasProject: - self.projView.refreshView() + self.outlineView.refreshView() return @pyqtSlot(int) - def _projStackChanged(self, tabIndex): + def _projStackChanged(self, stIndex): """Activated when the project view tab is changed. """ sHandle = None - if tabIndex == self.idxTreeView: - logger.verbose("Project tree tab activated") - sHandle = self.treeView.getSelectedHandle() + if stIndex == self.idxProjView: + logger.verbose("Project Tree View activated") + sHandle = self.projView.getSelectedHandle() - elif tabIndex == self.idxNovelView: - logger.verbose("Novel tree tab activated") + elif stIndex == self.idxNovelView: + logger.verbose("Novel Tree View activated") if self.hasProject: self.novelView.refreshTree() sHandle, _ = self.novelView.getSelectedHandle() - self.treeMeta.updateViewBox(sHandle) + self.itemDetails.updateViewBox(sHandle) return diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index b27a98a4..efeff928 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -65,16 +65,16 @@ class GuiBuildNovel(QDialog): FMT_JSON_H = 8 # HTML5 wrapped in JSON FMT_JSON_M = 9 # nW Markdown wrapped in JSON - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiBuildNovel ...") self.setObjectName("GuiBuildNovel") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme + self.theProject = mainGui.theProject self.htmlText = [] # List of html documents self.htmlStyle = [] # List of html styles @@ -93,7 +93,7 @@ class GuiBuildNovel(QDialog): self.docView = GuiBuildNovelDocView(self, self.theProject) - hS = self.theTheme.fontPixelSize + hS = self.mainTheme.fontPixelSize wS = 2*hS # Title Formats @@ -238,11 +238,11 @@ class GuiBuildNovel(QDialog): pOptions.getString("GuiBuildNovel", "textFont", self.mainConf.textFont) ) self.fontButton = QPushButton("...") - self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) + self.fontButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("..."))) self.fontButton.clicked.connect(self._selectFont) self.textSize = QSpinBox(self) - self.textSize.setFixedWidth(6*self.theTheme.textNWidth) + self.textSize.setFixedWidth(6*self.mainTheme.textNWidth) self.textSize.setMinimum(6) self.textSize.setMaximum(72) self.textSize.setSingleStep(1) @@ -251,7 +251,7 @@ class GuiBuildNovel(QDialog): ) self.lineHeight = QDoubleSpinBox(self) - self.lineHeight.setFixedWidth(6*self.theTheme.textNWidth) + self.lineHeight.setFixedWidth(6*self.mainTheme.textNWidth) self.lineHeight.setMinimum(0.8) self.lineHeight.setMaximum(3.0) self.lineHeight.setSingleStep(0.05) @@ -711,7 +711,7 @@ class GuiBuildNovel(QDialog): bldObj.initDocument() # Make sure the project and document is up to date - self.theParent.saveDocument() + self.mainGui.saveDocument() self.buildProgress.setMaximum(len(self.theProject.tree)) self.buildProgress.setValue(0) @@ -760,7 +760,7 @@ class GuiBuildNovel(QDialog): logger.debug("Built project in %.3f ms", 1000*(tEnd - tStart)) if bldObj.errData: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("There were problems when building the project:") ] + bldObj.errData, nwAlert.ERROR) @@ -1003,11 +1003,11 @@ class GuiBuildNovel(QDialog): # ============== if wSuccess: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("{0} file successfully written to:").format(textFmt), savePath ], nwAlert.INFO) else: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to write {0} file. {1}" ).format(textFmt, errMsg), nwAlert.ERROR) @@ -1193,18 +1193,18 @@ class GuiBuildNovel(QDialog): class GuiBuildNovelDocView(QTextBrowser): - def __init__(self, theParent, theProject): - QTextBrowser.__init__(self, theParent) + def __init__(self, mainGui, theProject): + QTextBrowser.__init__(self, mainGui) logger.debug("Initialising GuiBuildNovelDocView ...") self.mainConf = novelwriter.CONFIG self.theProject = theProject - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme self.buildTime = 0 - self.setMinimumWidth(40*self.theParent.theTheme.textNWidth) + self.setMinimumWidth(40*self.mainGui.mainTheme.textNWidth) self.setOpenExternalLinks(False) self.document().setDocumentMargin(self.mainConf.getTextMargin()) @@ -1238,9 +1238,9 @@ class GuiBuildNovelDocView(QTextBrowser): lblPalette.setColor(QPalette.Foreground, lblPalette.toolTipText().color()) lblFont = self.font() - lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize) - fPx = int(1.1*self.theTheme.fontPixelSize) + fPx = int(1.1*self.mainTheme.fontPixelSize) self.theTitle = QLabel("", self) self.theTitle.setIndent(0) diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index 4ce21643..2086d615 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -42,15 +42,15 @@ logger = logging.getLogger(__name__) class GuiLipsum(QDialog): - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiLipsum ...") self.setObjectName("GuiLipsum") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme self.setWindowTitle(self.tr("Insert Placeholder Text")) @@ -61,7 +61,7 @@ class GuiLipsum(QDialog): nPx = self.mainConf.pxInt(64) vSp = self.mainConf.pxInt(4) self.docIcon = QLabel() - self.docIcon.setPixmap(self.theParent.theTheme.getPixmap("proj_document", (nPx, nPx))) + self.docIcon.setPixmap(self.mainTheme.getPixmap("proj_document", (nPx, nPx))) self.leftBox = QVBoxLayout() self.leftBox.setSpacing(vSp) @@ -129,7 +129,7 @@ class GuiLipsum(QDialog): pCount = self.paraCount.value() inText = "\n\n".join(lipsumText[0:pCount]) + "\n\n" - self.theParent.docEditor.insertText(inText) + self.mainGui.docEditor.insertText(inText) return diff --git a/novelwriter/tools/projwizard.py b/novelwriter/tools/projwizard.py index 765445b9..5b21f69a 100644 --- a/novelwriter/tools/projwizard.py +++ b/novelwriter/tools/projwizard.py @@ -48,17 +48,17 @@ PAGE_FINAL = 4 class GuiProjectWizard(QWizard): - def __init__(self, theParent): - QWizard.__init__(self, theParent) + def __init__(self, mainGui): + QWizard.__init__(self, mainGui) logger.debug("Initialising GuiProjectWizard ...") self.setObjectName("GuiProjectWizard") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme - self.sideImage = self.theTheme.loadDecoration( + self.sideImage = self.mainTheme.loadDecoration( "wiz-back", None, self.mainConf.pxInt(370) ) self.setWizardStyle(QWizard.ModernStyle) @@ -92,7 +92,7 @@ class ProjWizardIntroPage(QWizardPage): self.mainConf = novelwriter.CONFIG self.theWizard = theWizard - self.theTheme = theWizard.theTheme + self.mainTheme = theWizard.mainTheme self.setTitle(self.tr("Create New Project")) self.theText = QLabel(self.tr( @@ -107,7 +107,7 @@ class ProjWizardIntroPage(QWizardPage): "Peter Mitterhofer", "CC BY-SA 4.0" )) lblFont = self.imgCredit.font() - lblFont.setPointSizeF(0.6*self.theTheme.fontPointSize) + lblFont.setPointSizeF(0.6*self.mainTheme.fontPointSize) self.imgCredit.setFont(lblFont) xW = self.mainConf.pxInt(300) @@ -162,7 +162,7 @@ class ProjWizardFolderPage(QWizardPage): self.mainConf = novelwriter.CONFIG self.theWizard = theWizard - self.theTheme = theWizard.theTheme + self.mainTheme = theWizard.mainTheme self.setTitle(self.tr("Select Project Folder")) self.theText = QLabel(self.tr( @@ -180,7 +180,7 @@ class ProjWizardFolderPage(QWizardPage): self.projPath.setPlaceholderText(self.tr("Required")) self.browseButton = QPushButton("...") - self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) + self.browseButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("..."))) self.browseButton.clicked.connect(self._doBrowse) self.errLabel = QLabel("") diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index aff7bc3d..add052ec 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -57,16 +57,16 @@ class GuiWritingStats(QDialog): FMT_JSON = 0 FMT_CSV = 1 - def __init__(self, theParent): - QDialog.__init__(self, theParent) + def __init__(self, mainGui): + QDialog.__init__(self, mainGui) logger.debug("Initialising GuiWritingStats ...") self.setObjectName("GuiWritingStats") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.mainGui = mainGui + self.mainTheme = mainGui.mainTheme + self.theProject = mainGui.theProject self.logData = [] self.filterData = [] @@ -125,7 +125,7 @@ class GuiWritingStats(QDialog): self.listBox.setSortingEnabled(True) # Word Bar - self.barHeight = int(round(0.5*self.theTheme.fontPixelSize)) + self.barHeight = int(round(0.5*self.mainTheme.fontPixelSize)) self.barWidth = self.mainConf.pxInt(200) self.barImage = QPixmap(self.barHeight, self.barHeight) self.barImage.fill(self.palette().highlight().color()) @@ -136,27 +136,27 @@ class GuiWritingStats(QDialog): self.infoBox.setLayout(self.infoForm) self.labelTotal = QLabel(formatTime(0)) - self.labelTotal.setFont(self.theTheme.guiFontFixed) + self.labelTotal.setFont(self.mainTheme.guiFontFixed) self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.labelIdleT = QLabel(formatTime(0)) - self.labelIdleT.setFont(self.theTheme.guiFontFixed) + self.labelIdleT.setFont(self.mainTheme.guiFontFixed) self.labelIdleT.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.labelFilter = QLabel(formatTime(0)) - self.labelFilter.setFont(self.theTheme.guiFontFixed) + self.labelFilter.setFont(self.mainTheme.guiFontFixed) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.novelWords = QLabel("0") - self.novelWords.setFont(self.theTheme.guiFontFixed) + self.novelWords.setFont(self.mainTheme.guiFontFixed) self.novelWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.notesWords = QLabel("0") - self.notesWords.setFont(self.theTheme.guiFontFixed) + self.notesWords.setFont(self.mainTheme.guiFontFixed) self.notesWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.totalWords = QLabel("0") - self.totalWords.setFont(self.theTheme.guiFontFixed) + self.totalWords.setFont(self.mainTheme.guiFontFixed) self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) lblTTime = QLabel(self.tr("Total Time:")) @@ -183,7 +183,7 @@ class GuiWritingStats(QDialog): self.infoForm.setRowStretch(6, 1) # Filter Options - sPx = self.theTheme.baseIconSize + sPx = self.mainTheme.baseIconSize self.filterBox = QGroupBox(self.tr("Filters"), self) self.filterForm = QGridLayout(self) @@ -411,11 +411,11 @@ class GuiWritingStats(QDialog): # Report to user if wSuccess: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("{0} file successfully written to:").format(textFmt), savePath ], nwAlert.INFO) else: - self.theParent.makeAlert([ + self.mainGui.makeAlert([ self.tr("Failed to write {0} file.").format(textFmt), errMsg ], nwAlert.ERROR) @@ -478,7 +478,7 @@ class GuiWritingStats(QDialog): self.logData.append((dStart, sDiff, wcNovel, wcNotes, sIdle)) except Exception as exc: - self.theParent.makeAlert(self.tr( + self.mainGui.makeAlert(self.tr( "Failed to read session log file." ), nwAlert.ERROR, exception=exc) return False @@ -605,13 +605,13 @@ class GuiWritingStats(QDialog): newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter) - newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed) - newItem.setFont(self.C_LENGTH, self.theTheme.guiFontFixed) - newItem.setFont(self.C_COUNT, self.theTheme.guiFontFixed) + newItem.setFont(self.C_TIME, self.mainTheme.guiFontFixed) + newItem.setFont(self.C_LENGTH, self.mainTheme.guiFontFixed) + newItem.setFont(self.C_COUNT, self.mainTheme.guiFontFixed) if showIdleTime: - newItem.setFont(self.C_IDLE, self.theTheme.guiFontFixed) + newItem.setFont(self.C_IDLE, self.mainTheme.guiFontFixed) else: - newItem.setFont(self.C_IDLE, self.theTheme.guiFont) + newItem.setFont(self.C_IDLE, self.mainTheme.guiFont) self.listBox.addTopLevelItem(newItem) self.timeFilter += sDiff diff --git a/sample/content/a520879ca0b45.nwd b/sample/content/a520879ca0b45.nwd index 12d7a62a..75ba39c8 100644 --- a/sample/content/a520879ca0b45.nwd +++ b/sample/content/a520879ca0b45.nwd @@ -1,7 +1,7 @@ %%~name: Chapter One %%~path: e5e47ebf63b1c/a520879ca0b45 %%~kind: NOVEL/DOCUMENT -### Chapter One +## Chapter One @pov: Jane diff --git a/sample/content/edca4be2fcaf8.nwd b/sample/content/edca4be2fcaf8.nwd index 3028b3fd..8b3efc78 100644 --- a/sample/content/edca4be2fcaf8.nwd +++ b/sample/content/edca4be2fcaf8.nwd @@ -3,4 +3,4 @@ %%~kind: NOVEL/DOCUMENT # Part One ->> In the beginning … << \ No newline at end of file +>> In the beginning … << diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 9f3a6587..6ba4b5f1 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1334 - 225 - 67746 + 1371 + 236 + 69222 False @@ -15,8 +15,10 @@ True None True - a520879ca0b45 + 636b6aa9b697b 636b6aa9b697b + 7031beac91f75 + None 1363 954 409 @@ -33,13 +35,13 @@
- New - Notes - Started - 1st Draft - 2nd Draft + New + Notes + Started + 1st Draft + 2nd Draft 3rd Draft - Finished + Finished None @@ -63,34 +65,34 @@ - Part One + Part One Chapter One - + Making a Scene - + Another Scene - Interlude + Interlude - A Note on Structure + A Note on Structure - + Chapter Two - + We Found John! @@ -99,7 +101,7 @@ - Title Page + Title Page diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index 02e3df49..a8cf6393 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -16,6 +16,8 @@ True 7a992350f3eb6 None + None + None 3847 3109 738 diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index af7595a4..430ecf17 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -17,6 +17,8 @@ True None None + None + None 10 10 0 diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index be4845bf..f046fceb 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -17,6 +17,8 @@ True None None + None + None 0 0 0 diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 12f911cd..5d02172c 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -17,6 +17,8 @@ True None None + None + None 0 0 0 diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx similarity index 75% rename from tests/reference/coreProject_NewFile_nwProject.nwx rename to tests/reference/coreProject_NewFileFolder_nwProject.nwx index 19a2f4bf..20aeb027 100644 --- a/tests/reference/coreProject_NewFile_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,9 +16,11 @@ True None None - 0 - 0 - 0 + None + None + 2 + 1 + 1 %title% @@ -28,19 +30,19 @@
- New + New Note Draft Finished - New + New Minor Major Main
- + Novel @@ -73,13 +75,17 @@ New Scene - - - Hello + + + Stuff - - - Jane + + + Hello + + + + Jane
diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 422bb6b2..ba08600f 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -15,6 +15,8 @@ True None None + None + None 0 0 0 diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index cd25c1cf..0a606137 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,6 +16,8 @@ True None None + None + None 0 0 0 diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index a47d454d..ecc96604 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,6 +16,8 @@ True 000000000000f None + 0000000000008 + None 129 102 27 diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 03313570..1a79d2c2 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,6 +16,8 @@ True None None + None + None 9 9 0 diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index f12c1dc2..883cb26d 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -17,6 +17,8 @@ True None None + None + None 9 9 0 diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 3d40a9df..8f126e56 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -232,9 +232,6 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd): assert theIndex.getHandleHeaderLevel(cHandle) == "H1" assert theIndex.getHandleHeaderLevel(nHandle) == "H1" - assert theIndex.getHandleHeaderIntLevel(cHandle) == 1 - assert theIndex.getHandleHeaderIntLevel(nHandle) == 1 - assert theIndex.getHandleHeaderIntLevel("stuff") == 0 # Zero Items assert theIndex.checkThese([], cItem) == [] diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 005277c6..a6409b98 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -29,10 +29,14 @@ from lxml import etree from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE from mock import causeOSError -from novelwriter.core.project import NWProject from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.common import formatTimeStamp from novelwriter.constants import nwFiles +from novelwriter.core.tree import NWTree +from novelwriter.core.index import NWIndex +from novelwriter.core.project import NWProject +from novelwriter.core.options import OptionState +from novelwriter.core.document import NWDoc @pytest.mark.core @@ -280,12 +284,12 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd): @pytest.mark.core -def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI, mockRnd): +def testCoreProject_NewFileFolder(fncDir, outDir, refDir, mockGUI, mockRnd): """Check that new files can be added to the project. """ projFile = os.path.join(fncDir, "nwProject.nwx") - testFile = os.path.join(outDir, "coreProject_NewFile_nwProject.nwx") - compFile = os.path.join(refDir, "coreProject_NewFile_nwProject.nwx") + testFile = os.path.join(outDir, "coreProject_NewFileFolder_nwProject.nwx") + compFile = os.path.join(refDir, "coreProject_NewFileFolder_nwProject.nwx") theProject = NWProject(mockGUI) buildTestProject(theProject, fncDir) @@ -295,9 +299,33 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI, mockRnd): assert theProject.closeProject() is True assert theProject.openProject(projFile) is True - assert isinstance(theProject.newFile("Hello", "31489056e0916"), str) - assert isinstance(theProject.newFile("Jane", "71ee45a3c0db9"), str) - assert theProject.projChanged + # Invalid call + assert theProject.newFolder("New Folder", "1234567890abc") is None + assert theProject.newFile("New File", "1234567890abc") is None + + # Add files properly + assert theProject.newFolder("Stuff", "0000000000015") == "0000000000028" + assert theProject.newFile("Hello", "0000000000015") == "0000000000029" + assert theProject.newFile("Jane", "0000000000012") == "000000000002a" + + assert "0000000000028" in theProject.tree + assert "0000000000029" in theProject.tree + assert "000000000002a" in theProject.tree + + # Write to file, failed + assert theProject.writeNewFile("blabla", 1, True) is False # Not a handle + assert theProject.writeNewFile("0000000000028", 1, True) is False # Not a file + assert theProject.writeNewFile("0000000000014", 1, True) is False # Already has content + + # Write to file, success + assert theProject.writeNewFile("0000000000029", 2, True) is True + assert NWDoc(theProject, "0000000000029").readDocument() == "## Hello\n\n" + + assert theProject.writeNewFile("000000000002a", 1, False) is True + assert NWDoc(theProject, "000000000002a").readDocument() == "# Jane\n\n" + + # Save, close and check + assert theProject.projChanged is True assert theProject.saveProject() is True assert theProject.closeProject() is True @@ -305,7 +333,7 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI, mockRnd): assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) assert theProject.projChanged is False -# END Test testCoreProject_NewFile +# END Test testCoreProject_NewFileFolder @pytest.mark.core @@ -435,12 +463,15 @@ def testCoreProject_Open(monkeypatch, nwMinimal, mockGUI): os.rename(oName, rName) # Add some legacy stuff that cannot be removed - writeFile(os.path.join(nwMinimal, "junk"), "stuff") - os.mkdir(os.path.join(nwMinimal, "data_0")) - writeFile(os.path.join(nwMinimal, "data_0", "junk"), "stuff") - mockGUI.clear() - assert theProject.openProject(nwMinimal) is True - assert "data_0" in mockGUI.lastAlert + with monkeypatch.context() as mp: + mp.setattr(theProject, "_legacyDataFolder", causeOSError) + os.mkdir(os.path.join(nwMinimal, "data_0")) + writeFile(os.path.join(nwMinimal, "data_0", "123456789abc_main.nwd"), "stuff") + writeFile(os.path.join(nwMinimal, "data_0", "123456789abc_main.bak"), "stuff") + mockGUI.clear() + assert theProject.openProject(nwMinimal) is True + assert "There was an error updating the project." in mockGUI.lastAlert + assert theProject.closeProject() # END Test testCoreProject_Open @@ -613,6 +644,11 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI): theProject = NWProject(mockGUI) theProject.openProject(nwMinimal) + # Storage Objects + assert isinstance(theProject.index, NWIndex) + assert isinstance(theProject.tree, NWTree) + assert isinstance(theProject.options, OptionState) + # Move Novel ROOT to after its files oldOrder = [ "a508bb932959c", # ROOT: Novel @@ -722,7 +758,7 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): # Change Importance # ================= - fHandle = theProject.newFile("Jane Doe", "8b9d2e465e150") + fHandle = theProject.newFile("Jane Doe", "0000000000012") theProject.tree[fHandle].setImport("Main") assert theProject.tree[fHandle].itemImport == importKeys[3] @@ -894,6 +930,10 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.setProjectLang("en_GB") is True assert theProject.projLang == "en_GB" + # Language Lookup + assert theProject.localLookup(1) == "One" + assert theProject.localLookup(10) == "Ten" + # Automatic outline update theProject.projChanged = False assert theProject.setAutoOutline(True) @@ -1007,7 +1047,8 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): # Add a file with non-existent parent # This file will be renoved from the project on open - assert theProject.newFile("Oops", "0000000000000") + oHandle = theProject.newFile("Oops", "b3643d0f92e32") + theProject.tree[oHandle].setParent("1234567890abc") # Save and close assert theProject.saveProject() is True @@ -1103,14 +1144,6 @@ def testCoreProject_OldFormat(mockGUI, nwOldProj): os.path.join(nwOldProj, "meta", "sessionLogOptions.json"), ] - # Add some files that shouldn't be there - deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.nwd")) - deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.txt")) - - # Add some folders that shouldn't be there - os.mkdir(os.path.join(nwOldProj, "stuff")) - os.mkdir(os.path.join(nwOldProj, "data_1", "stuff")) - # Create mock files os.mkdir(os.path.join(nwOldProj, "cache")) for aFile in deleteFiles: @@ -1124,7 +1157,6 @@ def testCoreProject_OldFormat(mockGUI, nwOldProj): for aFile in deleteFiles: assert not os.path.isfile(aFile) - assert not os.path.isdir(os.path.join(nwOldProj, "data_1", "stuff")) assert not os.path.isdir(os.path.join(nwOldProj, "data_1")) assert not os.path.isdir(os.path.join(nwOldProj, "data_7")) assert not os.path.isdir(os.path.join(nwOldProj, "data_8")) @@ -1132,12 +1164,6 @@ def testCoreProject_OldFormat(mockGUI, nwOldProj): assert not os.path.isdir(os.path.join(nwOldProj, "data_a")) assert not os.path.isdir(os.path.join(nwOldProj, "data_f")) - # Check stuff that has been moved - assert os.path.isdir(os.path.join(nwOldProj, "junk")) - assert os.path.isdir(os.path.join(nwOldProj, "junk", "stuff")) - assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.nwd")) - assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.txt")) - # Check that files we want to keep are in the right place assert os.path.isdir(os.path.join(nwOldProj, "cache")) assert os.path.isdir(os.path.join(nwOldProj, "content")) @@ -1179,7 +1205,7 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir): with monkeypatch.context() as mp: mp.setattr("os.unlink", causeOSError) - assert not theProject._deprecatedFiles() + assert theProject._deprecatedFiles() is False assert theProject._deprecatedFiles() assert not os.path.isfile(tstFile) @@ -1188,63 +1214,36 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir): tstFile = os.path.join(fncDir, "data_0") writeFile(tstFile, "stuff") assert os.path.isfile(tstFile) - - errList = [] - errList = theProject._legacyDataFolder(tstFile, errList) - assert len(errList) > 0 - - # Move folder in data folder, shouldn't be there - tstData = os.path.join(fncDir, "data_1") - errItem = os.path.join(fncDir, "data_1", "stuff") - os.mkdir(tstData) - os.mkdir(errItem) - assert os.path.isdir(tstData) - assert os.path.isdir(errItem) - - # This causes a failure to create the 'junk' folder - with monkeypatch.context() as mp: - mp.setattr("os.mkdir", causeOSError) - errList = [] - errList = theProject._legacyDataFolder(tstData, errList) - assert len(errList) > 0 - - # This causes a failure to move 'stuff' to 'junk' - with monkeypatch.context() as mp: - mp.setattr("os.rename", causeOSError) - errList = [] - errList = theProject._legacyDataFolder(tstData, errList) - assert len(errList) > 0 - - # This should be successful - errList = [] - errList = theProject._legacyDataFolder(tstData, errList) - assert len(errList) == 0 - assert os.path.isdir(os.path.join(fncDir, "junk", "stuff")) + assert theProject._legacyDataFolder(tstFile) is False # Check renaming/deleting of old document files - tstData = os.path.join(fncDir, "data_2") - tstDoc1m = os.path.join(tstData, "000000000001_main.nwd") - tstDoc1b = os.path.join(tstData, "000000000001_main.bak") - tstDoc2m = os.path.join(tstData, "000000000002_main.nwd") - tstDoc2b = os.path.join(tstData, "000000000002_main.bak") - tstDoc3m = os.path.join(tstData, "tooshort003_main.nwd") - tstDoc3b = os.path.join(tstData, "tooshort003_main.bak") + tstData2 = os.path.join(fncDir, "data_2") + tstData3 = os.path.join(fncDir, "data_3") + tstDoc1m = os.path.join(tstData2, "000000000001_main.nwd") + tstDoc1b = os.path.join(tstData2, "000000000001_main.bak") + tstDoc2m = os.path.join(tstData2, "000000000002_main.nwd") + tstDoc2b = os.path.join(tstData2, "000000000002_main.bak") + tstDoc3m = os.path.join(tstData3, "tooshort003_main.nwd") + tstDoc3b = os.path.join(tstData3, "tooshort003_main.bak") + tstDir4a = os.path.join(tstData3, "stuff") - os.mkdir(tstData) + os.mkdir(tstData2) + os.mkdir(tstData3) writeFile(tstDoc1m, "stuff") writeFile(tstDoc1b, "stuff") writeFile(tstDoc2m, "stuff") writeFile(tstDoc2b, "stuff") writeFile(tstDoc3m, "stuff") writeFile(tstDoc3b, "stuff") + os.mkdir(tstDir4a) # Make the above fail with monkeypatch.context() as mp: mp.setattr("os.rename", causeOSError) mp.setattr("os.unlink", causeOSError) - errList = [] - errList = theProject._legacyDataFolder(tstData, errList) - assert len(errList) > 0 + with pytest.raises(OSError): + theProject._legacyDataFolder(tstData2) + theProject._legacyDataFolder(tstData3) assert os.path.isfile(tstDoc1m) assert os.path.isfile(tstDoc1b) assert os.path.isfile(tstDoc2m) @@ -1253,15 +1252,16 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir): assert os.path.isfile(tstDoc3b) # And succeed ... - errList = [] - errList = theProject._legacyDataFolder(tstData, errList) - assert len(errList) == 0 + assert theProject._legacyDataFolder(tstData2) is True + assert theProject._legacyDataFolder(tstData3) is True - assert not os.path.isdir(tstData) + assert not os.path.isdir(tstData2) + assert os.path.isdir(tstData3) assert os.path.isfile(os.path.join(fncDir, "content", "2000000000001.nwd")) assert os.path.isfile(os.path.join(fncDir, "content", "2000000000002.nwd")) - assert os.path.isfile(os.path.join(fncDir, "junk", "tooshort003_main.nwd")) - assert os.path.isfile(os.path.join(fncDir, "junk", "tooshort003_main.bak")) + assert os.path.isfile(os.path.join(fncDir, tstData3, "tooshort003_main.nwd")) + assert os.path.isfile(os.path.join(fncDir, tstData3, "tooshort003_main.bak")) + assert os.path.isdir(tstDir4a) # END Test testCoreProject_LegacyData diff --git a/tests/test_dialogs/test_dlg_about.py b/tests/test_dialogs/test_dlg_about.py index c5ccfe7f..a4623c1f 100644 --- a/tests/test_dialogs/test_dlg_about.py +++ b/tests/test_dialogs/test_dlg_about.py @@ -36,8 +36,8 @@ def testDlgAbout_NWDialog(qtbot, monkeypatch, nwGUI): monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) # NW About - nwGUI.theTheme.themeName = "A Theme" - nwGUI.theTheme.themeAuthor = "An Author" + nwGUI.mainTheme.themeName = "A Theme" + nwGUI.mainTheme.themeAuthor = "An Author" assert nwGUI.showAboutNWDialog(showNotes=True) is True qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000) diff --git a/tests/test_dialogs/test_dlg_dialogs.py b/tests/test_dialogs/test_dlg_dialogs.py index 7ae56d94..3423beb5 100644 --- a/tests/test_dialogs/test_dlg_dialogs.py +++ b/tests/test_dialogs/test_dlg_dialogs.py @@ -24,11 +24,7 @@ import pytest from PyQt5.QtCore import QItemSelectionModel from PyQt5.QtWidgets import QAction, QListWidgetItem, QDialog, QMessageBox -from novelwriter.dialogs import GuiQuoteSelect, GuiUpdates - -keyDelay = 2 -typeDelay = 1 -stepDelay = 20 +from novelwriter.dialogs import GuiQuoteSelect, GuiUpdates, GuiEditLabel @pytest.mark.gui @@ -101,3 +97,24 @@ def testDlgOther_Updates(qtbot, monkeypatch, nwGUI): nwUpdate._doClose() # END Test testDlgOther_Updates + + +@pytest.mark.gui +def testDlgOther_EditLabel(qtbot, monkeypatch): + """Test the label editor dialog. + """ + monkeypatch.setattr(GuiEditLabel, "exec_", lambda *a: None) + + with monkeypatch.context() as mp: + mp.setattr(GuiEditLabel, "result", lambda *a: QDialog.Accepted) + newLabel, dlgOk = GuiEditLabel.getLabel(None, text="Hello World") + assert dlgOk is True + assert newLabel == "Hello World" + + with monkeypatch.context() as mp: + mp.setattr(GuiEditLabel, "result", lambda *a: QDialog.Rejected) + newLabel, dlgOk = GuiEditLabel.getLabel(None, text="Hello World") + assert dlgOk is False + assert newLabel == "Hello World" + +# END Test testDlgOther_EditLabel diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index a82c77bd..4e3138f6 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -25,10 +25,10 @@ import pytest from mock import causeOSError from tools import getGuiItem, readFile, writeFile, buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox, QDialog +from PyQt5.QtWidgets import QAction, QMessageBox from novelwriter.enum import nwItemType, nwWidget -from novelwriter.dialogs import GuiDocMerge, GuiItemEditor +from novelwriter.dialogs import GuiDocMerge, GuiEditLabel from novelwriter.core.tree import NWTree @@ -39,6 +39,7 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) # Create a new project buildTestProject(nwGUI, fncProj) @@ -54,13 +55,12 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): hMergedDoc = "0000000000023" # Add Project Content - monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted) nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE) - nwGUI.treeView.newTreeItem(nwItemType.FILE) - nwGUI.treeView.newTreeItem(nwItemType.FILE) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) assert nwGUI.saveProject() is True assert nwGUI.closeProject() is True @@ -82,8 +82,8 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Open the Merge tool nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True) monkeypatch.setattr(GuiDocMerge, "exec_", lambda *a: None) nwGUI.mainMenu.aMergeDocs.activate(QAction.Trigger) @@ -101,27 +101,27 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwMerge.listBox.count() == 0 # No item selected - nwGUI.treeView.clearSelection() + nwGUI.projView.projTree.clearSelection() assert nwMerge._populateList() is False assert nwMerge.listBox.count() == 0 # Non-existing item with monkeypatch.context() as mp: mp.setattr(NWTree, "__getitem__", lambda *a: None) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True) assert nwMerge._populateList() is False assert nwMerge.listBox.count() == 0 # Select a non-folder - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterOne).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hChapterOne).setSelected(True) assert nwMerge._populateList() is False assert nwMerge.listBox.count() == 0 # Select the chapter folder - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True) assert nwMerge._populateList() is True assert nwMerge.listBox.count() == 5 diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index 90e3375d..d0201d6a 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -25,10 +25,10 @@ import pytest from mock import causeOSError from tools import getGuiItem, readFile, writeFile, buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox, QDialog +from PyQt5.QtWidgets import QAction, QMessageBox from novelwriter.enum import nwItemType, nwWidget -from novelwriter.dialogs import GuiDocSplit, GuiItemEditor +from novelwriter.dialogs import GuiDocSplit, GuiEditLabel from novelwriter.core.tree import NWTree from novelwriter.core.document import NWDoc @@ -40,6 +40,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) # Create a new project buildTestProject(nwGUI, fncProj) @@ -58,11 +59,10 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): hSceneFive = "0000000000028" # Add Project Content - monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted) nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hNovelRoot).setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hNovelRoot).setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) assert nwGUI.saveProject() is True assert nwGUI.closeProject() is True @@ -89,8 +89,8 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Open the Split tool nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hToSplit).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hToSplit).setSelected(True) monkeypatch.setattr(GuiDocSplit, "exec_", lambda *a: None) nwGUI.mainMenu.aSplitDoc.activate(QAction.Trigger) @@ -109,7 +109,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # No item selected nwSplit.sourceItem = None - nwGUI.treeView.clearSelection() + nwGUI.projView.projTree.clearSelection() assert nwSplit._populateList() is False assert nwSplit.listBox.count() == 0 @@ -117,15 +117,15 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): with monkeypatch.context() as mp: mp.setattr(NWTree, "__getitem__", lambda *a: None) nwSplit.sourceItem = None - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hToSplit).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hToSplit).setSelected(True) assert nwSplit._populateList() is False assert nwSplit.listBox.count() == 0 # Select a non-file nwSplit.sourceItem = None - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem(hChapterDir).setSelected(True) assert nwSplit._populateList() is False assert nwSplit.listBox.count() == 0 diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py deleted file mode 100644 index 95b1ee71..00000000 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ /dev/null @@ -1,245 +0,0 @@ -""" -novelWriter – Item Editor Dialog Class Tester -============================================= - -This file is a part of novelWriter -Copyright 2018–2022, 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 pytest - -from tools import getGuiItem, buildTestProject - -from PyQt5.QtWidgets import QAction, QDialog, QMessageBox - -from novelwriter.gui import GuiProjectTree -from novelwriter.enum import nwItemLayout, nwItemType -from novelwriter.dialogs import GuiItemEditor -from novelwriter.core.tree import NWTree - -statusKeys = ["s000000", "s000001", "s000002", "s000003"] -importKeys = ["i000004", "i000005", "i000006", "i000007"] - - -@pytest.mark.gui -def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): - """Test launching the item editor dialog from GuiMain. - """ - # Block message box - monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - - # Block Dialog exec_ - monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: None) - - # Open Editor wo/Project - assert nwGUI.editItem() is False - - # Create and Open Project - buildTestProject(nwGUI, fncProj) - tHandle = "000000000000f" - - # No Selection - nwGUI.treeView.clearSelection() - assert nwGUI.editItem() is False - - # Force opening from editor - assert nwGUI.openDocument(tHandle) - nwGUI.isFocusMode = True - - # Block Tree Lookup - with monkeypatch.context() as mp: - mp.setattr(NWTree, "__getitem__", lambda *a: None) - assert nwGUI.editItem() is False - - # Invalid Type - nwGUI.theProject.tree[tHandle]._type = nwItemType.NO_TYPE - assert nwGUI.editItem() is False - nwGUI.theProject.tree[tHandle]._type = nwItemType.FILE - - # Open Properly - assert nwGUI.editItem() is True - qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000) - itemEdit = getGuiItem("GuiItemEditor") - assert itemEdit is not None - itemEdit.close() - - # Open Via Menu - with monkeypatch.context() as mp: - mp.setattr(GuiItemEditor, "result", lambda *a: QDialog.Accepted) - nwGUI.mainMenu.aEditItem.activate(QAction.Trigger) - qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000) - itemEdit = getGuiItem("GuiItemEditor") - assert itemEdit is not None - itemEdit.close() - - nwGUI.isFocusMode = False - -# END Test testDlgItemEditor_Dialog - - -@pytest.mark.gui -def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): - """Test the item editor dialog for a novel document. - """ - # Block message box - monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) - - # Create Project and Open Document - buildTestProject(nwGUI, fncProj) - tHandle = "000000000000f" - - assert nwGUI.theProject.statusItems.name(statusKeys[0]) == "New" - assert nwGUI.theProject.statusItems.name(statusKeys[1]) == "Note" - - assert nwGUI.openDocument(tHandle) is True - - # Check that an invalid handle is managed - itemEdit = GuiItemEditor(nwGUI, "whatever") - itemEdit.show() - itemEdit._doClose() - - # Edit a Document - itemEdit = GuiItemEditor(nwGUI, tHandle) - itemEdit.show() - - # Check Existing Settings - assert itemEdit.editName.text() == "New Scene" - assert itemEdit.editStatus.currentData() == statusKeys[0] - assert itemEdit.editLayout.currentData() == nwItemLayout.DOCUMENT - assert itemEdit.editExport.isChecked() is True - - # Change Settings - layoutIdx = itemEdit.editLayout.findData(nwItemLayout.NOTE) - itemEdit.editName.setText("Great Scene") - itemEdit.editStatus.setCurrentIndex(1) - itemEdit.editLayout.setCurrentIndex(layoutIdx) - itemEdit.editExport.setChecked(False) - - # Check New Settings - itemEdit._doSave() - assert itemEdit.theItem.itemName == "Great Scene" - assert itemEdit.theItem.itemStatus == statusKeys[1] - assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE - assert itemEdit.theItem.isExported is False - - # Check that the editor header is updated - nwGUI.docEditor.updateDocInfo(tHandle) - assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel › New Chapter › Great Scene" - - itemEdit.close() - del itemEdit - # qtbot.stopForInteraction() - -# END Test testDlgItemEditor_Dialog - - -@pytest.mark.gui -def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): - """Test the item editor dialog for a project note. - """ - # Block message box - monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) - - # Create Project and Open Document - buildTestProject(nwGUI, fncProj) - assert nwGUI.theProject.statusItems.name(statusKeys[0]) == "New" - assert nwGUI.theProject.statusItems.name(statusKeys[1]) == "Note" - assert nwGUI.theProject.importItems.name(importKeys[0]) == "New" - assert nwGUI.theProject.importItems.name(importKeys[1]) == "Minor" - - # Create Note - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("000000000000a").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) - - # Open Note - assert nwGUI.openDocument("0000000000010") - - # Edit a Document - itemEdit = GuiItemEditor(nwGUI, "0000000000010") - itemEdit.show() - - # Check Existing Settings - assert itemEdit.editName.text() == "New Note" - assert itemEdit.editStatus.currentData() == importKeys[0] - assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE - assert itemEdit.editExport.isChecked() is True - - # Change Settings - itemEdit.editName.setText("New Character") - itemEdit.editStatus.setCurrentIndex(1) - itemEdit.editExport.setChecked(False) - itemEdit._doSave() - - # Check New Settings - assert itemEdit.theItem.itemName == "New Character" - assert itemEdit.theItem.itemStatus == statusKeys[0] - assert itemEdit.theItem.itemImport == importKeys[1] - assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE - assert itemEdit.theItem.isExported is False - - itemEdit.close() - del itemEdit - # qtbot.stopForInteraction() - -# END Test testDlgItemEditor_Note - - -@pytest.mark.gui -def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): - """Test the item editor dialog for a folder. - """ - # Block message box - monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) - - # Create Project and Open Document - buildTestProject(nwGUI, fncProj) - - # Edit a Folder - itemEdit = GuiItemEditor(nwGUI, "000000000000d") - itemEdit.show() - - assert nwGUI.theProject.statusItems.name(statusKeys[0]) == "New" - assert nwGUI.theProject.statusItems.name(statusKeys[1]) == "Note" - - # Check Existing Settings - assert itemEdit.editName.text() == "New Chapter" - assert itemEdit.editStatus.currentData() == statusKeys[0] - assert itemEdit.editLayout.currentData() == nwItemLayout.NO_LAYOUT - assert itemEdit.editExport.isChecked() is False - - assert itemEdit.editLayout.isEnabled() is False - assert itemEdit.editExport.isEnabled() is False - - # Change Settings - itemEdit.editName.setText("Chapter One") - itemEdit.editStatus.setCurrentIndex(1) - - # Check New Settings - itemEdit._doSave() - assert itemEdit.theItem.itemName == "Chapter One" - assert itemEdit.theItem.itemStatus == statusKeys[1] - assert itemEdit.theItem.itemLayout == nwItemLayout.NO_LAYOUT - assert itemEdit.theItem.isExported is False - - itemEdit.close() - del itemEdit - # qtbot.stopForInteraction() - -# END Test testDlgItemEditor_Folder diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 1727b69f..d4a3d749 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -1147,7 +1147,7 @@ def testGuiEditor_Tags(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): assert nwGUI.openDocument(cHandle) is True assert nwGUI.docEditor.replaceText(theText) is True assert nwGUI.saveDocument() is True - assert nwGUI.treeView.revealNewTreeItem(cHandle) + assert nwGUI.projView.revealNewTreeItem(cHandle) nwGUI.docEditor.updateTagHighLighting() # Follow Tag diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 1fa6b2c7..2226a4c3 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -51,12 +51,12 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): assert nwGUI.theProject.index._itemIndex._items != {} # Select a document in the project tree - nwGUI.treeView.setSelectedHandle("88243afbe5ed8") + nwGUI.projView.setSelectedHandle("88243afbe5ed8") # Middle-click the selected item - theItem = nwGUI.treeView._getTreeItem("88243afbe5ed8") - theRect = nwGUI.treeView.visualItemRect(theItem) - qtbot.mouseClick(nwGUI.treeView.viewport(), Qt.MidButton, pos=theRect.center()) + theItem = nwGUI.projView.projTree._getTreeItem("88243afbe5ed8") + theRect = nwGUI.projView.projTree.visualItemRect(theItem) + qtbot.mouseClick(nwGUI.projView.projTree.viewport(), Qt.MidButton, pos=theRect.center()) assert nwGUI.docViewer.docHandle() == "88243afbe5ed8" # Reload the text @@ -117,7 +117,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): assert nwGUI.docViewer.docAction(nwDocAction.COPY) is False # Open again via menu - assert nwGUI.treeView.setSelectedHandle("88243afbe5ed8") + assert nwGUI.projView.setSelectedHandle("88243afbe5ed8") nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger) # Select "Bod" link diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 5f49d51d..6407b84b 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -26,14 +26,13 @@ from shutil import copyfile from tools import cmpFiles, buildTestProject, XML_IGNORE, writeFile from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMessageBox, QDialog +from PyQt5.QtWidgets import QMessageBox, QInputDialog -from novelwriter.gui import ( - GuiDocEditor, GuiProjectTree, GuiNovelTree, GuiOutline -) -from novelwriter.enum import nwItemType, nwWidget +from novelwriter.gui import GuiDocEditor, GuiNovelView, GuiOutlineView +from novelwriter.enum import nwItemType, nwView, nwWidget from novelwriter.tools import GuiProjectWizard -from novelwriter.dialogs.itemeditor import GuiItemEditor +from novelwriter.gui.projtree import GuiProjectTree +from novelwriter.dialogs import GuiEditLabel keyDelay = 2 typeDelay = 1 @@ -58,7 +57,7 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI): assert nwGUI.mergeDocuments() is False assert nwGUI.splitDocument() is False assert nwGUI.openSelectedItem() is False - assert nwGUI.editItem() is False + assert nwGUI.editItemLabel() is False assert nwGUI.requestNovelTreeRefresh() is False assert nwGUI.rebuildIndex() is False assert nwGUI.showProjectSettingsDialog() is False @@ -121,44 +120,44 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.openSelectedItem() is False # Project Tree has focus + nwGUI._changeView(nwView.PROJECT) nwGUI.switchFocus(nwWidget.TREE) nwGUI.projStack.setCurrentIndex(0) with monkeypatch.context() as mp: mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None - nwGUI.treeView._getTreeItem(sHandle).setSelected(True) + nwGUI.projView.projTree._getTreeItem(sHandle).setSelected(True) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True # Novel Tree has focus - nwGUI.projStack.setCurrentIndex(1) - nwGUI.novelView.refreshTree(True) + nwGUI._changeView(nwView.NOVEL) + nwGUI.novelView.novelTree.refreshTree(rootHandle=None, overRide=True) with monkeypatch.context() as mp: - mp.setattr(GuiNovelTree, "hasFocus", lambda *a: True) + mp.setattr(GuiNovelView, "treeHasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None - actItem = nwGUI.novelView.topLevelItem(0) - chpItem = actItem.child(0) - selItem = chpItem.child(0) - nwGUI.novelView.setCurrentItem(selItem) + selItem = nwGUI.novelView.novelTree.topLevelItem(2) + nwGUI.novelView.novelTree.setCurrentItem(selItem) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True # Project Outline has focus + nwGUI._changeView(nwView.OUTLINE) nwGUI.switchFocus(nwWidget.OUTLINE) with monkeypatch.context() as mp: - mp.setattr(GuiOutline, "treeFocus", lambda *a: True) + mp.setattr(GuiOutlineView, "treeHasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None - actItem = nwGUI.projView.outlineView.topLevelItem(0) + actItem = nwGUI.outlineView.outlineTree.topLevelItem(0) chpItem = actItem.child(0) selItem = chpItem.child(0) - nwGUI.projView.outlineView.setCurrentItem(selItem) + nwGUI.outlineView.outlineTree.setCurrentItem(selItem) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True - # qtbot.stopForInteraction() + # qtbot.stop() # END Test testGuiMain_ProjectTreeItems @@ -170,10 +169,10 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: None) - monkeypatch.setattr(GuiItemEditor, "result", lambda *a: QDialog.Accepted) monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) # Create new, save, close project buildTestProject(nwGUI, fncProj) @@ -220,14 +219,14 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.spellCheck is False # Check that tree items have been created - assert nwGUI.treeView._getTreeItem("0000000000008") is not None - assert nwGUI.treeView._getTreeItem("0000000000009") is not None - assert nwGUI.treeView._getTreeItem("000000000000a") is not None - assert nwGUI.treeView._getTreeItem("000000000000b") is not None - assert nwGUI.treeView._getTreeItem("000000000000c") is not None - assert nwGUI.treeView._getTreeItem("000000000000d") is not None - assert nwGUI.treeView._getTreeItem("000000000000e") is not None - assert nwGUI.treeView._getTreeItem("000000000000f") is not None + assert nwGUI.projView.projTree._getTreeItem("0000000000008") is not None + assert nwGUI.projView.projTree._getTreeItem("0000000000009") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000a") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000b") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000c") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000d") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000e") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000f") is not None nwGUI.mainMenu.aSpellCheck.setChecked(True) assert nwGUI.mainMenu._toggleSpellCheck() @@ -240,9 +239,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Add a Character File nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("000000000000a").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) assert nwGUI.openSelectedItem() # Type something into the document @@ -262,9 +261,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Add a Plot File nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("0000000000009").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem("0000000000009").setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) assert nwGUI.openSelectedItem() # Type something into the document @@ -284,9 +283,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Add a World File nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("000000000000b").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem("000000000000b").setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) assert nwGUI.openSelectedItem() # Add Some Text @@ -315,10 +314,10 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Select the 'New Scene' file nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("0000000000008").setExpanded(True) - nwGUI.treeView._getTreeItem("000000000000d").setExpanded(True) - nwGUI.treeView._getTreeItem("000000000000f").setSelected(True) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem("0000000000008").setExpanded(True) + nwGUI.projView.projTree._getTreeItem("000000000000d").setExpanded(True) + nwGUI.projView.projTree._getTreeItem("000000000000f").setSelected(True) assert nwGUI.openSelectedItem() # Type something into the document @@ -461,12 +460,12 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock qtbot.wait(stepDelay) # Check a Quick Create and Delete - assert nwGUI.treeView.newTreeItem(nwItemType.FILE, None) - newHandle = nwGUI.treeView.getSelectedHandle() + assert nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None) + newHandle = nwGUI.projView.getSelectedHandle() assert nwGUI.theProject.tree["0000000000020"] is not None - assert nwGUI.treeView.deleteItem() - assert nwGUI.treeView.setSelectedHandle(newHandle) - assert nwGUI.treeView.deleteItem() + assert nwGUI.projView.deleteItem() + assert nwGUI.projView.setSelectedHandle(newHandle) + assert nwGUI.projView.deleteItem() assert nwGUI.theProject.tree["0000000000024"] is not None # Trash assert nwGUI.saveProject() diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 801abd68..f579a1a0 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -467,7 +467,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): buildTestProject(nwGUI, fncProj) - assert nwGUI.treeView._getTreeItem("000000000000f") is not None + assert nwGUI.projView.projTree._getTreeItem("000000000000f") is not None assert nwGUI.openDocument("000000000000f") is True nwGUI.docEditor.clear() @@ -476,10 +476,10 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): assert nwGUI.docEditor.getText() == "hello world" nwGUI.docEditor.clear() - assert not nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) + assert nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) is False assert nwGUI.docEditor.isEmpty() - assert not nwGUI.docEditor.insertText(None) + assert nwGUI.docEditor.insertText(None) is False assert nwGUI.docEditor.isEmpty() # qtbot.stopForInteraction() diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 2bf2df8a..102b03d2 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -19,73 +19,93 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import pytest import os +import pytest -from tools import writeFile +from tools import buildTestProject, writeFile from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox +from novelwriter.enum import nwWidget, nwItemType +from novelwriter.dialogs import GuiEditLabel +from novelwriter.gui.noveltree import NovelTreeColumn + @pytest.mark.gui -def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): +def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """Test navigating the novel tree. """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - nwGUI.openProject(nwMinimal) - nwTree = nwGUI.novelView + buildTestProject(nwGUI, fncProj) - ## - # Show/Hide Scrollbars - ## + nwGUI.switchFocus(nwWidget.TREE) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) - nwTree.mainConf.hideVScroll = True - nwTree.mainConf.hideHScroll = True - nwTree.initTree() - assert not nwTree.verticalScrollBar().isVisible() - assert not nwTree.horizontalScrollBar().isVisible() + writeFile( + os.path.join(nwGUI.theProject.projContent, "0000000000010.nwd"), + "# Jane Doe\n\n@tag: Jane\n\n" + ) + writeFile( + os.path.join(nwGUI.theProject.projContent, "000000000000f.nwd"), + "### Scene One\n\n@pov: Jane\n@focus: Jane\n\n" + ) - nwTree.mainConf.hideVScroll = False - nwTree.mainConf.hideHScroll = False - nwTree.initTree() - assert nwTree.verticalScrollBar().isEnabled() - assert nwTree.horizontalScrollBar().isEnabled() + novelView = nwGUI.novelView + novelTree = novelView.novelTree + novelBar = novelView.novelBar - ## - # Populate Tree - ## + # Show/Hide Scrollbars + # ==================== + + nwGUI.mainConf.hideVScroll = True + nwGUI.mainConf.hideHScroll = True + novelView.initSettings() + assert not novelTree.verticalScrollBar().isVisible() + assert not novelTree.horizontalScrollBar().isVisible() + + nwGUI.mainConf.hideVScroll = False + nwGUI.mainConf.hideHScroll = False + novelView.initSettings() + assert novelTree.verticalScrollBar().isEnabled() + assert novelTree.horizontalScrollBar().isEnabled() + + # Populate Tree + # ============= nwGUI.projStack.setCurrentIndex(nwGUI.idxNovelView) nwGUI.rebuildIndex() - nwTree._populateTree() - assert nwTree.topLevelItemCount() == 1 + novelTree._populateTree(rootHandle=None) + assert novelTree.topLevelItemCount() == 3 # Rebuild should preserve selection - topItem = nwTree.topLevelItem(0) + topItem = novelTree.topLevelItem(0) assert not topItem.isSelected() topItem.setSelected(True) - assert nwTree.selectedItems()[0] == topItem - assert nwTree.getSelectedHandle() == ("a35baf2e93843", 0) + assert novelTree.selectedItems()[0] == topItem + assert novelView.getSelectedHandle() == ("000000000000c", 0) - nwTree.refreshTree() - assert nwTree.topLevelItem(0).isSelected() + # Refresh using the slot for the butoom + novelBar._refreshNovelTree() + assert novelTree.topLevelItem(0).isSelected() - ## - # Open Items - ## + # Open Items + # ========== # Clear selection - nwTree.clearSelection() - scItem = nwTree.topLevelItem(0).child(0).child(0) + novelTree.clearSelection() + scItem = novelTree.topLevelItem(2) scItem.setSelected(True) assert scItem.isSelected() # Clear selection with mouse - vPort = nwTree.viewport() + vPort = novelTree.viewport() qtbot.mouseClick(vPort, Qt.LeftButton, pos=vPort.rect().center(), delay=10) assert not scItem.isSelected() @@ -93,8 +113,8 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): scItem.setSelected(True) assert scItem.isSelected() assert nwGUI.docEditor.docHandle() is None - nwTree._treeDoubleClick(scItem, 0) - assert nwGUI.docEditor.docHandle() == "8c659a11cd429" + novelTree._treeDoubleClick(scItem, 0) + assert nwGUI.docEditor.docHandle() == "000000000000f" # Open item with middle mouse button scItem.setSelected(True) @@ -103,58 +123,52 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): qtbot.mouseClick(vPort, Qt.MiddleButton, pos=vPort.rect().center(), delay=10) assert nwGUI.docViewer.docHandle() is None - scRect = nwTree.visualItemRect(scItem) - oldData = scItem.data(nwTree.C_TITLE, Qt.UserRole) - scItem.setData(nwTree.C_TITLE, Qt.UserRole, (None, "", "")) + scRect = novelTree.visualItemRect(scItem) + oldData = scItem.data(novelTree.C_TITLE, Qt.UserRole) + scItem.setData(novelTree.C_TITLE, Qt.UserRole, (None, "", "")) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) assert nwGUI.docViewer.docHandle() is None - scItem.setData(nwTree.C_TITLE, Qt.UserRole, oldData) + scItem.setData(novelTree.C_TITLE, Qt.UserRole, oldData) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) - assert nwGUI.docViewer.docHandle() == "8c659a11cd429" + assert nwGUI.docViewer.docHandle() == "000000000000f" - ## - # Populate Tree - ## + # Last Column + # =========== - # Add weird titles to first file to check hnadling of non-standard - # order of title levels. - writeFile(os.path.join(nwMinimal, "content", "a35baf2e93843.nwd"), ( - "#### Section wo/Scene\n\n" - "### Scene wo/Chapter\n\n" - "## Chapter wo/Title\n\n" - "# Title\n\n" - "#### Section w/Title, wo/Scene\n\n" - "### Scene w/Title, wo/Chapter\n\n" - "## Chapter\n\n" - "#### Section w/Chapter, wo/Scene\n\n" - "### Scene\n\n" - "#### Section\n\n" - )) - nwGUI.rebuildIndex() - nwTree._populateTree() - assert nwTree.topLevelItem(0).text(nwTree.C_TITLE) == "Section wo/Scene" - assert nwTree.topLevelItem(1).text(nwTree.C_TITLE) == "Scene wo/Chapter" - assert nwTree.topLevelItem(2).text(nwTree.C_TITLE) == "Chapter wo/Title" - assert nwTree.topLevelItem(3).text(nwTree.C_TITLE) == "Title" + novelBar.setLastColType(NovelTreeColumn.HIDDEN) + assert novelTree.isColumnHidden(novelTree.C_LAST) is True + assert novelTree.lastColType == NovelTreeColumn.HIDDEN + assert novelTree._getLastColumnText("000000000000f", "T000001") == ("", "") - tTitle = nwTree.topLevelItem(3) - assert tTitle.child(0).text(nwTree.C_TITLE) == "Section w/Title, wo/Scene" - assert tTitle.child(1).text(nwTree.C_TITLE) == "Scene w/Title, wo/Chapter" - assert tTitle.child(2).text(nwTree.C_TITLE) == "Chapter" + novelBar.setLastColType(NovelTreeColumn.POV) + assert novelTree.isColumnHidden(novelTree.C_LAST) is False + assert novelTree.lastColType == NovelTreeColumn.POV + assert novelTree._getLastColumnText("000000000000f", "T000001") == ( + "Jane", "Point of View: Jane" + ) - tChap = tTitle.child(2) - assert tChap.child(0).text(nwTree.C_TITLE) == "Section w/Chapter, wo/Scene" - assert tChap.child(1).text(nwTree.C_TITLE) == "Scene" + novelBar.setLastColType(NovelTreeColumn.FOCUS) + assert novelTree.isColumnHidden(novelTree.C_LAST) is False + assert novelTree.lastColType == NovelTreeColumn.FOCUS + assert novelTree._getLastColumnText("000000000000f", "T000001") == ( + "Jane", "Focus: Jane" + ) - tScene = tChap.child(1) - assert tScene.child(0).text(nwTree.C_TITLE) == "Section" + novelBar.setLastColType(NovelTreeColumn.PLOT) + assert novelTree.isColumnHidden(novelTree.C_LAST) is False + assert novelTree.lastColType == NovelTreeColumn.PLOT + assert novelTree._getLastColumnText("000000000000f", "T000001") == ( + "", "Plot: " + ) - ## - # Close - ## + novelTree._lastCol = None + assert novelTree._getLastColumnText("0000000000000", "T000000") == ("", "") - # qtbot.stopForInteraction() + # Close + # ===== + + # qtbot.stop() nwGUI.closeProject() # END Test testGuiNovelTree_TreeItems diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 49a86b16..26ca9803 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -46,57 +46,57 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): nwGUI.rebuildIndex() nwGUI._changeView(nwView.OUTLINE) - outlineMain = nwGUI.projView - outlineView = outlineMain.outlineView - outlineData = outlineMain.outlineData - outlineMenu = outlineMain.outlineBar.mColumns + outlineView = nwGUI.outlineView + outlineTree = outlineView.outlineTree + outlineData = outlineView.outlineData + outlineMenu = outlineView.outlineBar.mColumns # Toggle scrollbars nwGUI.mainConf.hideVScroll = True nwGUI.mainConf.hideHScroll = True - nwGUI.projView.initOutline() - assert outlineView.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff - assert outlineView.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff + outlineView.initOutline() + assert outlineTree.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff + assert outlineTree.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff nwGUI.mainConf.hideVScroll = False nwGUI.mainConf.hideHScroll = False - nwGUI.projView.initOutline() - assert outlineView.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded - assert outlineView.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded + outlineView.initOutline() + assert outlineTree.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded + assert outlineTree.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded # Check focus with monkeypatch.context() as mp: mp.setattr(QWidget, "hasFocus", lambda *a: True) - assert outlineMain.treeFocus() is True + assert outlineView.treeHasFocus() is True - outlineMain.setTreeFocus() # Can't check. just ensures that it doesn't error + outlineView.setTreeFocus() # Can't check. just ensures that it doesn't error # Option State # ============ pOptions = nwGUI.theProject.options colNames = [h.name for h in nwOutline] colItems = [h for h in nwOutline] - colWidth = {h: outlineView.DEF_WIDTH[h] for h in nwOutline} - colHidden = {h: outlineView.DEF_HIDDEN[h] for h in nwOutline} + colWidth = {h: outlineTree.DEF_WIDTH[h] for h in nwOutline} + colHidden = {h: outlineTree.DEF_HIDDEN[h] for h in nwOutline} - assert outlineView.topLevelItemCount() > 0 + assert outlineTree.topLevelItemCount() > 0 # Save header state not allowed - outlineView._lastBuild = 0 - outlineView._saveHeaderState() + outlineTree._lastBuild = 0 + outlineTree._saveHeaderState() assert pOptions.getValue("GuiOutline", "headerOrder", []) == [] # Allow saving header state - outlineView._lastBuild = time.time() - outlineView._saveHeaderState() + outlineTree._lastBuild = time.time() + outlineTree._saveHeaderState() assert pOptions.getValue("GuiOutline", "headerOrder", []) == colNames - assert outlineView._treeOrder == colItems - assert outlineView._colWidth == colWidth - assert outlineView._colHidden == colHidden + assert outlineTree._treeOrder == colItems + assert outlineTree._colWidth == colWidth + assert outlineTree._colHidden == colHidden # Get default values optItems = pOptions.getValue("GuiOutline", "headerOrder", []) @@ -105,49 +105,49 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): # Add invalid column name pOptions.setValue("GuiOutline", "headerOrder", optItems + ["blabla"]) - outlineView._loadHeaderState() - assert outlineView._treeOrder == colItems - assert outlineView._colHidden == colHidden + outlineTree._loadHeaderState() + assert outlineTree._treeOrder == colItems + assert outlineTree._colHidden == colHidden # Add duplicate column name pOptions.setValue("GuiOutline", "headerOrder", optItems + [optItems[-1]]) - outlineView._loadHeaderState() - assert outlineView._treeOrder == colItems - assert outlineView._colHidden == colHidden + outlineTree._loadHeaderState() + assert outlineTree._treeOrder == colItems + assert outlineTree._colHidden == colHidden # Invalid column width data pOptions.setValue("GuiOutline", "headerOrder", optItems) pOptions.setValue("GuiOutline", "columnWidth", {"blabla": None}) - outlineView._loadHeaderState() - assert outlineView._treeOrder == colItems - assert outlineView._colHidden == colHidden + outlineTree._loadHeaderState() + assert outlineTree._treeOrder == colItems + assert outlineTree._colHidden == colHidden # Invalid column width data pOptions.setValue("GuiOutline", "headerOrder", optItems) pOptions.setValue("GuiOutline", "columnWidth", optWidth) pOptions.setValue("GuiOutline", "columnHidden", {"bloabla": None}) - outlineView._loadHeaderState() - assert outlineView._treeOrder == colItems - assert outlineView._colHidden == colHidden + outlineTree._loadHeaderState() + assert outlineTree._treeOrder == colItems + assert outlineTree._colHidden == colHidden # Valid settings pOptions.setValue("GuiOutline", "headerOrder", optItems) pOptions.setValue("GuiOutline", "columnWidth", optWidth) pOptions.setValue("GuiOutline", "columnHidden", optHidden) - outlineView._loadHeaderState() - assert outlineView._treeOrder == colItems - assert outlineView._colHidden == colHidden + outlineTree._loadHeaderState() + assert outlineTree._treeOrder == colItems + assert outlineTree._colHidden == colHidden # Header Menu # =========== # Trigger the menu entry for all hidden columns for hItem in nwOutline: - if outlineView.DEF_HIDDEN[hItem]: + if outlineTree.DEF_HIDDEN[hItem]: outlineMenu.actionMap[hItem].activate(QAction.Trigger) # Now no columns should be hidden - outlineView._saveHeaderState() + outlineTree._saveHeaderState() assert not any(pOptions.getValue("GuiOutline", "columnHidden", None).values()) # qtbot.stop() @@ -169,10 +169,10 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.rebuildIndex() nwGUI._changeView(nwView.OUTLINE) - outlineMain = nwGUI.projView - outlineBar = outlineMain.outlineBar - outlineView = outlineMain.outlineView - outlineData = outlineMain.outlineData + outlineView = nwGUI.outlineView + outlineBar = outlineView.outlineBar + outlineTree = outlineView.outlineTree + outlineData = outlineView.outlineData lipHandle = "b3643d0f92e32" @@ -183,7 +183,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): # Add a second novel folder newHandle = nwGUI.theProject.newRoot(nwItemClass.NOVEL) - nwGUI.treeView.revealNewTreeItem(newHandle) + nwGUI.projView.revealNewTreeItem(newHandle) # Check new values in dropdown list assert outlineBar.novelValue.itemData(0) == lipHandle @@ -202,7 +202,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): aHandle = nwGUI.theProject.newFile(dTitle, newHandle) hHash = "#"*hLevel writeFile(os.path.join(nwLipsum, "content", f"{aHandle}.nwd"), f"{hHash} {dTitle}\n\n") - nwGUI.treeView.revealNewTreeItem(aHandle) + nwGUI.projView.revealNewTreeItem(aHandle) nwGUI.rebuildIndex() @@ -218,10 +218,10 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): # ============= # First Item - outlineView.refreshTree() - selItem = outlineView.topLevelItem(0) + outlineTree.refreshTree() + selItem = outlineTree.topLevelItem(0) - outlineView.setCurrentItem(selItem) + outlineTree.setCurrentItem(selItem) assert outlineData.titleLabel.text() == "Title" assert outlineData.titleValue.text() == "Lorem Ipsum" assert outlineData.fileValue.text() == "Lorem Ipsum" @@ -232,12 +232,12 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): assert outlineData.pCValue.text() == "3" # Scene One - actItem = outlineView.topLevelItem(1) + actItem = outlineTree.topLevelItem(1) chpItem = actItem.child(0) selItem = chpItem.child(0) - outlineView.setCurrentItem(selItem) - tHandle, tLine = outlineView.getSelectedHandle() + outlineTree.setCurrentItem(selItem) + tHandle, tLine = outlineTree.getSelectedHandle() assert tHandle == "88243afbe5ed8" assert tLine == 0 @@ -248,17 +248,17 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): # Click POV Link assert outlineData.povKeyValue.text() == "Bod" - nwGUI.projView._tagClicked("Bod") + outlineView._tagClicked("Bod") assert nwGUI.docViewer.docHandle() == "4c4f28287af27" # Scene One, Section Two - actItem = outlineView.topLevelItem(1) + actItem = outlineTree.topLevelItem(1) chpItem = actItem.child(0) scnItem = chpItem.child(0) selItem = scnItem.child(0) - outlineView.setCurrentItem(selItem) - tHandle, tLine = outlineView.getSelectedHandle() + outlineTree.setCurrentItem(selItem) + tHandle, tLine = outlineTree.getSelectedHandle() assert tHandle == "88243afbe5ed8" assert tLine == 12 @@ -267,7 +267,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): assert outlineData.fileValue.text() == "Scene One" assert outlineData.itemValue.text() == "Finished" - outlineView._treeDoubleClick(selItem, 0) + outlineTree._treeDoubleClick(selItem, 0) assert nwGUI.docEditor.docHandle() == "88243afbe5ed8" # qtbot.stop() diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 258ad150..feef859d 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -24,11 +24,11 @@ import os from tools import buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox +from PyQt5.QtWidgets import QMessageBox, QMenu -from novelwriter.guimain import GuiMain +from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass +from novelwriter.dialogs import GuiEditLabel from novelwriter.gui.projtree import GuiProjectTree -from novelwriter.enum import nwItemType, nwItemClass @pytest.mark.gui @@ -40,86 +40,101 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - nwTree = nwGUI.treeView + nwTree = nwGUI.projView # Try to add item with no project - assert nwTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False # Create a project prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, prjDir) # No itemType set - nwTree.clearSelection() - assert nwTree.newTreeItem(None) is False + nwTree.projTree.clearSelection() + assert nwTree.projTree.newTreeItem(None) is False # Root Items # ========== # No class set - assert nwTree.newTreeItem(nwItemType.ROOT) is False + assert nwTree.projTree.newTreeItem(nwItemType.ROOT) is False # Create root item - assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True + assert nwTree.projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True assert "0000000000010" in nwGUI.theProject.tree # File/Folder Items # ================= # No location selected for new item - nwTree.clearSelection() + nwTree.projTree.clearSelection() caplog.clear() - assert nwTree.newTreeItem(nwItemType.FILE) is False - assert nwTree.newTreeItem(nwItemType.FOLDER) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is False assert "Did not find anywhere" in caplog.text # Create new folder as child of Novel folder nwTree.setSelectedHandle("0000000000008") - assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True assert nwGUI.theProject.tree["0000000000011"].itemParent == "0000000000008" assert nwGUI.theProject.tree["0000000000011"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000011"].itemClass == nwItemClass.NOVEL # Add a new file in the new folder nwTree.setSelectedHandle("0000000000011") - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True assert nwGUI.theProject.tree["0000000000012"].itemParent == "0000000000011" assert nwGUI.theProject.tree["0000000000012"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000012"].itemClass == nwItemClass.NOVEL - # Add a new file next to the other new file + # Add a new chapter next to the other new file nwTree.setSelectedHandle("0000000000012") - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=2) is True assert nwGUI.theProject.tree["0000000000013"].itemParent == "0000000000011" assert nwGUI.theProject.tree["0000000000013"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL assert nwGUI.openDocument("0000000000013") - assert nwGUI.docEditor.getText() == "## New Document\n\n" + assert nwGUI.docEditor.getText() == "## New Chapter\n\n" + + # Add a new scene next to the other new file + nwTree.setSelectedHandle("0000000000012") + assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=3) is True + assert nwGUI.theProject.tree["0000000000014"].itemParent == "0000000000011" + assert nwGUI.theProject.tree["0000000000014"].itemRoot == "0000000000008" + assert nwGUI.theProject.tree["0000000000014"].itemClass == nwItemClass.NOVEL + assert nwGUI.openDocument("0000000000014") + assert nwGUI.docEditor.getText() == "### New Scene\n\n" # Add a new file to the characters folder nwTree.setSelectedHandle("000000000000a") - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwGUI.theProject.tree["0000000000014"].itemParent == "000000000000a" - assert nwGUI.theProject.tree["0000000000014"].itemRoot == "000000000000a" - assert nwGUI.theProject.tree["0000000000014"].itemClass == nwItemClass.CHARACTER - assert nwGUI.openDocument("0000000000014") + assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True + assert nwGUI.theProject.tree["0000000000015"].itemParent == "000000000000a" + assert nwGUI.theProject.tree["0000000000015"].itemRoot == "000000000000a" + assert nwGUI.theProject.tree["0000000000015"].itemClass == nwItemClass.CHARACTER + assert nwGUI.openDocument("0000000000015") assert nwGUI.docEditor.getText() == "# New Note\n\n" # Make sure the sibling folder bug trap works nwTree.setSelectedHandle("0000000000013") nwGUI.theProject.tree["0000000000013"].setParent(None) # This should not happen caplog.clear() - assert nwTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False assert "Internal error" in caplog.text nwGUI.theProject.tree["0000000000013"].setParent("0000000000011") + # Cancel during creation + with monkeypatch.context() as mp: + mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("", False)) + nwTree.setSelectedHandle("0000000000013") + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False + # Get the trash folder - nwTree._addTrashRoot() + nwTree.projTree._addTrashRoot() trashHandle = nwGUI.theProject.trashFolder() nwTree.setSelectedHandle(trashHandle) - assert nwTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False assert "Cannot add new files or folders to the Trash folder" in caplog.text # Other Checks @@ -148,12 +163,12 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - nwTree = nwGUI.treeView + nwTree = nwGUI.projView # Try to move item with no project - assert nwTree.moveTreeItem(1) is False + assert nwTree.projTree.moveTreeItem(1) is False # Create a project prjDir = os.path.join(fncDir, "project") @@ -164,42 +179,33 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # Add some files nwTree.setSelectedHandle("000000000000d") - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", ] - # Move item without focus - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) - assert nwTree.moveTreeItem(1) is False - assert nwTree.getTreeFromHandle("000000000000d") == [ - "000000000000d", "000000000000e", "000000000000f", - "0000000000010", "0000000000011", "0000000000012", - ] - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) - # Move with no selections - nwTree.clearSelection() - assert nwTree.moveTreeItem(1) is False + nwTree.projTree.clearSelection() + assert nwTree.projTree.moveTreeItem(1) is False # Move second item up twice (should give same result) nwTree.setSelectedHandle("000000000000f") - assert nwTree.moveTreeItem(-1) is True + assert nwTree.projTree.moveTreeItem(-1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000f", "000000000000e", "0000000000010", "0000000000011", "0000000000012", ] - assert nwTree.moveTreeItem(-1) is False + assert nwTree.projTree.moveTreeItem(-1) is False assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000f", "000000000000e", "0000000000010", "0000000000011", "0000000000012", ] - # Restore via menu entry - nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) + # Restore + assert nwTree.projTree.moveTreeItem(1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -207,19 +213,19 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # Move fifth item down twice (should give same result) nwTree.setSelectedHandle("0000000000011") - assert nwTree.moveTreeItem(1) is True + assert nwTree.projTree.moveTreeItem(1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000012", "0000000000011", ] - assert nwTree.moveTreeItem(1) is False + assert nwTree.projTree.moveTreeItem(1) is False assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000012", "0000000000011", ] - # Restore via menu entry - nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) + # Restore + assert nwTree.projTree.moveTreeItem(-1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -227,12 +233,12 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # Move down again, and restore via undo nwTree.setSelectedHandle("0000000000011") - assert nwTree.moveTreeItem(1) is True + assert nwTree.projTree.moveTreeItem(1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000012", "0000000000011", ] - nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger) + assert nwTree.projTree.undoLastMove() is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -245,15 +251,15 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Move novel folder up - assert nwTree.moveTreeItem(-1) is False + assert nwTree.projTree.moveTreeItem(-1) is False assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Move novel folder down - assert nwTree.moveTreeItem(1) is True + assert nwTree.projTree.moveTreeItem(1) is True assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 1 # Move novel folder up again - assert nwTree.moveTreeItem(-1) is True + assert nwTree.projTree.moveTreeItem(-1) is True assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Clean up @@ -272,9 +278,9 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - nwTree = nwGUI.treeView + nwTree = nwGUI.projView # Try to run with no project assert nwTree.emptyTrash() is False @@ -289,9 +295,9 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # Add some files nwTree.setSelectedHandle("000000000000d") - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -304,13 +310,13 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # No selection made - nwTree.clearSelection() + nwTree.projTree.clearSelection() caplog.clear() assert nwTree.deleteItem() is False assert "no item to delete" in caplog.text # Not a valid handle - nwTree.clearSelection() + nwTree.projTree.clearSelection() caplog.clear() assert nwTree.deleteItem("0000000000000") is False assert "Could not find tree item" in caplog.text @@ -326,10 +332,10 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # =========== # Block adding trash folder - funcPointer = nwTree._addTrashRoot - nwTree._addTrashRoot = lambda *a: None + funcPointer = nwTree.projTree._addTrashRoot + nwTree.projTree._addTrashRoot = lambda *a: None assert nwTree.deleteItem("0000000000012") is False - nwTree._addTrashRoot = funcPointer + nwTree.projTree._addTrashRoot = funcPointer # Delete last two documents, which also adds the trash folder assert nwTree.deleteItem("0000000000012") is True @@ -373,10 +379,10 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # Add a folder with two files nwTree.setSelectedHandle("0000000000009") - assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True nwTree.setSelectedHandle("0000000000014") - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000015.nwd")) assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000016.nwd")) @@ -399,7 +405,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # Add an empty folder, which can be deleted with no further restrictions nwTree.setSelectedHandle("0000000000009") - assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True assert nwTree.getTreeFromHandle("0000000000009") == ["0000000000009", "0000000000017"] nwTree.setSelectedHandle("0000000000017") @@ -441,7 +447,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd")) # Delete proper - assert nwTree._deleteTreeItem("000000000000e") is True + assert nwTree.projTree._deleteTreeItem("000000000000e") is True assert not os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd")) # Clean up @@ -449,3 +455,95 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR nwGUI.closeProject() # END Test testGuiProjTree_DeleteItems + + +@pytest.mark.gui +def testGuiProjTree_ContextMenu(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd): + """Test the building of the project tree context menu. All this does + is test that the menu builds. It doesn't open the actual menu, + """ + # Block message box + monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) + monkeypatch.setattr(QMenu, "exec_", lambda *a: None) + + # Create a project + prjDir = os.path.join(fncDir, "project") + buildTestProject(nwGUI, prjDir) + + # Handles for new objects + hNovelRoot = "0000000000008" + hTitlePage = "000000000000c" + hChapterDir = "000000000000d" + hChapterFile = "000000000000e" + hCharRoot = "000000000000a" + hCharNote = "0000000000011" + hNovelNote = "0000000000012" + + projTree = nwGUI.projView.projTree + projTree._getTreeItem(hNovelRoot).setExpanded(True) + projTree._getTreeItem(hChapterDir).setExpanded(True) + + projTree._addTrashRoot() + hTrashRoot = projTree.theProject.tree.trashRoot() + + projTree.setSelectedHandle(hCharRoot) + projTree.newTreeItem(nwItemType.FILE) + projTree.setSelectedHandle(hNovelRoot) + projTree.newTreeItem(nwItemType.FILE, isNote=True) + + def itemPos(tHandle): + return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center() + + # No item under menu + assert projTree._openContextMenu(projTree.viewport().rect().bottomRight()) is False + + # Generate the possible menu combinarions + assert projTree._openContextMenu(itemPos(hTrashRoot)) is True + assert projTree._openContextMenu(itemPos(hNovelRoot)) is True + assert projTree._openContextMenu(itemPos(hNovelNote)) is True + assert projTree._openContextMenu(itemPos(hTitlePage)) is True + assert projTree._openContextMenu(itemPos(hChapterDir)) is True + assert projTree._openContextMenu(itemPos(hChapterFile)) is True + assert projTree._openContextMenu(itemPos(hCharRoot)) is True + assert projTree._openContextMenu(itemPos(hCharNote)) is True + + # Check the keyboard shortcut handler as well + projTree.setSelectedHandle(hNovelRoot) + assert projTree.openContextOnSelected() is True + projTree.clearSelection() + assert projTree.openContextOnSelected() is False + + # Direct Edit Functions + # ===================== + # Trigger the dedicated functions the menu entries connect to + nwItem = projTree.theProject.tree[hNovelNote] + + # Toggle exported flag + assert nwItem.isExported is True + projTree._toggleItemExported(hNovelNote) + assert nwItem.isExported is False + + # Change item status + assert nwItem.itemStatus == "s000000" + projTree._changeItemStatus(hNovelNote, "s000001") + assert nwItem.itemStatus == "s000001" + + # Change item importance + assert nwItem.itemImport == "i000004" + projTree._changeItemImport(hNovelNote, "i000005") + assert nwItem.itemImport == "i000005" + + # Change item layout + assert nwItem.itemLayout == nwItemLayout.NOTE + projTree._changeItemLayout(hNovelNote, nwItemLayout.DOCUMENT) + assert nwItem.itemLayout == nwItemLayout.DOCUMENT + projTree._changeItemLayout(hNovelNote, nwItemLayout.NOTE) + assert nwItem.itemLayout == nwItemLayout.NOTE + + # qtbot.stop() + +# END Test testGuiProjTree_ContextMenu diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index 51e9c70b..0c6b1e81 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -40,7 +40,7 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): cHandle = nwGUI.theProject.newFile("A Note", "000000000000a") newDoc = NWDoc(nwGUI.theProject, cHandle) newDoc.writeDocument("# A Note\n\n") - nwGUI.treeView.revealNewTreeItem(cHandle) + nwGUI.projView.revealNewTreeItem(cHandle) nwGUI.rebuildIndex(beQuiet=True) # Reference Time diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index da7fc414..8c7b934a 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -93,82 +93,82 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir): assert thePalette.link().color() == QColor(44, 152, 247) assert thePalette.linkVisited().color() == QColor(44, 152, 247) - assert nwGUI.theTheme.statNone == [150, 152, 150] - assert nwGUI.theTheme.statSaved == [39, 135, 78] - assert nwGUI.theTheme.statUnsaved == [138, 32, 32] + assert nwGUI.mainTheme.statNone == [150, 152, 150] + assert nwGUI.mainTheme.statSaved == [39, 135, 78] + assert nwGUI.mainTheme.statUnsaved == [138, 32, 32] # Check Syntax Colours - assert nwGUI.theTheme.colBack == [45, 45, 45] - assert nwGUI.theTheme.colText == [204, 204, 204] - assert nwGUI.theTheme.colLink == [102, 153, 204] - assert nwGUI.theTheme.colHead == [102, 153, 204] - assert nwGUI.theTheme.colHeadH == [102, 153, 204] - assert nwGUI.theTheme.colEmph == [249, 145, 57] - assert nwGUI.theTheme.colDialN == [242, 119, 122] - assert nwGUI.theTheme.colDialD == [153, 204, 153] - assert nwGUI.theTheme.colDialS == [255, 204, 102] - assert nwGUI.theTheme.colHidden == [153, 153, 153] - assert nwGUI.theTheme.colKey == [242, 119, 122] - assert nwGUI.theTheme.colVal == [204, 153, 204] - assert nwGUI.theTheme.colSpell == [242, 119, 122] - assert nwGUI.theTheme.colError == [153, 204, 153] - assert nwGUI.theTheme.colRepTag == [102, 204, 204] - assert nwGUI.theTheme.colMod == [249, 145, 57] + assert nwGUI.mainTheme.colBack == [45, 45, 45] + assert nwGUI.mainTheme.colText == [204, 204, 204] + assert nwGUI.mainTheme.colLink == [102, 153, 204] + assert nwGUI.mainTheme.colHead == [102, 153, 204] + assert nwGUI.mainTheme.colHeadH == [102, 153, 204] + assert nwGUI.mainTheme.colEmph == [249, 145, 57] + assert nwGUI.mainTheme.colDialN == [242, 119, 122] + assert nwGUI.mainTheme.colDialD == [153, 204, 153] + assert nwGUI.mainTheme.colDialS == [255, 204, 102] + assert nwGUI.mainTheme.colHidden == [153, 153, 153] + assert nwGUI.mainTheme.colKey == [242, 119, 122] + assert nwGUI.mainTheme.colVal == [204, 153, 204] + assert nwGUI.mainTheme.colSpell == [242, 119, 122] + assert nwGUI.mainTheme.colError == [153, 204, 153] + assert nwGUI.mainTheme.colRepTag == [102, 204, 204] + assert nwGUI.mainTheme.colMod == [249, 145, 57] # Test Icon class - theIcons = nwGUI.theTheme.theIcons + iconCache = nwGUI.mainTheme.iconCache novelwriter.CONFIG.guiIcons = "invalid" - assert theIcons.updateTheme() is True + assert iconCache.updateTheme() is True assert novelwriter.CONFIG.guiIcons == "typicons_light" # Ask for a non-existent key - anImg = theIcons.loadDecoration("nonsense", 20, 20) + anImg = iconCache.loadDecoration("nonsense", 20, 20) assert isinstance(anImg, QPixmap) assert anImg.isNull() # Add a non-existent file and request it - theIcons.DECO_MAP["nonsense"] = "nofile.jpg" - anImg = theIcons.loadDecoration("nonsense", 20, 20) + iconCache.IMAGE_MAP["nonsense"] = "nofile.jpg" + anImg = iconCache.loadDecoration("nonsense", 20, 20) assert isinstance(anImg, QPixmap) assert anImg.isNull() # Get a real image, with different size parameters - anImg = theIcons.loadDecoration("wiz-back", 20, None) + anImg = iconCache.loadDecoration("wiz-back", 20, None) assert isinstance(anImg, QPixmap) assert not anImg.isNull() assert anImg.width() == 20 assert anImg.height() >= 56 - anImg = theIcons.loadDecoration("wiz-back", None, 70) + anImg = iconCache.loadDecoration("wiz-back", None, 70) assert isinstance(anImg, QPixmap) assert not anImg.isNull() assert anImg.height() == 70 assert anImg.width() >= 24 - anImg = theIcons.loadDecoration("wiz-back", 30, 70) + anImg = iconCache.loadDecoration("wiz-back", 30, 70) assert isinstance(anImg, QPixmap) assert not anImg.isNull() assert anImg.height() == 70 assert anImg.width() == 30 - anImg = theIcons.loadDecoration("wiz-back", None, None) + anImg = iconCache.loadDecoration("wiz-back", None, None) assert isinstance(anImg, QPixmap) assert not anImg.isNull() assert anImg.height() >= 1500 assert anImg.width() >= 500 # Load icons - anIcon = theIcons.getIcon("nonsense") + anIcon = iconCache.getIcon("nonsense") assert isinstance(anIcon, QIcon) assert anIcon.isNull() - anIcon = theIcons.getIcon("novelwriter") + anIcon = iconCache.getIcon("novelwriter") assert isinstance(anIcon, QIcon) assert not anIcon.isNull() # Check return empty icon if file not found - theIcons.ICON_KEYS.add("testicon3") - anIcon = theIcons.getIcon("testicon3") + iconCache.ICON_KEYS.add("testicon3") + anIcon = iconCache.getIcon("testicon3") assert isinstance(anIcon, QIcon) assert anIcon.isNull()