Some nested bold and italic and "
"strikethrough text here
Line one
Line two
Line three
Synopsis: The synopsis ...
\n" ) # Comment - theHtml._text = "% A comment ...\n" - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theResult == "" + html._text = "% A comment ...\n" + html.tokenizeText() + html.doConvert() + assert html.theResult == "" - theHtml.setComments(True) - theHtml._text = "% A comment ...\n" - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theResult == ( + html.setComments(True) + html._text = "% A comment ...\n" + html.tokenizeText() + html.doConvert() + assert html.theResult == ( "Comment: A comment ...
\n" ) # Keywords - theHtml._text = "@char: Bod, Jane\n" - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theResult == "" + html._text = "@char: Bod, Jane\n" + html.tokenizeText() + html.doConvert() + assert html.theResult == "" - theHtml.setKeywords(True) - theHtml._text = "@char: Bod, Jane\n" - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theResult == ( + html.setKeywords(True) + html._text = "@char: Bod, Jane\n" + html.tokenizeText() + html.doConvert() + assert html.theResult == ( "\n" ) # Multiple Keywords - theHtml.setKeywords(True) - theHtml._text = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n" - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theResult == ( + html.setKeywords(True) + html._text = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n" + html.tokenizeText() + html.doConvert() + assert html.theResult == ( "" " Bod" "
\n" @@ -214,13 +213,13 @@ def testCoreToHtml_ConvertFormat(mockGUI): # Preview Mode # ============ - theHtml.setPreview(True, True) + html.setPreview(True, True) # Text (HTML4) - theHtml._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n" - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theResult == ( + html._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n" + html.tokenizeText() + html.doConvert() + assert html.theResult == ( "Some nested bold and italic and " "strikethrough " "text here
\n" @@ -231,109 +230,108 @@ def testCoreToHtml_ConvertFormat(mockGUI): @pytest.mark.core def testCoreToHtml_ConvertDirect(mockGUI): - """Test the converter directly using the ToHtml class. - """ - theProject = NWProject() - theHtml = ToHtml(theProject) + """Test the converter directly using the ToHtml class.""" + project = NWProject() + html = ToHtml(project) - theHtml._isNovel = True - theHtml._isNote = False - theHtml.setLinkHeaders(True) + html._isNovel = True + html._isNote = False + html.setLinkHeaders(True) # Special Titles # ============== # Title - theHtml._tokens = [ - (theHtml.T_TITLE, 1, "A Title", None, theHtml.A_PBB | theHtml.A_CENTRE), - (theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE), + html._tokens = [ + (html.T_TITLE, 1, "A Title", None, html.A_PBB | html.A_CENTRE), + (html.T_EMPTY, 1, "", None, html.A_NONE), ] - theHtml.doConvert() - assert theHtml.theResult == ( + html.doConvert() + assert html.theResult == ( "* * *
\n" + html.doConvert() + assert html.theResult == "* * *
\n" # Skip - theHtml._tokens = [ - (theHtml.T_SKIP, 1, "", None, theHtml.A_NONE), - (theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE), + html._tokens = [ + (html.T_SKIP, 1, "", None, html.A_NONE), + (html.T_EMPTY, 1, "", None, html.A_NONE), ] - theHtml.doConvert() - assert theHtml.theResult == "\n" + html.doConvert() + assert html.theResult == "
\n" # Alignment # ========= - theHtml.setLinkHeaders(False) + html.setLinkHeaders(False) # Align Left - theHtml.setStyles(False) - theHtml._tokens = [ - (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_LEFT), + html.setStyles(False) + html._tokens = [ + (html.T_HEAD1, 1, "A Title", None, html.A_LEFT), ] - theHtml.doConvert() - assert theHtml.theResult == ( + html.doConvert() + assert html.theResult == ( "
Some text ...
\n" ) # Indent Right - theHtml._tokens = [ - (theHtml.T_TEXT, 1, "Some text ...", [], theHtml.A_IND_R), - (theHtml.T_EMPTY, 2, "", None, theHtml.A_NONE), + html._tokens = [ + (html.T_TEXT, 1, "Some text ...", [], html.A_IND_R), + (html.T_EMPTY, 2, "", None, html.A_NONE), ] - theHtml.doConvert() - assert theHtml.theResult == ( + html.doConvert() + assert html.theResult == ( "Some text ...
\n" ) @@ -378,40 +376,39 @@ def testCoreToHtml_ConvertDirect(mockGUI): @pytest.mark.core def testCoreToHtml_SpecialCases(mockGUI): - """Test some special cases that have caused errors in the past. - """ - theProject = NWProject() - theHtml = ToHtml(theProject) - theHtml._isNovel = True + """Test some special cases that have caused errors in the past.""" + project = NWProject() + html = ToHtml(project) + html._isNovel = True # Greater/Lesser than symbols # =========================== - theHtml._text = "Text with > and < with some **bold text** in it.\n" - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theResult == ( + html._text = "Text with > and < with some **bold text** in it.\n" + html.tokenizeText() + html.doConvert() + assert html.theResult == ( "Text with > and < with some bold text in it.
\n" ) - theHtml._text = "Text with some <**bold text**> in it.\n" - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theResult == ( + html._text = "Text with some <**bold text**> in it.\n" + html.tokenizeText() + html.doConvert() + assert html.theResult == ( "Text with some <bold text> in it.
\n" ) - theHtml._text = "Let's > be > _difficult **shall** > we_?\n" - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theResult == ( + html._text = "Let's > be > _difficult **shall** > we_?\n" + html.tokenizeText() + html.doConvert() + assert html.theResult == ( "Let's > be > difficult shall > we?
\n" ) - theHtml._text = "Test > text _<**bold**>_ and more.\n" - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theResult == ( + html._text = "Test > text _<**bold**>_ and more.\n" + html.tokenizeText() + html.doConvert() + assert html.theResult == ( "Test > text <bold> and more.
\n" ) @@ -419,20 +416,20 @@ def testCoreToHtml_SpecialCases(mockGUI): # =================== # See: https://github.com/vkbo/novelWriter/issues/950 - theHtml.setComments(True) - theHtml._text = "% Test > text _<**bold**>_ and more.\n" - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theResult == ( + html.setComments(True) + html._text = "% Test > text _<**bold**>_ and more.\n" + html.tokenizeText() + html.doConvert() + assert html.theResult == ( "" "Comment: Test > text _<**bold**>_ and more." "
\n" ) - theHtml._text = "## Heading <1>\n" - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theResult == ( + html._text = "## Heading <1>\n" + html.tokenizeText() + html.doConvert() + assert html.theResult == ( "Test text **bold** and more.
\n" ) @@ -452,11 +449,10 @@ def testCoreToHtml_SpecialCases(mockGUI): @pytest.mark.core def testCoreToHtml_Complex(mockGUI, fncPath): - """Test the save method of the ToHtml class. - """ - theProject = NWProject() - theHtml = ToHtml(theProject) - theHtml._isNovel = True + """Test the save method of the ToHtml class.""" + project = NWProject() + html = ToHtml(project) + html._isNovel = True # Build Project # ============= @@ -502,21 +498,21 @@ def testCoreToHtml_Complex(mockGUI, fncPath): ] for i in range(len(docText)): - theHtml._text = docText[i] - theHtml.doPreProcessing() - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theResult == resText[i] + html._text = docText[i] + html.doPreProcessing() + html.tokenizeText() + html.doConvert() + assert html.theResult == resText[i] - assert theHtml.fullHTML == resText + assert html.fullHTML == resText - theHtml.replaceTabs(nSpaces=2, spaceChar=" ") + html.replaceTabs(nSpaces=2, spaceChar=" ") resText[6] = "More text in scene two.
\n" # Check File # ========== - theStyle = theHtml.getStyleSheet() + theStyle = html.getStyleSheet() htmlDoc = ( "\n" "\n" @@ -539,7 +535,7 @@ def testCoreToHtml_Complex(mockGUI, fncPath): ) saveFile = fncPath / "outFile.htm" - theHtml.saveHtml5(saveFile) + html.saveHtml5(saveFile) assert readFile(saveFile) == htmlDoc # END Test testCoreToHtml_Complex @@ -547,86 +543,84 @@ def testCoreToHtml_Complex(mockGUI, fncPath): @pytest.mark.core def testCoreToHtml_Methods(mockGUI): - """Test all the other methods of the ToHtml class. - """ - theProject = NWProject() - theHtml = ToHtml(theProject) - theHtml.setKeepMarkdown(True) + """Test all the other methods of the ToHtml class.""" + project = NWProject() + html = ToHtml(project) + html.setKeepMarkdown(True) # Auto-Replace, keep Unicode docText = "Text withText with <brackets> & short–dash, long—dash …
\n" ) # Auto-Replace, replace Unicode docText = "Text withText with <brackets> & short–dash, long—dash …
\n" ) # With Preview - theHtml.setPreview(True, True) - theHtml._text = docText - theHtml.doPreProcessing() - theHtml.tokenizeText() - theHtml.doConvert() - assert theHtml.theMarkdown[-1] == ( + html.setPreview(True, True) + html._text = docText + html.doPreProcessing() + html.tokenizeText() + html.doConvert() + assert html.theMarkdown[-1] == ( "Text withSynopsis: synopsis text
\n" ) - assert theHtml._formatComments("comment text") == ( + assert html._formatComments("comment text") == ( "Comment: comment text
\n" ) - assert theHtml._formatKeywords("") == "" - assert theHtml._formatKeywords("tag: Jane") == ( + assert html._formatKeywords("") == "" + assert html._formatKeywords("tag: Jane") == ( " Jane" ) - assert theHtml._formatKeywords("char: Bod, Jane") == ( + assert html._formatKeywords("char: Bod, Jane") == ( " " "Bod, " "Jane" @@ -635,20 +629,20 @@ def testCoreToHtml_Format(mockGUI): # Preview Mode # ============ - theHtml.setPreview(True, True) + html.setPreview(True, True) - assert theHtml._formatSynopsis("synopsis text") == ( + assert html._formatSynopsis("synopsis text") == ( "Synopsis: synopsis text
\n" ) - assert theHtml._formatComments("comment text") == ( + assert html._formatComments("comment text") == ( "comment text
\n" ) - assert theHtml._formatKeywords("") == "" - assert theHtml._formatKeywords("tag: Jane") == ( + assert html._formatKeywords("") == "" + assert html._formatKeywords("tag: Jane") == ( " Jane" ) - assert theHtml._formatKeywords("char: Bod, Jane") == ( + assert html._formatKeywords("char: Bod, Jane") == ( " " "Bod, " "Jane" diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index d746418d..16f34d9d 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -442,8 +442,8 @@ def testCoreToken_MetaFormat(mockGUI): theToken._text = "% A comment\n" theToken.tokenizeText() assert theToken._tokens == [ - (Tokenizer.T_COMMENT, 1, "A comment", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_COMMENT, 0, "A comment", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] assert theToken.theMarkdown[-1] == "\n" @@ -455,14 +455,14 @@ def testCoreToken_MetaFormat(mockGUI): theToken._text = "%synopsis: The synopsis\n" theToken.tokenizeText() assert theToken._tokens == [ - (Tokenizer.T_SYNOPSIS, 1, "The synopsis", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_SYNOPSIS, 0, "The synopsis", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] theToken._text = "% synopsis: The synopsis\n" theToken.tokenizeText() assert theToken._tokens == [ - (Tokenizer.T_SYNOPSIS, 1, "The synopsis", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_SYNOPSIS, 0, "The synopsis", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] assert theToken.theMarkdown[-1] == "\n" @@ -474,8 +474,8 @@ def testCoreToken_MetaFormat(mockGUI): theToken._text = "@char: Bod\n" theToken.tokenizeText() assert theToken._tokens == [ - (Tokenizer.T_KEYWORD, 1, "char: Bod", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_KEYWORD, 0, "char: Bod", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] assert theToken.theMarkdown[-1] == "\n" @@ -489,10 +489,10 @@ def testCoreToken_MetaFormat(mockGUI): styMid = Tokenizer.A_NONE | Tokenizer.A_Z_BTMMRG | Tokenizer.A_Z_TOPMRG styBtm = Tokenizer.A_NONE | Tokenizer.A_Z_TOPMRG assert theToken._tokens == [ - (Tokenizer.T_KEYWORD, 1, "pov: Bod", None, styTop), - (Tokenizer.T_KEYWORD, 2, "plot: Main", None, styMid), - (Tokenizer.T_KEYWORD, 3, "location: Europe", None, styBtm), - (Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_NONE), + (Tokenizer.T_KEYWORD, 0, "pov: Bod", None, styTop), + (Tokenizer.T_KEYWORD, 0, "plot: Main", None, styMid), + (Tokenizer.T_KEYWORD, 0, "location: Europe", None, styBtm), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] assert theToken.theMarkdown[-1] == "@pov: Bod\n@plot: Main\n@location: Europe\n\n" @@ -521,23 +521,23 @@ def testCoreToken_MarginFormat(mockGUI): ) theToken.tokenizeText() assert theToken._tokens == [ - (Tokenizer.T_TEXT, 1, "Some regular text", [], Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 3, "Some left-aligned text", [], Tokenizer.A_LEFT), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 5, "Some right-aligned text", [], Tokenizer.A_RIGHT), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 7, "Some centered text", [], Tokenizer.A_CENTRE), - (Tokenizer.T_EMPTY, 8, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 9, "Left-indented block", [], Tokenizer.A_IND_L), - (Tokenizer.T_EMPTY, 10, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 11, "Right-indented block", [], Tokenizer.A_IND_R), - (Tokenizer.T_EMPTY, 12, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 13, "Double-indented block", [], dblIndent), - (Tokenizer.T_EMPTY, 14, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 15, "Right-indent, right-aligned", [], rIndAlign), - (Tokenizer.T_EMPTY, 16, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 16, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 0, "Some regular text", [], Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 0, "Some left-aligned text", [], Tokenizer.A_LEFT), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 0, "Some right-aligned text", [], Tokenizer.A_RIGHT), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 0, "Some centered text", [], Tokenizer.A_CENTRE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 0, "Left-indented block", [], Tokenizer.A_IND_L), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 0, "Right-indented block", [], Tokenizer.A_IND_R), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 0, "Double-indented block", [], dblIndent), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 0, "Right-indent, right-aligned", [], rIndAlign), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] assert theToken.theMarkdown[-1] == ( "Some regular text\n\n" @@ -564,20 +564,20 @@ def testCoreToken_TextFormat(mockGUI): theToken._text = "Some plain text\non two lines\n\n\n" theToken.tokenizeText() assert theToken._tokens == [ - (Tokenizer.T_TEXT, 1, "Some plain text", [], Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 2, "on two lines", [], Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 0, "Some plain text", [], Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 0, "on two lines", [], Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] assert theToken.theMarkdown[-1] == "Some plain text\non two lines\n\n\n\n" theToken.setBodyText(False) theToken.tokenizeText() assert theToken._tokens == [ - (Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] assert theToken.theMarkdown[-1] == "\n\n\n" theToken.setBodyText(True) @@ -587,7 +587,7 @@ def testCoreToken_TextFormat(mockGUI): theToken.tokenizeText() assert theToken._tokens == [ ( - Tokenizer.T_TEXT, 1, + Tokenizer.T_TEXT, 0, "Some **bolded text** on this lines", [ [5, 2, Tokenizer.FMT_B_B], @@ -595,7 +595,7 @@ def testCoreToken_TextFormat(mockGUI): ], Tokenizer.A_NONE ), - (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] assert theToken.theMarkdown[-1] == "Some **bolded text** on this lines\n\n" @@ -603,7 +603,7 @@ def testCoreToken_TextFormat(mockGUI): theToken.tokenizeText() assert theToken._tokens == [ ( - Tokenizer.T_TEXT, 1, + Tokenizer.T_TEXT, 0, "Some _italic text_ on this lines", [ [5, 1, Tokenizer.FMT_I_B], @@ -611,7 +611,7 @@ def testCoreToken_TextFormat(mockGUI): ], Tokenizer.A_NONE ), - (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] assert theToken.theMarkdown[-1] == "Some _italic text_ on this lines\n\n" @@ -619,7 +619,7 @@ def testCoreToken_TextFormat(mockGUI): theToken.tokenizeText() assert theToken._tokens == [ ( - Tokenizer.T_TEXT, 1, + Tokenizer.T_TEXT, 0, "Some **_bold italic text_** on this lines", [ [5, 2, Tokenizer.FMT_B_B], @@ -629,7 +629,7 @@ def testCoreToken_TextFormat(mockGUI): ], Tokenizer.A_NONE ), - (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] assert theToken.theMarkdown[-1] == "Some **_bold italic text_** on this lines\n\n" @@ -637,7 +637,7 @@ def testCoreToken_TextFormat(mockGUI): theToken.tokenizeText() assert theToken._tokens == [ ( - Tokenizer.T_TEXT, 1, + Tokenizer.T_TEXT, 0, "Some ~~strikethrough text~~ on this lines", [ [5, 2, Tokenizer.FMT_D_B], @@ -645,7 +645,7 @@ def testCoreToken_TextFormat(mockGUI): ], Tokenizer.A_NONE ), - (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] assert theToken.theMarkdown[-1] == "Some ~~strikethrough text~~ on this lines\n\n" @@ -653,7 +653,7 @@ def testCoreToken_TextFormat(mockGUI): theToken.tokenizeText() assert theToken._tokens == [ ( - Tokenizer.T_TEXT, 1, + Tokenizer.T_TEXT, 0, "Some **nested bold and _italic_ and ~~strikethrough~~ text** here", [ [5, 2, Tokenizer.FMT_B_B], @@ -665,7 +665,7 @@ def testCoreToken_TextFormat(mockGUI): ], Tokenizer.A_NONE ), - (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 0, "", None, Tokenizer.A_NONE), ] assert theToken.theMarkdown[-1] == ( "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n" @@ -687,11 +687,11 @@ def testCoreToken_SpecialFormat(mockGUI): correctResp = [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_CENTRE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_HEAD1, 2, "Title Two", None, Tokenizer.A_CENTRE | Tokenizer.A_PBB), + (Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), - (Tokenizer.T_HEAD1, 5, "Title Two", None, Tokenizer.A_CENTRE | Tokenizer.A_PBB), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), ] # Command wo/Space @@ -735,12 +735,12 @@ def testCoreToken_SpecialFormat(mockGUI): theToken.tokenizeText() assert theToken._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), - (Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_NONE), - (Tokenizer.T_SKIP, 3, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 5, "Some text to go here ...", [], Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 1, "Some text to go here ...", [], Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # Multiple Empty Paragraphs @@ -755,12 +755,12 @@ def testCoreToken_SpecialFormat(mockGUI): theToken.tokenizeText() assert theToken._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), - (Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_NONE), - (Tokenizer.T_SKIP, 3, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 5, "Some text to go here ...", [], Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 1, "Some text to go here ...", [], Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # Three Skips @@ -772,14 +772,14 @@ def testCoreToken_SpecialFormat(mockGUI): theToken.tokenizeText() assert theToken._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), - (Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_NONE), - (Tokenizer.T_SKIP, 3, "", None, Tokenizer.A_NONE), - (Tokenizer.T_SKIP, 3, "", None, Tokenizer.A_NONE), - (Tokenizer.T_SKIP, 3, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 5, "Some text to go here ...", [], Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 1, "Some text to go here ...", [], Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # Malformed Command, Case 1 @@ -791,11 +791,11 @@ def testCoreToken_SpecialFormat(mockGUI): theToken.tokenizeText() assert theToken._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), - (Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 5, "Some text to go here ...", [], Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 1, "Some text to go here ...", [], Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # Malformed Command, Case 2 @@ -807,11 +807,11 @@ def testCoreToken_SpecialFormat(mockGUI): theToken.tokenizeText() assert theToken._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), - (Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 5, "Some text to go here ...", [], Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 1, "Some text to go here ...", [], Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # Malformed Command, Case 3 @@ -823,11 +823,11 @@ def testCoreToken_SpecialFormat(mockGUI): theToken.tokenizeText() assert theToken._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), - (Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 5, "Some text to go here ...", [], Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 1, "Some text to go here ...", [], Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # Empty Paragraph and Page Break @@ -843,13 +843,13 @@ def testCoreToken_SpecialFormat(mockGUI): theToken.tokenizeText() assert theToken._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), - (Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), - (Tokenizer.T_SKIP, 5, "", None, Tokenizer.A_PBB), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 7, "Some text to go here ...", [], 0), - (Tokenizer.T_EMPTY, 8, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 8, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_PBB), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 1, "Some text to go here ...", [], 0), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # Multiple Skip @@ -862,15 +862,15 @@ def testCoreToken_SpecialFormat(mockGUI): theToken.tokenizeText() assert theToken._tokens == [ (Tokenizer.T_HEAD1, 1, "Title One", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), - (Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), - (Tokenizer.T_SKIP, 5, "", None, Tokenizer.A_PBB), - (Tokenizer.T_SKIP, 5, "", None, Tokenizer.A_NONE), - (Tokenizer.T_SKIP, 5, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 6, "", None, Tokenizer.A_NONE), - (Tokenizer.T_TEXT, 7, "Some text to go here ...", [], 0), - (Tokenizer.T_EMPTY, 8, "", None, Tokenizer.A_NONE), - (Tokenizer.T_EMPTY, 8, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_PBB), + (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 1, "Some text to go here ...", [], 0), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), ] # END Test testCoreToken_SpecialFormat From a7e2b512252b349693dac226ee1fea243fd59a43 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 4 Nov 2023 10:05:37 +0100 Subject: [PATCH 19/29] Add blockers for illegal drop events on invisible root and for root items (#1569) --- novelwriter/gui/projtree.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index e753a828..aad2fd4d 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -507,8 +507,11 @@ class GuiProjectTree(QTreeWidget): # Allow Move by Drag & Drop self.setDragEnabled(True) self.setDragDropMode(QAbstractItemView.InternalMove) + self.setDropIndicatorShown(True) # But don't allow drop on root level + # Due to a bug, this stops working somewhere between Qt 5.15.3 + # and 5.15.8, so this is also blocked in dropEvent trRoot = self.invisibleRootItem() trRoot.setFlags(trRoot.flags() ^ Qt.ItemIsDropEnabled) @@ -1380,8 +1383,16 @@ class GuiProjectTree(QTreeWidget): """ sHandle = self.getSelectedHandle() sItem = self._getTreeItem(sHandle) if sHandle else None - if sHandle is None or sItem is None: + if sHandle is None or sItem is None or sItem.parent() is None: logger.error("Invalid drag and drop event") + event.ignore() + return + + if not self.indexAt(event.pos()).isValid(): + # Needed due to a bug somewhere around Qt 5.15.8 that + # ignores the invisible root item flags + logger.error("Invalid drop location") + event.ignore() return logger.debug("Drag'n'drop of item '%s' accepted", sHandle) @@ -1728,7 +1739,9 @@ class GuiProjectTree(QTreeWidget): newItem.setData(self.C_DATA, self.D_WORDS, 0) if pHandle is None and nwItem.isRootType(): + # newItem.setFlags(newItem.flags() ^ Qt.ItemFlag.ItemIsDragEnabled) pItem = self.invisibleRootItem() + # pItem.setFlags(pItem.flags() ^ Qt.ItemFlag.ItemIsDropEnabled) elif pHandle and pHandle in self._treeMap: pItem = self._treeMap[pHandle] else: From c5baa8d359b9853c01b676bbf5d4681e48c51ebb Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 4 Nov 2023 10:14:49 +0100 Subject: [PATCH 20/29] Remove commented out code --- novelwriter/gui/projtree.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index aad2fd4d..9ac115b4 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -1739,9 +1739,7 @@ class GuiProjectTree(QTreeWidget): newItem.setData(self.C_DATA, self.D_WORDS, 0) if pHandle is None and nwItem.isRootType(): - # newItem.setFlags(newItem.flags() ^ Qt.ItemFlag.ItemIsDragEnabled) pItem = self.invisibleRootItem() - # pItem.setFlags(pItem.flags() ^ Qt.ItemFlag.ItemIsDropEnabled) elif pHandle and pHandle in self._treeMap: pItem = self._treeMap[pHandle] else: From da3747c5f5ef104a9b2897946e5f35743516086c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 4 Nov 2023 16:45:16 +0100 Subject: [PATCH 21/29] Implement autoscroll in project tree, and disable library autoscroll feature (#1561) --- novelwriter/gui/projtree.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 9ac115b4..e1fd6533 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -31,8 +31,8 @@ from enum import Enum from time import time from typing import TYPE_CHECKING -from PyQt5.QtGui import QDropEvent, QMouseEvent, QPalette -from PyQt5.QtCore import QPoint, Qt, QSize, pyqtSignal, pyqtSlot +from PyQt5.QtGui import QDragMoveEvent, QDropEvent, QMouseEvent, QPalette +from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, @@ -508,6 +508,7 @@ class GuiProjectTree(QTreeWidget): self.setDragEnabled(True) self.setDragDropMode(QAbstractItemView.InternalMove) self.setDropIndicatorShown(True) + self.setAutoScroll(False) # But don't allow drop on root level # Due to a bug, this stops working somewhere between Qt 5.15.3 @@ -527,6 +528,13 @@ class GuiProjectTree(QTreeWidget): self.itemDoubleClicked.connect(self._treeDoubleClick) self.itemSelectionChanged.connect(self._treeSelectionChange) + # Autoscroll + self._scrollMargin = SHARED.theme.baseIconSize + self._scrollDirection = 0 + self._scrollTimer = QTimer() + self._scrollTimer.timeout.connect(self._doAutoScroll) + self._scrollTimer.setInterval(250) + # Set custom settings self.initSettings() @@ -1346,6 +1354,17 @@ class GuiProjectTree(QTreeWidget): return True + @pyqtSlot() + def _doAutoScroll(self) -> None: + """Scroll one item up or down based on direction value.""" + if self._scrollDirection == -1: + self.scrollToItem(self.itemAbove(self.itemAt(1, 1))) + elif self._scrollDirection == 1: + self.scrollToItem(self.itemBelow(self.itemAt(1, self.height() - 1))) + self._scrollDirection = 0 + self._scrollTimer.stop() + return + ## # Events ## @@ -1377,6 +1396,20 @@ class GuiProjectTree(QTreeWidget): return + def dragMoveEvent(self, event: QDragMoveEvent) -> None: + """Capture the drag move event to enable edge autoscroll.""" + y = event.pos().y() + if y < 16: + if not self._scrollTimer.isActive(): + self._scrollDirection = -1 + self._scrollTimer.start() + elif y > self.height() - 16: + if not self._scrollTimer.isActive(): + self._scrollDirection = 1 + self._scrollTimer.start() + super().dragMoveEvent(event) + return + def dropEvent(self, event: QDropEvent) -> None: """Overload the drop item event to ensure relevant data has been updated. From 05ddca043a4d7f437f63571a1131dc2f4504f243 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 4 Nov 2023 16:48:22 +0100 Subject: [PATCH 22/29] Add comment to explain why autoscroll was added --- novelwriter/gui/projtree.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index e1fd6533..89cd4e72 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -508,11 +508,14 @@ class GuiProjectTree(QTreeWidget): self.setDragEnabled(True) self.setDragDropMode(QAbstractItemView.InternalMove) self.setDropIndicatorShown(True) + + # Disable built-in autoscroll as it isn't working in some Qt + # releases (see #1561) and instead use our own implementation self.setAutoScroll(False) # But don't allow drop on root level # Due to a bug, this stops working somewhere between Qt 5.15.3 - # and 5.15.8, so this is also blocked in dropEvent + # and 5.15.8, so this is also blocked in dropEvent (see #1569) trRoot = self.invisibleRootItem() trRoot.setFlags(trRoot.flags() ^ Qt.ItemIsDropEnabled) From 6f8109609cb7bb5984c103ba2a6dd20c5193503b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 4 Nov 2023 16:50:41 +0100 Subject: [PATCH 23/29] Use the correct margin settings --- novelwriter/gui/projtree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 89cd4e72..31d2ca12 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -1402,11 +1402,11 @@ class GuiProjectTree(QTreeWidget): def dragMoveEvent(self, event: QDragMoveEvent) -> None: """Capture the drag move event to enable edge autoscroll.""" y = event.pos().y() - if y < 16: + if y < self._scrollMargin: if not self._scrollTimer.isActive(): self._scrollDirection = -1 self._scrollTimer.start() - elif y > self.height() - 16: + elif y > self.height() - self._scrollMargin: if not self._scrollTimer.isActive(): self._scrollDirection = 1 self._scrollTimer.start() From 8abcada86e3ef9fa5106d53bbb4311b7165acdf2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 4 Nov 2023 17:24:43 +0100 Subject: [PATCH 24/29] Make sure all QMenu instances have a parent widget set (#1536) --- novelwriter/gui/doceditor.py | 2 +- novelwriter/gui/docviewer.py | 2 +- novelwriter/gui/noveltree.py | 2 +- novelwriter/gui/projtree.py | 8 ++++---- novelwriter/gui/sidebar.py | 2 +- novelwriter/tools/manussettings.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index c93db0a8..ec4bd7d8 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -1121,7 +1121,7 @@ class GuiDocEditor(QTextEdit): userSelection = userCursor.hasSelection() posCursor = self.cursorForPosition(thePos) - mnuContext = QMenu() + mnuContext = QMenu(self) # Follow, Cut, Copy and Paste # =========================== diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index ca835ea6..18dfdbc5 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -381,7 +381,7 @@ class GuiDocViewer(QTextBrowser): userCursor = self.textCursor() userSelection = userCursor.hasSelection() - mnuContext = QMenu() + mnuContext = QMenu(self) # Cut, Copy and Paste # =================== diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 4d804c40..0742ab54 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -228,7 +228,7 @@ class GuiNovelToolBar(QWidget): self.tbRefresh.clicked.connect(self._refreshNovelTree) # More Options Menu - self.mMore = QMenu() + self.mMore = QMenu(self) self.mLastCol = self.mMore.addMenu(self.tr("Last Column")) self.gLastCol = QActionGroup(self.mMore) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 31d2ca12..929f37c6 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -248,7 +248,7 @@ class GuiProjectToolBar(QWidget): self.viewLabel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) # Quick Links - self.mQuick = QMenu() + self.mQuick = QMenu(self) self.tbQuick = QToolButton(self) self.tbQuick.setToolTip("%s [Ctrl+L]" % self.tr("Quick Links")) @@ -269,7 +269,7 @@ class GuiProjectToolBar(QWidget): self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1)) # Add Item Menu - self.mAdd = QMenu() + self.mAdd = QMenu(self) self.aAddEmpty = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["document"])) self.aAddEmpty.triggered.connect( @@ -307,7 +307,7 @@ class GuiProjectToolBar(QWidget): self.tbAdd.setPopupMode(QToolButton.InstantPopup) # More Options Menu - self.mMore = QMenu() + self.mMore = QMenu(self) self.aExpand = self.mMore.addAction(self.tr("Expand All")) self.aExpand.triggered.connect(lambda: self.projTree.setExpandedFromHandle(None, True)) @@ -1210,7 +1210,7 @@ class GuiProjectTree(QTreeWidget): logger.debug("No item found") return False - ctxMenu = QMenu() + ctxMenu = QMenu(self) # Trash Folder # ============ diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index 266d735a..8854f583 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -95,7 +95,7 @@ class GuiSideBar(QToolBar): self.aStats.triggered.connect(lambda: self.mainGui.showWritingStatsDialog()) # Settings Menu - self.mSettings = QMenu() + self.mSettings = QMenu(self) self.mSettings.addAction(self.mainGui.mainMenu.aEditWordList) self.mSettings.addAction(self.mainGui.mainMenu.aProjectSettings) diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index c4b211fb..cfa1b038 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -720,7 +720,7 @@ class _HeadingsTab(QWidget): self.formSyntax = _HeadingSyntaxHighlighter(self.editTextBox.document()) - self.menuInsert = QMenu() + self.menuInsert = QMenu(self) self.aInsTitle = self.menuInsert.addAction(self.tr("Title")) self.aInsChNum = self.menuInsert.addAction(self.tr("Chapter Number")) self.aInsChWord = self.menuInsert.addAction(self.tr("Chapter Number (Word)")) From dd61897d11a009d013a144cdfdd85b931e162e5a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 4 Nov 2023 17:43:02 +0100 Subject: [PATCH 25/29] Force a rehighlight on theme switch for small documents (#1535) --- novelwriter/gui/doceditor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index ec4bd7d8..75b59e97 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -359,6 +359,8 @@ class GuiDocEditor(QTextEdit): self.clearEditor() else: self.redrawText() + if not self._bigDoc: + self.highLight.rehighlight() return True From ee5f340fe2723d7121ef8e00c8ac300196158c69 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 4 Nov 2023 17:46:25 +0100 Subject: [PATCH 26/29] Refresh spell checking when word list changes (#1559) --- novelwriter/guimain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 96806648..a966346b 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -980,6 +980,7 @@ class GuiMain(QMainWindow): if dlgWords.result() == QDialog.Accepted: logger.debug("Reloading word list") SHARED.updateSpellCheckLanguage(reload=True) + self.docEditor.spellCheckDocument() return True From b126b8cf0a5e20cadca689452330937273673ec8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 4 Nov 2023 18:19:57 +0100 Subject: [PATCH 27/29] Bumb version and update release notes for v2.1.1 --- CHANGELOG.md | 57 +++++++++++++++++++++++ novelwriter/__init__.py | 6 +-- novelwriter/assets/text/release_notes.htm | 13 ++++++ sample/nwProject.nwx | 4 +- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bbbfd74..4ac00b49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,62 @@ # novelWriter Changelog +## Version 2.1.1 [2023-11-05] + +### Release Notes + +This is a patch release that fixes a layout issue and internationalisation issues with the new +Manuscript Build tool. It also fixes a number of issues related to bugs in the underlying Qt +framework that affects drag and drop functionality in the project tree. These issues were mostly +only affecting Debian Linux package releases. + +Other, minor issues related to updating the editor on colour theme change and project word list +changes have been fixed as well. See the full changelog for more details. + +### Detailed Changelog + +**Bugfixes** + +* Fix an issue with width of the last two columns on Selection page of the Build Settings dialog on + Windows. They were far too wide by default. Issue #1551. PR #1553. +* Fix an issue where a lot of string were not translated to the UI language in the new Manuscript + Build tool. Issue #1563. PR #1565. +* Fix an issue in the Document Viewer where it wouldn't scroll to a heading further down the page + when following a reference pointing to it. Issue #1566. PR #1568. +* Add back in checks for illegal drag and drop moves in the project tree. In 2.0, the logic here + was changed to set certain restrictions on the elements of the project tree itself, but there are + numerous bugs in the Qt framework related to drag and drop, so the checks are ignored on at least + Qt 5.15.8. In particular, it is possible to drop items on the root level, and it's possible to + move root items to other locations. Neither should be possible and will severely mess up the + project if done. Issue #1569. PR #1570. +* Add a custom autoscroll feature when dragging an item in the project tree to near the top or + bottom. This is actually a default feature of the tree widget in the Qt library, but this too is + broken in some versions of Qt 5.15.x. The default feature has been permanently disabled and + replaced by a custom written feature that behaves similarly. Issue #1561. PR #1571. +* Fix an issue where the editor document wasn't re-highlighted when the Syntax Theme for it was + changed. Issue #1535. PR #1573. +* Fix an issue where editing the Project Word List would not refresh the spell checking of the + editor. Issue #1559. PR #1573. + +**Usability** + +* Changed how the default UI language is selected. It used to default to the system locale, but + that is now changed to British English if the system local is not available in novelWriter. The + only real effects of this is that the dropdown box in Preferences now selects British English if + the system locale is not available rather than the first in the list (currently Deutch). The + second effect is that the language on buttons and other Qt components will match the rest of the + UI. Issue #1564. PR #1565. +* There is a bug in Qt on Wayland desktops where menus don't open in the correct location. + According to one Qt ticket, QTBUG-68636, this can be mitigated by ensuring all QMenu instances + have a parent set. This does not fix all issues, but it should help. The menus without a parent + set have now been updated. Issue #1536. PR #1572. + +**Documentation** + +* Fixed a number of spelling errors and typing mistakes in the documentation for 2.1. Contributed + by @nisemono-neko. PR #1567. + +---- + ## Version 2.1 [2023-10-17] ### Release Notes diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 2b821e98..d62201e9 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -60,9 +60,9 @@ __license__ = "GPLv3" __author__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" -__version__ = "2.1" -__hexversion__ = "0x020100f0" -__date__ = "2023-10-17" +__version__ = "2.1.1" +__hexversion__ = "0x020101f0" +__date__ = "2023-11-05" __status__ = "Stable" __domain__ = "novelwriter.io" diff --git a/novelwriter/assets/text/release_notes.htm b/novelwriter/assets/text/release_notes.htm index 521a5f6d..c74a1574 100644 --- a/novelwriter/assets/text/release_notes.htm +++ b/novelwriter/assets/text/release_notes.htm @@ -5,6 +5,8 @@Released on 17 October 2023
+Scroll down for Patch Notes
+The primary focus of this release has been a complete redesign of the Build Tool, that is, the tool that assembles your project into a manuscript document. The new tool, called the "Manuscript Build Tool" allows you to define multiple build definitions for your project. The build definitions @@ -30,5 +32,16 @@ a full list of changes, see the detailed changelogs.
See also the Releases page.
+This is a patch release that fixes a layout issue and internationalisation issues with the new +Manuscript Build tool. It also fixes a number of issues related to bugs in the underlying Qt +framework that affects drag and drop functionality in the project tree. These issues were mostly +only affecting Debian Linux package releases.
+Other, minor issues related to updating the editor on colour theme change and project word list +changes have been fixed as well. See the full changelog for more details.
+