Merge remote-tracking branch 'github/develop' into develop

Signed-off-by: TheJackiMonster <thejackimonster@gmail.com>
This commit is contained in:
TheJackiMonster 2021-04-16 13:42:19 +02:00
commit 16d8b2de7b
No known key found for this signature in database
GPG key ID: D850A5F772E880F9
105 changed files with 8009 additions and 4498 deletions

46
.github/workflows/linux.yml vendored Normal file
View file

@ -0,0 +1,46 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the develop branch
push:
branches: [ develop ]
pull_request:
branches: [ develop ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
test:
name: Test on node ${{ matrix.python_version }} and ${{ matrix.os }}
# The type of runner that the job will run on
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: [3.6]
# python-version: [3.6, 3.7, 3.8, 3.9]
os: [ubuntu-16.04]
# os: [ubuntu-16.04, ubuntu-latest, windows-latest, macos-10.15]
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pyqt5==5.9 lxml pytest pytest-faulthandler
sudo apt-get -qq update
sudo apt-get -qq install python3-pip python3-dev build-essential qt5-default libxml2-dev libxslt1-dev mesa-utils libgl1-mesa-glx libgl1-mesa-dev libxcb-xinerama0-dev
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Test with pytest
run: |
xvfb-run -s '-screen 0 640x480x24 +extension GLX' pytest -vs

5
.gitignore vendored
View file

@ -11,12 +11,17 @@
.idea
.project
.pydevproject
.python-version
.settings/org.eclipse.core.resources.prefs
.vscode
.vimrc
ExportTest
Notes.t2t
dist
build
icons/Numix
manuskript/pycallgraph.txt
manuskript.log
snowflake*
test-projects
main.pyproject.user

View file

@ -2,7 +2,7 @@ language: generic
os:
- osx
- linux
osx_image: xcode10.1
osx_image: xcode11
sudo: required
install:
- if [ "$TRAVIS_OS_NAME" = "osx" ]; then package/prepare_osx.sh; fi

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

3
main.pyproject Normal file

File diff suppressed because one or more lines are too long

View file

@ -11,6 +11,9 @@ from PyQt5.QtGui import QCursor
from manuskript.converters import abstractConverter
from manuskript.functions import mainWindow
import logging
LOGGER = logging.getLogger(__name__)
try:
import markdown as MD
except ImportError:
@ -26,12 +29,12 @@ class markdownConverter(abstractConverter):
@classmethod
def isValid(self):
return MD is not None
return MD != None
@classmethod
def convert(self, markdown):
if not self.isValid:
print("ERROR: markdownConverter is called but not valid.")
LOGGER.error("markdownConverter is called but not valid.")
return ""
html = MD.markdown(markdown)

View file

@ -11,6 +11,8 @@ from PyQt5.QtGui import QCursor
from manuskript.converters import abstractConverter
from manuskript.functions import mainWindow
import logging
LOGGER = logging.getLogger(__name__)
class pandocConverter(abstractConverter):
@ -38,7 +40,7 @@ class pandocConverter(abstractConverter):
@classmethod
def convert(self, src, _from="markdown", to="html", args=None, outputfile=None):
if not self.isValid:
print("ERROR: pandocConverter is called but not valid.")
LOGGER.error("pandocConverter is called but not valid.")
return ""
cmd = [self.runCmd()]
@ -70,7 +72,7 @@ class pandocConverter(abstractConverter):
if stderr:
err = stderr.decode("utf-8")
print(err)
LOGGER.error(err)
QMessageBox.critical(mainWindow().dialog,
qApp.translate("Export", "Error"), err)
return None

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python
#--!-- coding: utf8 --!--
# --!-- coding: utf8 --!--
from enum import IntEnum
@ -16,6 +16,8 @@ class Character(IntEnum):
summaryPara = 8
summaryFull = 9
notes = 10
pov = 11
infos = 12
class Plot(IntEnum):
name = 0
@ -60,8 +62,24 @@ class Outline(IntEnum):
textFormat = 15
revisions = 16
customIcon = 17
charCount = 18
class Abstract(IntEnum):
title = 0
ID = 1
type = 2
class FlatData(IntEnum):
summarySituation = 0,
summarySentence = 1,
summaryPara = 2,
summaryPage = 3,
summaryFull = 4
class Model(IntEnum):
Character = 0
Plot = 1
PlotStep = 2
World = 3
Outline = 4
FlatData = 5

View file

@ -10,6 +10,8 @@ from PyQt5.QtWidgets import QWidget
from manuskript.models import outlineItem
from manuskript.functions import mainWindow
import logging
LOGGER = logging.getLogger(__name__)
class basicExporter:
@ -58,7 +60,7 @@ class basicExporter:
elif self.isValid() == 1:
run = self.customPath
else:
print("Error: no command for", self.name)
LOGGER.error("No command for %s.", self.name)
return None
r = subprocess.check_output([run] + args) # timeout=.2
return r.decode("utf-8")
@ -71,7 +73,7 @@ class basicExporter:
# try:
# output = subprocess.check_output(cmdl, stdin=cmd.stdout, stderr=subprocess.STDOUT) # , cwd="/tmp"
# except subprocess.CalledProcessError as e:
# print("Error!")
# LOGGER.error("Failed to read from process output.")
# return text
# cmd.wait()
#

View file

@ -24,7 +24,7 @@ class HTML(markdown):
exportDefaultSuffix = ".html"
def isValid(self):
return MD is not None
return MD != None
def settingsWidget(self):
w = markdownSettings(self)

View file

@ -10,6 +10,9 @@ from manuskript.models import outlineItem
from manuskript.ui.exporters.manuskript.plainTextSettings import exporterSettings
import codecs
import logging
LOGGER = logging.getLogger(__name__)
class plainText(basicFormat):
name = qApp.translate("Export", "Plain text")
description = qApp.translate("Export", """Simplest export to plain text. Allows you to use your own markup not understood
@ -51,10 +54,10 @@ class plainText(basicFormat):
def getExportFilename(self, settingsWidget, varName=None, filter=None):
if varName is None:
if varName == None:
varName = self.exportVarName
if filter is None:
if filter == None:
filter = self.exportFilter
settings = settingsWidget.getSettings()
@ -90,7 +93,7 @@ class plainText(basicFormat):
content = self.output(settingsWidget)
if not content:
print("Error: No content. Nothing saved.")
LOGGER.error("No content. Nothing saved.")
return
with open(filename, "w", encoding='utf8') as f:

View file

@ -31,7 +31,7 @@ class PDF(abstractOutput):
def isValid(self):
path = shutil.which("pdflatex") or shutil.which("xelatex")
return path is not None
return path != None
def output(self, settingsWidget, outputfile=None):
args = settingsWidget.runnableSettings()

View file

@ -13,6 +13,8 @@ from manuskript.exporter.pandoc.outputFormats import ePub, OpenDocument, DocX
from manuskript.exporter.pandoc.plainText import reST, markdown, latex, OPML
from manuskript.functions import mainWindow
import logging
LOGGER = logging.getLogger(__name__)
class pandocExporter(basicExporter):
@ -53,7 +55,7 @@ class pandocExporter(basicExporter):
elif self.isValid() == 1:
run = self.customPath
else:
print("Error: no command for pandoc")
LOGGER.error("No command for pandoc.")
return None
args = [run] + args
@ -101,7 +103,7 @@ class pandocExporter(basicExporter):
+ "Return code" + ": %d\n" % (p.returncode) \
+ "Command and parameters" + ":\n%s\n" % (p.args) \
+ "Stderr content" + ":\n" + stderr.decode("utf-8")
print(err)
LOGGER.error(err)
QMessageBox.critical(mainWindow().dialog, qApp.translate("Export", "Error"), err)
return None

View file

@ -75,7 +75,7 @@ class pandocSetting:
"""Return whether the specific setting is active with the given format."""
# Empty formats means all
if self.formats is "":
if self.formats == "":
return True
# "html" in "html markdown latex"
@ -108,9 +108,9 @@ class pandocSettings(markdownSettings):
# pandoc v1 only
"normalize": pandocSetting("--normalize", "checkbox", "",
qApp.translate("Export", "Normalize the document (cleaner)")),
"base-header": pandocSetting("--base-header-level=", "number", "",
"base-header": pandocSetting("--shift-heading-level-by=", "number", "",
qApp.translate("Export", "Specify the base level for headers: "),
default=1, min=1),
default=0, min=0),
"disable-YAML": pandocSetting("EXT-yaml_metadata_block", "checkbox", "",
qApp.translate("Export", "Disable YAML metadata block.\nUse that if you get YAML related error.")),

View file

@ -3,16 +3,21 @@
import os
import re
import sys
import pathlib
from random import *
from PyQt5.QtCore import Qt, QRect, QStandardPaths, QObject, QRegExp, QDir
from PyQt5.QtCore import QUrl, QTimer
from PyQt5.QtCore import Qt, QRect, QStandardPaths, QObject, QProcess, QRegExp
from PyQt5.QtCore import QDir, QUrl, QTimer
from PyQt5.QtGui import QBrush, QIcon, QPainter, QColor, QImage, QPixmap
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import qApp, QFileDialog, QTextEdit
from PyQt5.QtWidgets import qApp, QFileDialog
from manuskript.enums import Outline
import logging
LOGGER = logging.getLogger(__name__)
# Used to detect multiple connections
AUC = Qt.AutoConnection | Qt.UniqueConnection
MW = None
@ -23,6 +28,14 @@ def wordCount(text):
t = [l for l in t if l]
return len(t)
def charCount(text, use_spaces = True):
t = text.strip()
if not use_spaces:
t = t.replace(" ", "")
return len(t)
validate_ok = lambda *args, **kwargs: True
def uiParse(input, default, converter, validator=validate_ok):
"""
@ -442,5 +455,126 @@ def inspect():
s.function))
print(" " + "".join(s.code_context))
def search(searchRegex, text):
"""
Search all occurrences of a regex in a text.
:param searchRegex: a regex object with the search to perform
:param text: text to search on
:return: list of tuples (startPos, endPos)
"""
if text is not None:
return [(m.start(), m.end(), getSearchResultContext(text, m.start(), m.end())) for m in searchRegex.finditer(text)]
else:
return []
def getSearchResultContext(text, startPos, endPos):
matchSize = endPos - startPos
maxContextSize = max(matchSize, 600)
extraContextSize = int((maxContextSize - matchSize) / 2)
separator = "[...]"
context = ""
i = startPos - 1
while i > 0 and (startPos - i) < extraContextSize and text[i] != '\n':
i -= 1
contextStartPos = i
if i > 0:
context += separator + " "
context += text[contextStartPos:startPos].replace('\n', '')
context += '<b>' + text[startPos:endPos].replace('\n', '') + '</b>'
i = endPos
while i < len(text) and (i - endPos) < extraContextSize and text[i] != '\n':
i += 1
contextEndPos = i
context += text[endPos:contextEndPos].replace('\n', '')
if i < len(text):
context += " " + separator
return context
# Based on answer by jfs at:
# https://stackoverflow.com/questions/3718657/how-to-properly-determine-current-script-directory
def getManuskriptPath(follow_symlinks=True):
"""Used to obtain the path Manuskript is located at."""
if getattr(sys, 'frozen', False): # py2exe, PyInstaller, cx_Freeze
path = os.path.abspath(sys.executable)
else:
import inspect
path = inspect.getabsfile(getManuskriptPath) + "/../.."
if follow_symlinks:
path = os.path.realpath(path)
return os.path.dirname(path)
# Based on answer by kagronik at:
# https://stackoverflow.com/questions/14989858/get-the-current-git-hash-in-a-python-script
def getGitRevision(base_path):
"""Get git revision without relying on external processes or libraries."""
git_dir = pathlib.Path(base_path) / '.git'
if not git_dir.exists():
return None
with (git_dir / 'HEAD').open('r') as head:
ref = head.readline().split(' ')[-1].strip()
with (git_dir / ref).open('r') as git_hash:
return git_hash.readline().strip()
def getGitRevisionAsString(base_path, short=False):
"""Catches errors and presents a nice string."""
try:
rev = getGitRevision(base_path)
if rev is not None:
if short:
rev = rev[:7]
return "#" + rev
else:
return "" # not a git repository
except Exception as e:
LOGGER.warning("Failed to obtain Git revision: %s", e)
return "#ERROR"
def showInFolder(path, open_file_as_fallback=False):
'''
Show a file or folder in explorer/finder, highlighting it where possible.
Source: https://stackoverflow.com/a/46019091/3388962
'''
path = os.path.abspath(path)
dirPath = path if os.path.isdir(path) else os.path.dirname(path)
if sys.platform == 'win32':
args = []
args.append('/select,')
args.append(QDir.toNativeSeparators(path))
if QProcess.startDetached('explorer', args):
return True
elif sys.platform == 'darwin':
args = []
args.append('-e')
args.append('tell application "Finder"')
args.append('-e')
args.append('activate')
args.append('-e')
args.append('select POSIX file "%s"' % path)
args.append('-e')
args.append('end tell')
args.append('-e')
args.append('return')
if not QProcess.execute('/usr/bin/osascript', args):
return True
#if not QtCore.QProcess.execute('/usr/bin/open', [dirPath]):
# return
# TODO: Linux is not implemented. It has many file managers (nautilus, xdg-open, etc.)
# each of which needs special ways to highlight a file in a file manager window.
# Fallback.
return QDesktopServices.openUrl(QUrl(path if open_file_as_fallback else dirPath))
# Spellchecker loads writablePath from this file, so we need to load it after they get defined
from manuskript.functions.spellchecker import Spellchecker

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
import os, gzip, json, glob
import os, gzip, json, glob, re
from PyQt5.QtCore import QLocale
from collections import OrderedDict
from manuskript.functions import writablePath
@ -28,6 +28,17 @@ except ImportError:
symspellpy = None
use_language_check = False
try:
try:
import language_tool_python as languagetool
except:
import language_check as languagetool
use_language_check = True
except:
languagetool = None
class Spellchecker:
dictionaries = {}
# In order of priority
@ -106,7 +117,7 @@ class Spellchecker:
(lib, name) = values
try:
d = Spellchecker.dictionaries.get(dictionary, None)
if d is None:
if d == None:
for impl in Spellchecker.implementations:
if impl.isInstalled() and lib == impl.getLibraryName():
d = impl(name)
@ -117,6 +128,17 @@ class Spellchecker:
pass
return None
class BasicMatch:
def __init__(self, startIndex, endIndex):
self.start = startIndex
self.end = endIndex
self.locqualityissuetype = 'misspelling'
self.replacements = []
self.msg = ''
def getWord(self, text):
return text[self.start:self.end]
class BasicDictionary:
def __init__(self, name):
self._lang = name
@ -162,12 +184,45 @@ class BasicDictionary:
def availableDictionaries():
raise NotImplemented
def checkText(self, text):
# Based on http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/
WORDS = r'(?iu)((?:[^_\W]|\')+)[^A-Za-z0-9\']'
# (?iu) means case insensitive and Unicode
# ((?:[^_\W]|\')+) means words exclude underscores but include apostrophes
# [^A-Za-z0-9\'] used with above hack to prevent spellcheck while typing word
#
# See also https://stackoverflow.com/questions/2062169/regex-w-in-utf-8
matches = []
for word_object in re.finditer(WORDS, text):
word = word_object.group(1)
if (self.isMisspelled(word) and not self.isCustomWord(word)):
matches.append(BasicMatch(
word_object.start(1), word_object.end(1)
))
return matches
def isMisspelled(self, word):
raise NotImplemented
def getSuggestions(self, word):
raise NotImplemented
def findSuggestions(self, text, start, end):
if start < end:
word = text[start:end]
if (self.isMisspelled(word) and not self.isCustomWord(word)):
match = BasicMatch(start, end)
match.replacements = self.getSuggestions(word)
return [ match ]
return []
def isCustomWord(self, word):
return word.lower() in self._customDict
@ -218,7 +273,7 @@ class EnchantDictionary(BasicDictionary):
@staticmethod
def isInstalled():
return enchant is not None
return enchant != None
@staticmethod
def availableDictionaries():
@ -235,9 +290,9 @@ class EnchantDictionary(BasicDictionary):
if default_locale and not enchant.dict_exists(default_locale):
default_locale = None
if default_locale is None:
if default_locale == None:
default_locale = QLocale.system().name()
if default_locale is None:
if default_locale == None:
default_locale = self.availableDictionaries()[0]
return default_locale
@ -278,7 +333,7 @@ class PySpellcheckerDictionary(BasicDictionary):
@staticmethod
def isInstalled():
return pyspellchecker is not None
return pyspellchecker != None
@staticmethod
def availableDictionaries():
@ -298,7 +353,7 @@ class PySpellcheckerDictionary(BasicDictionary):
default_locale = QLocale.system().name()
if default_locale:
default_locale = default_locale[0:2]
if default_locale is None:
if default_locale == None:
default_locale = "en"
return default_locale
@ -363,7 +418,7 @@ class SymSpellDictionary(BasicDictionary):
@staticmethod
def isInstalled():
return symspellpy is not None
return symspellpy != None
@classmethod
def availableDictionaries(cls):
@ -422,8 +477,192 @@ class SymSpellDictionary(BasicDictionary):
# Since 6.3.8
self._dict.delete_dictionary_entry(word)
def get_languagetool_match_errorLength(match):
if use_language_check:
return match.errorlength
else:
return match.errorLength
def get_languagetool_match_ruleIssueType(match):
if use_language_check:
return match.locqualityissuetype
else:
return match.ruleIssueType
def get_languagetool_match_message(match):
if use_language_check:
return match.msg
else:
return match.message
class LanguageToolCache:
def __init__(self, tool, text):
self._length = len(text)
self._matches = self._buildMatches(tool, text)
def getMatches(self):
return self._matches
def _buildMatches(self, tool, text):
matches = []
for match in tool.check(text):
start = match.offset
end = start + get_languagetool_match_errorLength(match)
basic_match = BasicMatch(start, end)
basic_match.locqualityissuetype = get_languagetool_match_ruleIssueType(match)
basic_match.replacements = match.replacements
basic_match.msg = get_languagetool_match_message(match)
matches.append(basic_match)
return matches
def update(self, tool, text):
if len(text) != self._length:
self._matches = self._buildMatches(tool, text)
def get_languagetool_languages(tool):
if use_language_check:
return languagetool.get_languages()
else:
return tool._get_languages()
def get_languagetool_locale_language():
if use_language_check:
return languagetool.get_locale_language()
else:
return languagetool.utils.get_locale_language()
class LanguageToolDictionary(BasicDictionary):
if languagetool:
_tool = languagetool.LanguageTool()
else:
_tool = None
def __init__(self, name):
BasicDictionary.__init__(self, name)
if not (self._lang and self._lang in get_languagetool_languages(self._tool)):
self._lang = self.getDefaultDictionary()
self._tool.language(self._lang)
self._cache = {}
@staticmethod
def getLibraryName():
return "LanguageTool"
@staticmethod
def getLibraryURL():
if use_language_check:
return "https://pypi.org/project/language-check/"
else:
return "https://pypi.org/project/language-tool-python/"
@staticmethod
def isInstalled():
if languagetool != None:
# This check, if Java is installed, is necessary to
# make sure LanguageTool can be run without problems.
#
return (os.system('java -version') == 0)
return False
@staticmethod
def availableDictionaries():
if LanguageToolDictionary.isInstalled():
languages = list(get_languagetool_languages(LanguageToolDictionary._tool))
languages.sort()
return languages
return []
@staticmethod
def getDefaultDictionary():
if not LanguageToolDictionary.isInstalled():
return None
default_locale = get_languagetool_locale_language()
if default_locale and not default_locale in get_languagetool_languages(LanguageToolDictionary._tool):
default_locale = None
if default_locale == None:
default_locale = QLocale.system().name()
if default_locale == None:
default_locale = self.availableDictionaries()[0]
return default_locale
def checkText(self, text):
matches = []
if len(text) == 0:
return matches
textId = hash(text)
cacheEntry = None
if not textId in self._cache:
cacheEntry = LanguageToolCache(self._tool, text)
self._cache[textId] = cacheEntry
else:
cacheEntry = self._cache[textId]
cacheEntry.update(self._tool, text)
for match in cacheEntry.getMatches():
word = match.getWord(text)
if not (match.locqualityissuetype == 'misspelling' and self.isCustomWord(word)):
matches.append(match)
return matches
def isMisspelled(self, word):
if self.isCustomWord(word):
return False
for match in self.checkText(word):
if match.locqualityissuetype == 'misspelling':
return True
return False
def getSuggestions(self, word):
suggestions = []
for match in self.checkText(word):
suggestions += match.replacements
return suggestions
def findSuggestions(self, text, start, end):
matches = []
checked = self.checkText(text)
if start == end:
# Check for containing area:
for match in checked:
if (start >= match.start and start <= match.end):
matches.append(match)
else:
# Check for overlapping area:
for match in checked:
if (match.end > start and match.start < end):
matches.append(match)
return matches
# Register the implementations in order of priority
Spellchecker.implementations.append(EnchantDictionary)
Spellchecker.registerImplementation(EnchantDictionary)
Spellchecker.registerImplementation(SymSpellDictionary)
Spellchecker.registerImplementation(PySpellcheckerDictionary)
Spellchecker.registerImplementation(LanguageToolDictionary)

View file

@ -47,7 +47,7 @@ class mindMapImporter(abstractImporter):
node = root.find("node")
items = []
if node is not None:
if node != None:
items.extend(self.parseItems(node, parentItem))
ret = True
@ -97,7 +97,7 @@ class mindMapImporter(abstractImporter):
# Rich text content
content = ""
content = underElement.find("richcontent")
if content is not None:
if content != None:
# In Freemind, can be note or node
# Note: it's a note
# Node: it's the title of the node, in rich text
@ -130,7 +130,7 @@ class mindMapImporter(abstractImporter):
children = underElement.findall('node')
# Process children
if children is not None and len(children) > 0:
if children != None and len(children) > 0:
for c in children:
items.extend(self.parseItems(c, item))

View file

@ -52,10 +52,10 @@ class opmlImporter(abstractImporter):
bodyNode = opmlNode.find("body")
items = []
if bodyNode is not None:
if bodyNode != None:
outlineEls = bodyNode.findall("outline")
if outlineEls is not None:
if outlineEls != None:
for element in outlineEls:
items.extend(cls.parseItems(element, parentItem))
ret = True
@ -74,19 +74,20 @@ class opmlImporter(abstractImporter):
def parseItems(cls, underElement, parentItem=None):
items = []
title = underElement.get('text')
if title is not None:
if title != None:
card = outlineItem(parent=parentItem, title=title)
items.append(card)
body = ""
note = underElement.get('_note')
if note is not None and not cls.isWhitespaceOnly(note):
if note != None and not cls.isWhitespaceOnly(note):
#body = cls.restoreNewLines(note)
body = note
children = underElement.findall('outline')
if children is not None and len(children) > 0:
if children != None and len(children) > 0:
for el in children:
items.extend(cls.parseItems(el, card))
else:
@ -121,4 +122,4 @@ class opmlImporter(abstractImporter):
s = cls.restoreNewLines(inString)
s = ''.join(s.split())
return len(s) is 0
return len(s) == 0

View file

@ -38,7 +38,7 @@ class pandocImporter(abstractImporter):
r = pandocExporter().run(args)
if r is None:
if r == None:
return None
if formatTo == "opml":

View file

@ -9,6 +9,8 @@ import zipfile
import manuskript.load_save.version_0 as v0
import manuskript.load_save.version_1 as v1
import logging
LOGGER = logging.getLogger(__name__)
def saveProject(version=None):
@ -57,8 +59,8 @@ def loadProject(project):
with open(project, "r", encoding="utf-8") as f:
version = int(f.read())
print("Loading:", project)
print("Detected file format version: {}. Zip: {}.".format(version, isZip))
LOGGER.info("Loading: %s", project)
LOGGER.info("Detected file format version: {}. Zip: {}.".format(version, isZip))
if version == 0:
v0.loadProject(project)

View file

@ -16,6 +16,9 @@ from manuskript import settings
from manuskript.functions import iconColor, iconFromColorString, mainWindow
from manuskript.models.characterModel import Character, CharacterInfo
import logging
LOGGER = logging.getLogger(__name__)
try:
import zlib # Used with zipfile for compression
@ -37,7 +40,7 @@ def saveProject():
files.append((saveStandardItemModelXML(mw.mdlFlatData),
"flatModel.xml"))
print("ERROR: file format 0 does not save characters !")
LOGGER.error("File format 0 does not save characters!")
# files.append((saveStandardItemModelXML(mw.mdlCharacter),
# "perso.xml"))
files.append((saveStandardItemModelXML(mw.mdlWorld),
@ -91,7 +94,7 @@ def saveStandardItemModelXML(mdl, xml=None):
data = ET.SubElement(root, "data")
saveItem(data, mdl)
# print(qApp.tr("Saving to {}.").format(xml))
# LOGGER.info("Saving to {}.".format(xml))
if xml:
ET.ElementTree(root).write(xml, encoding="UTF-8", xml_declaration=True, pretty_print=True)
else:
@ -189,13 +192,13 @@ def loadStandardItemModelXML(mdl, xml, fromString=False):
"""Load data to a QStandardItemModel mdl from xml.
By default xml is a filename. If fromString=True, xml is a string containing the data."""
# print(qApp.tr("Loading {}... ").format(xml), end="")
# LOGGER.info("Loading {}...".format(xml))
if not fromString:
try:
tree = ET.parse(xml)
except:
print("Failed.")
LOGGER.error("Failed to load XML for QStandardItemModel (%s).", xml)
return
else:
root = ET.fromstring(xml)
@ -210,7 +213,7 @@ def loadStandardItemModelXML(mdl, xml, fromString=False):
for l in root.find("header").find("vertical").findall("label"):
vLabels.append(l.attrib["text"])
# print(root.find("header").find("vertical").text)
# LOGGER.debug(root.find("header").find("vertical").text)
# mdl.setVerticalHeaderLabels(vLabels)
# mdl.setHorizontalHeaderLabels(hLabels)

View file

@ -26,6 +26,9 @@ from manuskript.load_save.version_0 import loadFilesFromZip
from manuskript.models.characterModel import CharacterInfo
from manuskript.models import outlineItem
import logging
LOGGER = logging.getLogger(__name__)
try:
import zlib # Used with zipfile for compression
@ -40,6 +43,7 @@ characterMap = OrderedDict([
(Character.name, "Name"),
(Character.ID, "ID"),
(Character.importance, "Importance"),
(Character.pov, "POV"),
(Character.motivation, "Motivation"),
(Character.goal, "Goal"),
(Character.conflict, "Conflict"),
@ -47,11 +51,9 @@ characterMap = OrderedDict([
(Character.summarySentence, "Phrase Summary"),
(Character.summaryPara, "Paragraph Summary"),
(Character.summaryFull, "Full Summary"),
(Character.notes, "Notes"),
(Character.notes, "Notes")
])
# If true, logs infos while saving and loading.
LOG = False
def formatMetaData(name, value, tabLength=10):
@ -91,11 +93,6 @@ def slugify(name):
return newName
def log(*args):
if LOG:
print(" ".join(str(a) for a in args))
def saveProject(zip=None):
"""
Saves the project. If zip is False, the project is saved as a multitude of plain-text files for the most parts
@ -106,10 +103,10 @@ def saveProject(zip=None):
settings.
@return: True if successful, False otherwise.
"""
if zip is None:
if zip == None:
zip = settings.saveToZip
log("\n\nSaving to:", "zip" if zip else "folder")
LOGGER.info("Saving to: %s", "zip" if zip else "folder")
# List of files to be written
files = []
@ -123,8 +120,8 @@ def saveProject(zip=None):
project = mw.currentProject
# Sanity check (see PR-583): make sure we actually have a current project.
if project is None:
print("Error: cannot save project because there is no current project in the UI.")
if project == None:
LOGGER.error("Cannot save project because there is no current project in the UI.")
return False
# File format version
@ -197,7 +194,7 @@ def saveProject(zip=None):
# We skip the first row, which is empty and transparent
for i in range(1, mdl.rowCount()):
color = ""
if mdl.data(mdl.index(i, 0), Qt.DecorationRole) is not None:
if mdl.data(mdl.index(i, 0), Qt.DecorationRole) != None:
color = iconColor(mdl.data(mdl.index(i, 0), Qt.DecorationRole)).name(QColor.HexRgb)
color = color if color != "#ff000000" else "#00000000"
@ -306,7 +303,7 @@ def saveProject(zip=None):
# not exist, we check the parent folder, because it might be a new project.
if os.path.exists(project) and not os.access(project, os.W_OK) or \
not os.path.exists(project) and not os.access(os.path.dirname(project), os.W_OK):
print("Error: you don't have write access to save this project there.")
LOGGER.error("You don't have write access to save this project there.")
return False
####################################################################################################################
@ -340,7 +337,7 @@ def saveProject(zip=None):
folder = os.path.splitext(os.path.basename(project))[0]
# Debug
log("\nSaving to folder", folder)
LOGGER.debug("Saving to folder %s", folder)
# If cache is empty (meaning we haven't loaded from disk), we wipe folder, just to be sure.
if not cache:
@ -357,7 +354,7 @@ def saveProject(zip=None):
# Move the old file to the new place
try:
os.replace(oldPath, newPath)
log("* Renaming/moving {} to {}".format(old, new))
LOGGER.debug("* Renaming/moving {} to {}".format(old, new))
except FileNotFoundError:
# Maybe parent folder has been renamed
pass
@ -367,7 +364,7 @@ def saveProject(zip=None):
for f in cache:
f2 = f.replace(old, new)
if f2 != f:
log(" * Updating cache:", f, f2)
LOGGER.debug(" * Updating cache: %s, %s", f, f2)
cache2[f2] = cache[f]
cache = cache2
@ -378,7 +375,7 @@ def saveProject(zip=None):
# Check if content is in cache, and write if necessary
if path not in cache or cache[path] != content:
log("* Writing file {} ({})".format(path, "not in cache" if path not in cache else "different"))
LOGGER.debug("* Writing file {} ({})".format(path, "not in cache" if path not in cache else "different"))
# mode = "w" + ("b" if type(content) == bytes else "")
if type(content) == bytes:
with open(filename, "wb") as f:
@ -392,7 +389,7 @@ def saveProject(zip=None):
# Removing phantoms
for path in [p for p in cache if p not in [p for p, c in files]]:
filename = os.path.join(dir, folder, path)
log("* Removing", path)
LOGGER.debug("* Removing %s", path)
if os.path.isdir(filename):
shutil.rmtree(filename)
@ -409,7 +406,7 @@ def saveProject(zip=None):
newDir = os.path.join(root, dir)
try:
os.removedirs(newDir)
log("* Removing empty directory:", newDir)
LOGGER.debug("* Removing empty directory: %s", newDir)
except:
# Directory not empty, we don't remove.
pass
@ -535,8 +532,8 @@ def exportOutlineItem(root):
lp = child._lastPath
if lp and spath != lp:
moves.append((lp, spath))
log(child.title(), "has been renamed (", lp, "", spath, ")")
log(" → We mark for moving:", lp)
LOGGER.debug("%s has been renamed (%s%s)", child.title(), lp, spath)
LOGGER.debug(" → We mark for moving: %s", lp)
# Updates item last's path
child._lastPath = spath
@ -552,7 +549,7 @@ def exportOutlineItem(root):
files.append((spath, content))
else:
log("Unknown type")
LOGGER.debug("Unknown type: %s", child.type())
f, m, r = exportOutlineItem(child)
files += f
@ -630,7 +627,7 @@ def loadProject(project, zip=None):
####################################################################################################################
# Read and store everything in a dict
log("\nLoading {} ({})".format(project, "ZIP" if zip else "not zip"))
LOGGER.debug("Loading {} ({})".format(project, "zip" if zip else "folder"))
if zip:
files = loadFilesFromZip(project)
@ -694,7 +691,7 @@ def loadProject(project, zip=None):
mdl = mw.mdlLabels
mdl.appendRow(QStandardItem("")) # Empty = No labels
if "labels.txt" in files:
log("\nReading labels:")
LOGGER.debug("Reading labels:")
for s in files["labels.txt"].split("\n"):
if not s:
continue
@ -702,7 +699,7 @@ def loadProject(project, zip=None):
m = re.search(r"^(.*?):\s*(.*)$", s)
txt = m.group(1)
col = m.group(2)
log("* Add status: {} ({})".format(txt, col))
LOGGER.debug("* Add status: {} ({})".format(txt, col))
icon = iconFromColorString(col)
mdl.appendRow(QStandardItem(icon, txt))
@ -715,11 +712,11 @@ def loadProject(project, zip=None):
mdl = mw.mdlStatus
mdl.appendRow(QStandardItem("")) # Empty = No status
if "status.txt" in files:
log("\nReading Status:")
LOGGER.debug("Reading status:")
for s in files["status.txt"].split("\n"):
if not s:
continue
log("* Add status:", s)
LOGGER.debug("* Add status: %s", s)
mdl.appendRow(QStandardItem(s))
else:
errors.append("status.txt")
@ -761,7 +758,7 @@ def loadProject(project, zip=None):
mdl = mw.mdlPlots
if "plots.xml" in files:
log("\nReading plots:")
LOGGER.debug("Reading plots:")
# xml = bytearray(files["plots.xml"], "utf-8")
root = ET.fromstring(files["plots.xml"])
@ -770,7 +767,7 @@ def loadProject(project, zip=None):
row = getStandardItemRowFromXMLEnum(plot, Plot)
# Log
log("* Add plot: ", row[0].text())
LOGGER.debug("* Add plot: %s", row[0].text())
# Characters
if row[Plot.characters].text():
@ -797,7 +794,7 @@ def loadProject(project, zip=None):
mdl = mw.mdlWorld
if "world.opml" in files:
log("\nReading World:")
LOGGER.debug("Reading World:")
# xml = bytearray(files["plots.xml"], "utf-8")
root = ET.fromstring(files["world.opml"])
body = root.find("body")
@ -813,7 +810,7 @@ def loadProject(project, zip=None):
# Characters
mdl = mw.mdlCharacter
log("\nReading Characters:")
LOGGER.debug("Reading Characters:")
for f in [f for f in files if "characters" in f]:
md, body = parseMMDFile(files[f])
c = mdl.addCharacter()
@ -839,7 +836,7 @@ def loadProject(project, zip=None):
else:
c.infos.append(CharacterInfo(c, desc, val))
log("* Adds {} ({})".format(c.name(), c.ID()))
LOGGER.debug("* Adds {} ({})".format(c.name(), c.ID()))
####################################################################################################################
# Texts
@ -847,14 +844,14 @@ def loadProject(project, zip=None):
# everything, but the outline folder takes precedence (in cases it's been edited outside of manuskript.
mdl = mw.mdlOutline
log("\nReading outline:")
LOGGER.debug("Reading outline:")
paths = [f for f in files if "outline" in f]
outline = OrderedDict()
# We create a structure of imbricated OrderedDict to store the whole tree.
for f in paths:
split = f.split(os.path.sep)[1:]
# log("* ", split)
# LOGGER.debug("* %s", split)
last = ""
parent = outline
@ -900,7 +897,7 @@ def addTextItems(mdl, odict, parent=None):
@param odict: OrderedDict
@return: nothing
"""
if parent is None:
if parent == None:
parent = mdl.rootItem
for k in odict:
@ -909,7 +906,7 @@ def addTextItems(mdl, odict, parent=None):
if type(odict[k]) == OrderedDict and "folder.txt" in odict[k]:
# Adds folder
log("{}* Adds {} to {} (folder)".format(" " * parent.level(), k, parent.title()))
LOGGER.debug("{}* Adds {} to {} (folder)".format(" " * parent.level(), k, parent.title()))
item = outlineFromMMD(odict[k]["folder.txt"], parent=parent)
item._lastPath = odict[k + ":lastPath"]
@ -918,12 +915,12 @@ def addTextItems(mdl, odict, parent=None):
# k is not a folder
elif type(odict[k]) == str and k != "folder.txt" and not ":lastPath" in k:
log("{}* Adds {} to {} (file)".format(" " * parent.level(), k, parent.title()))
LOGGER.debug("{}* Adds {} to {} (file)".format(" " * parent.level(), k, parent.title()))
item = outlineFromMMD(odict[k], parent=parent)
item._lastPath = odict[k + ":lastPath"]
elif not ":lastPath" in k and k != "folder.txt":
print("* Strange things in file {}".format(k))
LOGGER.debug("Strange things in file %s".format(k))
def outlineFromMMD(text, parent):
@ -934,9 +931,11 @@ def outlineFromMMD(text, parent):
@return: outlineItem
"""
item = outlineItem(parent=parent)
md, body = parseMMDFile(text, asDict=True)
# Assign ID on creation, to avoid generating a new ID for this object
item = outlineItem(parent=parent, ID=md.pop('ID'))
# Store metadata
for k in md:
if k in Outline.__members__:
@ -973,17 +972,19 @@ def appendRevisions(mdl, root):
# Get root's ID
ID = root.attrib["ID"]
if not ID:
log("* Serious problem: no ID!")
LOGGER.debug("* Serious problem: no ID!")
LOGGER.error("Revision has no ID associated!")
continue
# Find outline item in model
item = mdl.getItemByID(ID)
if not item:
log("* Error: no item whose ID is", ID)
LOGGER.debug("* Error: no item whose ID is %s", ID)
LOGGER.error("Could not identify the item matching the revision ID.")
continue
# Store revision
log("* Appends revision ({}) to {}".format(child.attrib["timestamp"], item.title()))
LOGGER.debug("* Appends revision ({}) to {}".format(child.attrib["timestamp"], item.title()))
item.appendRevision(child.attrib["timestamp"], child.attrib["text"])
@ -995,7 +996,7 @@ def getOutlineItem(item, enum):
@return: [QStandardItem]
"""
row = getStandardItemRowFromXMLEnum(item, enum)
log("* Add worldItem:", row[0].text())
LOGGER.debug("* Add worldItem: %s", row[0].text())
for child in item:
sub = getOutlineItem(child, enum)
row[0].appendRow(sub)

377
manuskript/logging.py Normal file
View file

@ -0,0 +1,377 @@
# -*- coding: utf-8 -*-
# While all logging should be done through the facilities offered by the
# standard python `logging` module, this module will take care of specific
# manuskript needs to keep it separate from the rest of the logic.
import os
import sys
import time
import logging
import pathlib
from manuskript.functions import writablePath
from importlib import import_module
from pprint import pformat
LOGGER = logging.getLogger(__name__)
LOGFORMAT_CONSOLE = "%(levelname)s> %(message)s"
LOGFORMAT_FILE = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
def setUp(console_level=logging.WARN):
"""Sets up a convenient environment for logging.
To console: >WARNING, plain. (Only the essence.)"""
# The root_logger should merely trigger on warnings since it is the final
# stop after all categories we really care about didn't match.
root_logger = logging.getLogger()
root_logger.setLevel(logging.WARN)
# The manuskript_logger is what all of our own code will come by.
# Obviously, we care greatly about logging every single message.
manuskript_logger = logging.getLogger("manuskript")
manuskript_logger.setLevel(logging.DEBUG)
# The qt_logger sees all the Qt nonsense when it breaks.
# We don't really want to know... but we have to know.
qt_logger = logging.getLogger("qt")
qt_logger.setLevel(logging.DEBUG)
# Send logs of WARNING+ to STDERR for higher visibility.
ch = logging.StreamHandler()
ch.setLevel(console_level)
ch.setFormatter(logging.Formatter(LOGFORMAT_CONSOLE))
root_logger.addHandler(ch)
# Any exceptions we did not account for need to be logged.
logFutureExceptions()
LOGGER.debug("Logging to STDERR.")
def getDefaultLogFile():
"""Returns a filename to log to inside {datadir}/logs/.
It also prunes old logs so that we do not hog disk space excessively over time.
"""
# Ensure logs directory exists.
logsPath = os.path.join(writablePath(), "logs")
os.makedirs(logsPath, exist_ok=True)
# Prune irrelevant log files. They are only kept for 35 days.
try: # Guard against os.scandir() in the name of paranoia.
now = time.time()
with os.scandir(logsPath) as it:
for f in it:
try: # Avoid triggering outer try-except inside loop.
if f.is_dir():
continue # If a subdirectory exists for whatever reason, don't touch it.
if (now - f.stat().st_ctime) // (24 * 3600) >= 35:
os.remove(f)
except OSError:
continue # Fail silently, but make sure we check other files.
except OSError:
pass # Fail silently. Don't explode and prevent Manuskript from starting.
return os.path.join(logsPath, "%Y-%m-%d_%H-%M-%S_manuskript#%#.log")
def formatLogName(formatString, pid=None, now=None):
"""A minor hack on top of `strftime()` to support an identifier for the process ID.
We want to support this in case some genius manages to start two manuskript processes
during the exact same second, causing a conflict in log filenames.
Additionally, there is a tiny chance that the pid could actually end up relevant when
observing strange behaviour with a Manuskript process but having multiple instances open.
"""
if pid == None:
pid = os.getpid()
if now == None:
now = time.localtime()
# Replace %# that is NOT preceded by %. Although this is not a perfect solution,
# it is good enough because it is unlikely anyone would want to format '%pid'.
lidx = 0
while True: # This could be neater with the := operator of Python 3.8 ...
fidx = formatString.find("%#", lidx)
if fidx == -1:
break
elif (fidx == 0) or (formatString[fidx-1] != "%"):
formatString = formatString[:fidx] + str(pid) + formatString[fidx+2:]
lidx = fidx + len(str(pid)) - 2
else: # skip and avoid endless loop
lidx = fidx + 1
# Finally apply strftime normally.
return time.strftime(formatString, now)
def logToFile(file_level=logging.DEBUG, logfile=None):
"""Sets up the FileHandler that logs to a file.
This is being done separately due to relying on QApplication being properly
configured; without it we cannot detect the proper location for the log file.
To log file: >DEBUG, timestamped. (All the details.)"""
if logfile is None:
logfile = getDefaultLogFile()
logfile = formatLogName(logfile)
# Log with extreme prejudice; everything goes to the log file.
# Because Qt gave me a megabyte-sized logfile while testing, it
# makes sense that the default behaviour of appending to existing
# log files may not be in our users best interest for the time
# being. (Unfortunately.)
try:
fh = logging.FileHandler(logfile, mode='w', encoding='utf-8')
fh.setLevel(file_level)
fh.setFormatter(logging.Formatter(LOGFORMAT_FILE))
root_logger = logging.getLogger()
root_logger.addHandler(fh)
# Use INFO level to make it easier to find for users.
LOGGER.info("Logging to file: %s", logfile)
except Exception as ex:
LOGGER.warning("Cannot log to file '%s'. Reason: %s", logfile, ex)
def getLogFilePath():
"""Extracts a filename we are logging to from the first FileHandler we find."""
root_logger = logging.getLogger()
for handler in root_logger.handlers:
if isinstance(handler, logging.FileHandler):
return handler.baseFilename
return None
# Log uncaught and unraisable exceptions.
# Uncaught exceptions trigger moments before a thread is terminated due to
# an uncaught exception. It is the final stop, and as such is very likely
# to be the reason Manuskript suddenly closed on the user without warning.
# (It can also happen on other threads, but it is a bad thing regardless!)
def handle_uncaught_exception(exc_type, exc_value, exc_traceback):
# Allow Ctrl+C for script execution to keep functioning as-is.
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return # default exception hook handled it
# Anything that reaches this handler can be considered a deal-breaker.
LOGGER.critical("An unhandled exception has occurred!", exc_info=(exc_type, exc_value, exc_traceback))
# Exit the program to preserve PyQt 'functionality' that is broken by
# having our own uncaught exception hook. For more information, see:
# https://stackoverflow.com/questions/49065371/why-does-sys-excepthook-behave-differently-when-wrapped
sys.exit(1)
# Note that without it, unhandled Python exceptions thrown while in the
# bowels of Qt may be written to the log multiple times. Under the motto
# of failing fast and not having a misleading log file, this appears to
# be the best course of action.
# The situation with threads and uncaught exceptions is fraught in peril.
# Hopefully this solves our problems on more recent versions of Python.
def handle_uncaught_thread_exception(args):
if issubclass(exc_type, SystemExit):
return # match behaviour of default hook, see manual
# Anything that reaches this handler can be considered a minor deal-breaker.
LOGGER.error("An unhandled exception has occurred in a thread: %s", repr(args.thread),
exc_info=(args.exc_type, args.exc_value, args.exc_traceback))
# Unraisable exceptions are exceptions that failed to be raised to a caller
# due to the nature of the exception. Examples: __del__(), GC error, etc.
# Logging these may expose bugs / errors that would otherwise go unnoticed.
def handle_unraisable_exception(unraisable):
# Log as warning because the application is likely to limp along with
# no serious side effects; a resource leak is the most likely.
LOGGER.warning("%s: %s", unraisable.err_msg or "Exception ignored in", repr(unraisable.object),
exc_info=(unraisable.exc_type, unraisable.exc_value, unraisable.exc_traceback))
# Because we are already somewhat careful in regards to the order of code
# execution when it comes to setting up the logging environment, this has
# been put in its own function as opposed to letting a direct import handle it.
def logFutureExceptions():
"""Log all the interesting exceptions that may happen in the future."""
sys.excepthook = handle_uncaught_exception
try:
import threading # threading module was optional pre-3.7
if hasattr(threading, "excepthook"): # Python 3.8+
threading.excepthook = handle_uncaught_thread_exception
except:
pass
if hasattr(sys, "unraisablehook"): # Python 3.8+
sys.unraisablehook = handle_unraisable_exception
# Qt has its own logging facility that we would like to integrate into our own.
# See: http://thispageintentionally.blogspot.com/2014/03/trapping-qt-log-messages.html
from PyQt5.QtCore import qInstallMessageHandler, QLibraryInfo, QMessageLogContext
from PyQt5.Qt import QtMsgType
def qtMessageHandler(msg_type, msg_log_context, msg_string):
"""Forwards Qt messages to Python logging system."""
# Convert Qt msg type to logging level
log_level = [logging.DEBUG,
logging.WARNING,
logging.ERROR,
logging.FATAL] [ int(msg_type) ]
qtcl = logging.getLogger(msg_log_context.category or "qt.???")
# Some information may not be available unless using a PyQt debug build.
# See: https://www.riverbankcomputing.com/static/Docs/PyQt5/api/qtcore/qmessagelogcontext.html
if QLibraryInfo.isDebugBuild():
qtcl.log(logging.DEBUG,
' @ {0} : {1}'.format((msg_log_context.file or "<unknown source file>"), msg_log_context.line)
)
qtcl.log(logging.DEBUG,
' ! {0}'.format((msg_log_context.function or "<unknown function>"))
)
qtcl.log(log_level, msg_string)
def integrateQtLogging():
"""Integrates Qt logging facilities to be a part of our own."""
# Note: the qtlogger is initialized in setUp() because it fits in
# nicely with the initialization of the other loggers over there.
# I also feel a lot safer this way. Qt is a curse that just keeps
# on giving, even when it isn't actually at fault. I hate you, Qt.
qInstallMessageHandler(qtMessageHandler)
def versionTupleToString(t):
"""A bit of generic tuple conversion code that hopefully handles all the
different sorts of tuples we may come across while logging versions.
None -> "N/A"
(,) -> "N/A"
(2, 4, 6) -> "2.4.6"
(2, 4, "alpha", 8) -> "2.4-alpha.8"
"""
s = []
if t is None or len(t) == 0:
return "N/A"
else:
s.append(str(t[0]))
def version_chunk(v):
if isinstance(v, str):
return "-", str(v)
else:
return ".", str(v)
s.extend(f for p in t[1:] for f in version_chunk(p))
return "".join(s)
def attributesFromOptionalModule(module, *attributes):
"""It is nice to cut down on the try-except boilerplate by
putting this logic into its own function.
Returns as many values as there are attributes.
A value will be None if it failed to get the attribute."""
assert(len(attributes) != 0)
v = []
try:
m = import_module(module)
for a in attributes:
v.append(getattr(m, a, None))
except ImportError:
v.extend(None for _ in range(len(attributes)))
if len(v) == 1:
# Return the value directly so we can use it in an expression.
return v[0]
else:
# The list is consumed as a part of the unpacking syntax.
return v
def logRuntimeInformation(logger=None):
"""Logs all important runtime information neatly together.
Due to the generic nature, use the manuskript logger by default."""
if not logger:
logger = logging.getLogger("manuskript")
vt2s = versionTupleToString
afom = attributesFromOptionalModule
# Basic system information.
from platform import python_version, platform, processor, machine
logger.info("Operating System: %s", platform())
logger.info("Hardware: %s / %s", machine(), processor())
# Information about the running instance. See:
# https://pyinstaller.readthedocs.io/en/v3.3.1/runtime-information.html
# http://www.py2exe.org/index.cgi/Py2exeEnvironment
# https://cx-freeze.readthedocs.io/en/latest/faq.html#data-files
frozen = getattr(sys, 'frozen', False)
if frozen:
logger.info("Running in a frozen (packaged) state.")
logger.debug("* sys.frozen = %s", pformat(frozen))
# PyInstaller, py2exe and cx_Freeze modules are not accessible while frozen,
# so logging their version is (to my knowledge) impossible without including
# special steps into the distribution process. But some traces do exist...
logger.debug("* sys._MEIPASS = %s", getattr(sys, '_MEIPASS', "N/A")) # PyInstaller bundle
# cx_Freeze and py2exe do not appear to leave anything similar exposed.
else:
logger.info("Running from unpackaged source code.")
# File not found? These bits of information might help.
logger.debug("* sys.executable = %s", pformat(sys.executable))
logger.debug("* sys.argv = %s", pformat(sys.argv))
logger.debug("* sys.path = %s", pformat(sys.path))
logger.debug("* sys.prefix = %s", pformat(sys.prefix))
# Manuskript and Python info.
from manuskript.functions import getGitRevisionAsString, getManuskriptPath
from manuskript.version import getVersion
logger.info("Manuskript %s%s (Python %s)", getVersion(),
getGitRevisionAsString(getManuskriptPath(), short=True),
python_version())
# Installed Python packages.
# PyQt + Qt
from PyQt5.Qt import PYQT_VERSION_STR, qVersion
from PyQt5.QtCore import QT_VERSION_STR
logger.info("* PyQt %s (compiled against Qt %s)", PYQT_VERSION_STR, QT_VERSION_STR)
logger.info(" * Qt %s (runtime)", qVersion())
# Lxml
# See: https://lxml.de/FAQ.html#i-think-i-have-found-a-bug-in-lxml-what-should-i-do
from lxml import etree
logger.info("* lxml.etree %s", vt2s(etree.LXML_VERSION))
logger.info(" * libxml %s (compiled: %s)", vt2s(etree.LIBXML_VERSION), vt2s(etree.LIBXML_COMPILED_VERSION))
logger.info(" * libxslt %s (compiled: %s)", vt2s(etree.LIBXSLT_VERSION), vt2s(etree.LIBXSLT_COMPILED_VERSION))
# Spellcheckers. (Optional)
enchant_mod_ver, enchant_lib_ver = afom("enchant", "__version__", "get_enchant_version")
if enchant_lib_ver:
enchant_lib_ver = enchant_lib_ver()
if isinstance(enchant_lib_ver, bytes): # PyEnchant version < 3.0.2
enchant_lib_ver = enchant_lib_ver.decode('utf-8')
logger.info("* pyEnchant %s (libenchant: %s)", enchant_mod_ver or "N/A", enchant_lib_ver or "N/A")
logger.info("* pySpellChecker %s", afom("spellchecker", "__version__") or "N/A")
logger.info("* Symspellpy %s", afom("symspellpy", "__version__") or "N/A")
# Markdown. (Optional)
logger.info("* Markdown %s", afom("markdown", "__version__") or "N/A")
# Web rendering engine
from manuskript.ui.views.webView import webEngine
logger.info("Web rendering engine: %s", webEngine)
# Do not collect version information for Pandoc; that would require
# executing `pandov -v` and parsing the output, all of which is too slow.

View file

@ -4,8 +4,9 @@ import faulthandler
import os
import platform
import sys
import signal
import manuskript.ui.views.webView
import manuskript.logging
from PyQt5.QtCore import QLocale, QTranslator, QSettings, Qt
from PyQt5.QtGui import QIcon, QColor, QPalette
from PyQt5.QtWidgets import QApplication, qApp, QStyleFactory
@ -15,14 +16,26 @@ from manuskript.version import getVersion
faulthandler.enable()
def prepare(tests=False):
import logging
LOGGER = logging.getLogger(__name__)
def prepare(arguments, tests=False):
app = QApplication(sys.argv)
app.setOrganizationName("manuskript"+("_tests" if tests else ""))
app.setOrganizationName("manuskript" + ("_tests" if tests else ""))
app.setOrganizationDomain("www.theologeek.ch")
app.setApplicationName("manuskript"+("_tests" if tests else ""))
app.setApplicationName("manuskript" + ("_tests" if tests else ""))
app.setApplicationVersion(getVersion())
print("Running manuskript version {}.".format(getVersion()))
# Beginning logging to a file. This cannot be done earlier due to the
# default location of the log file being dependent on QApplication.
manuskript.logging.logToFile(logfile=arguments.logfile)
# Handle all sorts of Qt logging messages in Python.
manuskript.logging.integrateQtLogging()
# Log all the versions for less headaches.
manuskript.logging.logRuntimeInformation()
icon = QIcon()
for i in [16, 32, 64, 128, 256, 512]:
icon.addFile(appPath("icons/Manuskript/icon-{}px.png".format(i)))
@ -38,13 +51,14 @@ def prepare(tests=False):
# Translation process
appTranslator = QTranslator(app)
# By default: locale
def tryLoadTranslation(translation, source):
"""Tries to load and activate a given translation for use."""
if appTranslator.load(translation, appPath("i18n")):
app.installTranslator(appTranslator)
print("Loaded translation: {}".format(translation))
LOGGER.info("Loaded translation: {}".format(translation))
# Note: QTranslator.load() does some fancy heuristics where it simplifies
# the given locale until it is 'close enough' if the given filename does
# not work out. For example, if given 'i18n/manuskript_en_US.qm', it tries:
@ -59,7 +73,7 @@ def prepare(tests=False):
# filenames when you observe strange behaviour with the loaded translations.
return True
else:
print("No translation found or loaded. ({})".format(translation))
LOGGER.info("No translation found or loaded. ({})".format(translation))
return False
def activateTranslation(translation, source):
@ -82,7 +96,7 @@ def prepare(tests=False):
break
if using_builtin_translation:
print("Using the builtin translation.")
LOGGER.info("Using the builtin translation. (U.S. English)")
# Load application translation
translation = ""
@ -96,24 +110,26 @@ def prepare(tests=False):
translation = QLocale().uiLanguages()
source = "available ui languages"
print("Preferred translation: {} (based on {})".format(("builtin" if translation == "" else translation), source))
LOGGER.info("Preferred translation: {} (based on {})".format(("builtin" if translation == "" else translation), source))
activateTranslation(translation, source)
def respectSystemDarkThemeSetting():
"""Adjusts the Qt theme to match the OS 'dark theme' setting configured by the user."""
if platform.system() is not 'Windows':
if platform.system() != 'Windows':
return
# Basic Windows 10 Dark Theme support.
# Source: https://forum.qt.io/topic/101391/windows-10-dark-theme/4
themeSettings = QSettings("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", QSettings.NativeFormat)
themeSettings = QSettings(
"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
QSettings.NativeFormat)
if themeSettings.value("AppsUseLightTheme") == 0:
darkPalette = QPalette()
darkColor = QColor(45,45,45)
disabledColor = QColor(127,127,127)
darkColor = QColor(45, 45, 45)
disabledColor = QColor(127, 127, 127)
darkPalette.setColor(QPalette.Window, darkColor)
darkPalette.setColor(QPalette.WindowText, Qt.white)
darkPalette.setColor(QPalette.Base, QColor(18,18,18))
darkPalette.setColor(QPalette.Base, QColor(18, 18, 18))
darkPalette.setColor(QPalette.AlternateBase, darkColor)
darkPalette.setColor(QPalette.ToolTipBase, Qt.white)
darkPalette.setColor(QPalette.ToolTipText, Qt.white)
@ -137,7 +153,7 @@ def prepare(tests=False):
# This broke the Settings Dialog at one point... and then it stopped breaking it.
# TODO: Why'd it break? Check if tooltips look OK... and if not, make them look OK.
#app.setStyleSheet("QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }")
# app.setStyleSheet("QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }")
respectSystemDarkThemeSetting()
@ -159,16 +175,17 @@ def prepare(tests=False):
MW._defaultCursorFlashTime = qApp.cursorFlashTime()
# Command line project
if len(sys.argv) > 1 and sys.argv[1][-4:] == ".msk":
#if len(sys.argv) > 1 and sys.argv[1][-4:] == ".msk":
if arguments.filename is not None and arguments.filename[-4:] == ".msk":
#TODO: integrate better with argparsing.
if os.path.exists(sys.argv[1]):
path = os.path.abspath(sys.argv[1])
MW._autoLoadProject = path
return app, MW
def launch(app, MW = None):
if MW is None:
def launch(arguments, app, MW = None):
if MW == None:
from manuskript.functions import mainWindow
MW = mainWindow()
@ -176,10 +193,10 @@ def launch(app, MW = None):
# Support for IPython Jupyter QT Console as a debugging aid.
# Last argument must be --console to enable it
# Code reference :
# Code reference :
# https://github.com/ipython/ipykernel/blob/master/examples/embedding/ipkernel_qtapp.py
# https://github.com/ipython/ipykernel/blob/master/examples/embedding/internal_ipkernel.py
if len(sys.argv) > 1 and sys.argv[-1] == "--console":
if arguments.console:
try:
from IPython.lib.kernel import connect_qtconsole
from ipykernel.kernelapp import IPKernelApp
@ -188,7 +205,7 @@ def launch(app, MW = None):
# Create IPython kernel within our application
kernel = IPKernelApp.instance()
# Initialize it and use matplotlib for main event loop integration with QT
kernel.initialize(['python', '--matplotlib=qt'])
@ -207,6 +224,7 @@ def launch(app, MW = None):
app.quit()
console.kill()
kernel.io_loop.stop()
app.lastWindowClosed.connect(console_cleanup)
# Very important, IPython-specific step: this gets GUI event loop
@ -221,17 +239,61 @@ def launch(app, MW = None):
qApp.exec_()
qApp.deleteLater()
def sigint_handler(sig, MW):
def handler(*args):
# Log before winding down to preserve order of cause and effect.
LOGGER.info(f'{sig} received. Quitting...')
MW.close()
print(f'{sig} received, quit.')
return handler
def setup_signal_handlers(MW):
signal.signal(signal.SIGINT, sigint_handler("SIGINT", MW))
signal.signal(signal.SIGTERM, sigint_handler("SIGTERM", MW))
def process_commandline(argv):
import argparse
parser = argparse.ArgumentParser(description="Run the manuskript application.")
parser.add_argument("--console", help="open the IPython Jupyter QT Console as a debugging aid",
action="store_true")
parser.add_argument("-v", "--verbose", action="count", default=1, help="lower the threshold for messages logged to the terminal")
parser.add_argument("-L", "--logfile", default=None, help="override the default log file location")
parser.add_argument("filename", nargs="?", metavar="FILENAME", help="the manuskript project (.msk) to open")
args = parser.parse_args(args=argv)
# Verbosity logic, see: https://gist.github.com/ms5/9f6df9c42a5f5435be0e
#args.verbose = 70 - (10*args.verbose) if args.verbose > 0 else 0
# Users cannot report what they do not notice: show CRITICAL, ERROR and WARNING always.
# Note that the default is set to 1, so account for that.
args.verbose = 40 - (10*args.verbose) if args.verbose > 0 else 0
return args
def run():
"""
Run separates prepare and launch for two reasons:
1. I've read somewhere it helps with potential segfault (see comment below)
2. So that prepare can be used in tests, without running the whole thing
"""
# Parse command-line arguments.
arguments = process_commandline(sys.argv)
# Initialize logging. (Does not include Qt integration yet.)
manuskript.logging.setUp(console_level=arguments.verbose)
# Need to return and keep `app` otherwise it gets deleted.
app, MW = prepare()
app, MW = prepare(arguments)
setup_signal_handlers(MW)
# Separating launch to avoid segfault, so it seem.
# Cf. http://stackoverflow.com/questions/12433491/is-this-pyqt-4-python-bug-or-wrongly-behaving-code
launch(app, MW)
launch(arguments, app, MW)
if __name__ == "__main__":
run()

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
import imp
import importlib
import os
import re
@ -9,13 +9,14 @@ from PyQt5.QtCore import (pyqtSignal, QSignalMapper, QTimer, QSettings, Qt, QPoi
QRegExp, QUrl, QSize, QModelIndex)
from PyQt5.QtGui import QStandardItemModel, QIcon, QColor
from PyQt5.QtWidgets import QMainWindow, QHeaderView, qApp, QMenu, QActionGroup, QAction, QStyle, QListWidgetItem, \
QLabel, QDockWidget, QWidget, QMessageBox
QLabel, QDockWidget, QWidget, QMessageBox, QLineEdit
from manuskript import settings
from manuskript.enums import Character, PlotStep, Plot, World, Outline
from manuskript.functions import wordCount, appPath, findWidgetsOfClass
from manuskript.functions import wordCount, appPath, findWidgetsOfClass, openURL, showInFolder
import manuskript.functions as F
from manuskript import loadSave
from manuskript.logging import getLogFilePath
from manuskript.models.characterModel import characterModel
from manuskript.models import outlineModel
from manuskript.models.plotModel import plotModel
@ -38,6 +39,9 @@ from manuskript.ui.statusLabel import statusLabel
from manuskript.ui.views.textEditView import textEditView
from manuskript.functions import Spellchecker
import logging
LOGGER = logging.getLogger(__name__)
class MainWindow(QMainWindow, Ui_MainWindow):
# dictChanged = pyqtSignal(str)
@ -129,6 +133,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.actCopy.triggered.connect(self.documentsCopy)
self.actCut.triggered.connect(self.documentsCut)
self.actPaste.triggered.connect(self.documentsPaste)
self.actSearch.triggered.connect(self.doSearch)
self.actRename.triggered.connect(self.documentsRename)
self.actDuplicate.triggered.connect(self.documentsDuplicate)
self.actDelete.triggered.connect(self.documentsDelete)
@ -175,6 +180,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# Main Menu:: Tool
self.actToolFrequency.triggered.connect(self.frequencyAnalyzer)
self.actSupport.triggered.connect(self.support)
self.actLocateLog.triggered.connect(self.locateLogFile)
self.actAbout.triggered.connect(self.about)
self.makeUIConnections()
@ -270,7 +277,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.mainEditor
]
while new is not None:
while new != None:
if new in targets:
self._lastFocus = new
break
@ -346,6 +353,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# Slider importance
self.updateCharacterImportance(c.ID())
# POV state
self.updateCharacterPOVState(c.ID())
# Character Infos
self.tblPersoInfos.setRootIndex(index)
@ -366,6 +376,22 @@ class MainWindow(QMainWindow, Ui_MainWindow):
c = self.mdlCharacter.getCharacterByID(ID)
self.sldPersoImportance.setValue(int(c.importance()))
def updateCharacterPOVState(self, ID):
c = self.mdlCharacter.getCharacterByID(ID)
self.disconnectAll(self.chkPersoPOV.stateChanged, self.lstCharacters.changeCharacterPOVState)
if c.pov():
self.chkPersoPOV.setCheckState(Qt.Checked)
else:
self.chkPersoPOV.setCheckState(Qt.Unchecked)
try:
self.chkPersoPOV.stateChanged.connect(self.lstCharacters.changeCharacterPOVState, F.AUC)
self.chkPersoPOV.setEnabled(len(self.mdlOutline.findItemsByPOV(ID)) == 0)
except TypeError:
#don't know what's up with this
pass
###############################################################################
# PLOTS
###############################################################################
@ -480,6 +506,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
def documentsPaste(self):
"Paste clipboard item(s) into selected item."
if self._lastFocus: self._lastFocus.paste()
def doSearch(self):
"Do a global search."
self.dckSearch.show()
self.dckSearch.activateWindow()
searchTextInput = self.dckSearch.findChild(QLineEdit, 'searchTextInput')
searchTextInput.setFocus()
searchTextInput.selectAll()
def documentsRename(self):
"Rename selected item."
if self._lastFocus: self._lastFocus.rename()
@ -557,14 +590,14 @@ class MainWindow(QMainWindow, Ui_MainWindow):
If ``loadFromFile`` is False, then it does not load datas from file.
It assumes that the datas have been populated in a different way."""
if loadFromFile and not os.path.exists(project):
print(self.tr("The file {} does not exist. Has it been moved or deleted?").format(project))
LOGGER.warning("The file {} does not exist. Has it been moved or deleted?".format(project))
F.statusMessage(
self.tr("The file {} does not exist. Has it been moved or deleted?").format(project), importance=3)
return
if loadFromFile:
# Load empty settings
imp.reload(settings)
importlib.reload(settings)
settings.initDefaultValues()
# Load data
@ -612,7 +645,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.mdlCharacter.dataChanged.connect(self.startTimerNoChanges)
self.mdlPlots.dataChanged.connect(self.startTimerNoChanges)
self.mdlWorld.dataChanged.connect(self.startTimerNoChanges)
# self.mdlPersosInfos.dataChanged.connect(self.startTimerNoChanges)
self.mdlStatus.dataChanged.connect(self.startTimerNoChanges)
self.mdlLabels.dataChanged.connect(self.startTimerNoChanges)
@ -638,9 +670,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# Add project name to Window's name
self.setWindowTitle(self.projectName() + " - " + self.tr("Manuskript"))
# Stuff
# self.checkPersosID() # Shouldn't be necessary any longer
# Show main Window
self.switchToProject()
@ -768,6 +797,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# Remembering the current items (stores outlineItem's ID)
settings.openIndexes = self.mainEditor.tabSplitter.openIndexes()
# Call close on the main window to clean children widgets
if self.mainEditor:
self.mainEditor.close()
# Save data from models
if settings.saveOnQuit:
self.saveDatas()
@ -823,11 +856,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# risk a scenario where the timer somehow triggers a new save while saving.
self.saveTimerNoChanges.stop()
if self.currentProject is None:
if self.currentProject == None:
# No UI feedback here as this code path indicates a race condition that happens
# after the user has already closed the project through some way. But in that
# scenario, this code should not be reachable to begin with.
print("Bug: there is no current project to save.")
LOGGER.error("There is no current project to save.")
return
r = loadSave.saveProject() # version=0
@ -838,18 +871,15 @@ class MainWindow(QMainWindow, Ui_MainWindow):
feedback = self.tr("Project {} saved.").format(projectName)
F.statusMessage(feedback, importance=0)
LOGGER.info("Project {} saved.".format(projectName))
else:
feedback = self.tr("WARNING: Project {} not saved.").format(projectName)
F.statusMessage(feedback, importance=3)
# Giving some feedback in console
print(feedback)
LOGGER.warning("Project {} not saved.".format(projectName))
def loadEmptyDatas(self):
self.mdlFlatData = QStandardItemModel(self)
self.mdlCharacter = characterModel(self)
# self.mdlPersosProxy = persosProxyModel(self)
# self.mdlPersosInfos = QStandardItemModel(self)
self.mdlLabels = QStandardItemModel(self)
self.mdlStatus = QStandardItemModel(self)
self.mdlPlots = plotModel(self)
@ -862,13 +892,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# Giving some feedback
if not errors:
print(self.tr("Project {} loaded.").format(project))
LOGGER.info("Project {} loaded.".format(project))
F.statusMessage(
self.tr("Project {} loaded.").format(project), 2000)
else:
print(self.tr("Project {} loaded with some errors:").format(project))
LOGGER.error("Project {} loaded with some errors:".format(project))
for e in errors:
print(self.tr(" * {} wasn't found in project file.").format(e))
LOGGER.error(" * {} wasn't found in project file.".format(e))
F.statusMessage(
self.tr("Project {} loaded with some errors.").format(project), 5000, importance = 3)
@ -933,11 +963,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# Characters
self.lstCharacters.setCharactersModel(self.mdlCharacter)
self.tblPersoInfos.setModel(self.mdlCharacter)
self.btnAddPerso.clicked.connect(self.mdlCharacter.addCharacter, F.AUC)
try:
self.btnAddPerso.clicked.connect(self.lstCharacters.addCharacter, F.AUC)
self.btnRmPerso.clicked.connect(self.lstCharacters.removeCharacter, F.AUC)
self.btnPersoColor.clicked.connect(self.lstCharacters.choseCharacterColor, F.AUC)
self.chkPersoPOV.stateChanged.connect(self.lstCharacters.changeCharacterPOVState, F.AUC)
self.btnPersoAddInfo.clicked.connect(self.lstCharacters.addCharacterInfo, F.AUC)
self.btnPersoRmInfo.clicked.connect(self.lstCharacters.removeCharacterInfo, F.AUC)
except TypeError:
@ -1078,7 +1110,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# disconnect only removes one connection at a time.
while True:
try:
if oldHandler is not None:
if oldHandler != None:
signal.disconnect(oldHandler)
else:
signal.disconnect()
@ -1089,9 +1121,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# Break connections for UI elements that were connected in makeConnections()
# Characters
self.disconnectAll(self.btnAddPerso.clicked, self.mdlCharacter.addCharacter)
self.disconnectAll(self.btnAddPerso.clicked, self.lstCharacters.addCharacter)
self.disconnectAll(self.btnRmPerso.clicked, self.lstCharacters.removeCharacter)
self.disconnectAll(self.btnPersoColor.clicked, self.lstCharacters.choseCharacterColor)
self.disconnectAll(self.chkPersoPOV.stateChanged, self.lstCharacters.changeCharacterPOVState)
self.disconnectAll(self.btnPersoAddInfo.clicked, self.lstCharacters.addCharacterInfo)
self.disconnectAll(self.btnPersoRmInfo.clicked, self.lstCharacters.removeCharacterInfo)
@ -1147,6 +1182,50 @@ class MainWindow(QMainWindow, Ui_MainWindow):
r2 = self.geometry()
win.move(r2.center() - QPoint(r.width()/2, r.height()/2))
def support(self):
openURL("https://github.com/olivierkes/manuskript/wiki/Technical-Support")
def locateLogFile(self):
logfile = getLogFilePath()
# Make sure we are even logging to a file.
if not logfile:
QMessageBox(QMessageBox.Information,
self.tr("Sorry!"),
"<p><b>" +
self.tr("This session is not being logged.") +
"</b></p>",
QMessageBox.Ok).exec()
return
# Remind user that log files are at their best once they are complete.
msg = QMessageBox(QMessageBox.Information,
self.tr("A log file is a Work in Progress!"),
"<p><b>" +
self.tr("The log file \"{}\" will continue to be written to until Manuskript is closed.").format(os.path.basename(logfile)) +
"</b></p>" +
"<p>" +
self.tr("It will now be displayed in your file manager, but is of limited use until you close Manuskript.") +
"</p>",
QMessageBox.Ok)
ret = msg.exec()
# Open the filemanager.
if ret == QMessageBox.Ok:
if not showInFolder(logfile):
# If everything convenient fails, at least make sure the user can browse to its location manually.
QMessageBox(QMessageBox.Critical,
self.tr("Error!"),
"<p><b>" +
self.tr("An error was encountered while trying to show the log file below in your file manager.") +
"</b></p>" +
"<p>" +
logfile +
"</p>",
QMessageBox.Ok).exec()
def about(self):
self.dialog = aboutDialog(mw=self)
self.dialog.setFixedSize(self.dialog.size())
@ -1355,7 +1434,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
dictionaries = Spellchecker.availableDictionaries()
# Set first run dictionary
if settings.dict is None:
if settings.dict == None:
settings.dict = Spellchecker.getDefaultDictionary()
# Check if project dict is unavailable on this machine
@ -1499,7 +1578,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.menuView.addMenu(self.menuMode)
self.menuView.addSeparator()
# print("Generating menus with", settings.viewSettings)
# LOGGER.debug("Generating menus with %s.", settings.viewSettings)
for mnu, mnud, icon in menus:
m = QMenu(mnu, self.menuView)
@ -1566,7 +1645,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
w.cmbPOV.setVisible(val)
# POV in outline view
if val is None and Outline.POV in settings.outlineViewColumns:
if val == None and Outline.POV in settings.outlineViewColumns:
settings.outlineViewColumns.remove(Outline.POV)
from manuskript.ui.views.outlineView import outlineView
@ -1593,7 +1672,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
warning2 = self.tr("PyQt {} and Qt {} are in use.").format(qVersion(), PYQT_VERSION_STR)
# Don't translate for debug log.
print("WARNING:", warning1, warning2)
LOGGER.warning(warning1)
LOGGER.warning(warning2)
msg = QMessageBox(QMessageBox.Warning,
self.tr("Proceed with import at your own risk"),

View file

@ -13,6 +13,8 @@ import re
from manuskript import enums
import logging
LOGGER = logging.getLogger(__name__)
class abstractItem():
@ -39,14 +41,17 @@ class abstractItem():
self._data[self.enum.title] = title
self._data[self.enum.type] = _type
if xml is not None:
if xml != None:
self.setFromXML(xml)
if parent:
# add this as a child to the parent, and link to the outlineModel of the parent
parent.appendChild(self)
if ID:
self._data[self.enum.ID] = ID
if parent:
parent.appendChild(self)
#######################################################################
# Model
@ -54,6 +59,11 @@ class abstractItem():
def setModel(self, model):
self._model = model
if not self.ID():
self.getUniqueID()
elif model:
# if we are setting a model update it's ID
self._model.updateAvailableIDs(self.ID())
for c in self.children():
c.setModel(model)
@ -135,8 +145,6 @@ class abstractItem():
self.childItems.insert(row, child)
child._parent = self
child.setModel(self._model)
if not child.ID():
child.getUniqueID()
def removeChild(self, row):
"""
@ -195,7 +203,7 @@ class abstractItem():
###############################################################################
def getUniqueID(self, recursive=False):
self.setData(self.enum.ID, self._model.rootItem.findUniqueID())
self.setData(self.enum.ID, self._model.requestNewID())
if recursive:
for c in self.children():
@ -209,7 +217,7 @@ class abstractItem():
self.IDs = self.listAllIDs()
if max([self.IDs.count(i) for i in self.IDs if i]) != 1:
print("WARNING ! There are some items with same IDs:", [i for i in self.IDs if i and self.IDs.count(i) != 1])
LOGGER.warning("There are some items with overlapping IDs: %s", [i for i in self.IDs if i and self.IDs.count(i) != 1])
def checkChildren(item):
for c in item.children():
@ -226,14 +234,6 @@ class abstractItem():
IDs.extend(c.listAllIDs())
return IDs
def findUniqueID(self):
IDs = [int(i) for i in self.IDs]
k = 1
while k in IDs:
k += 1
self.IDs.append(str(k))
return str(k)
#######################################################################
# Data
#######################################################################
@ -250,6 +250,10 @@ class abstractItem():
# Setting data
self._data[column] = data
# The _model will be none during splitting
if self._model and column == self.enum.ID:
self._model.updateAvailableIDs(data)
# Emit signal
self.emitDataChanged(cols=[column]) # new in 0.5.0

View file

@ -26,6 +26,8 @@ except:
pass
import time, os
import logging
LOGGER = logging.getLogger(__name__)
class abstractModel(QAbstractItemModel):
"""
@ -36,17 +38,29 @@ class abstractModel(QAbstractItemModel):
- Interface with QModelIndex and stuff
- XML Import / Export
- Drag'n'drop
Row => item/abstractModel/etc.
Col => data sub-element. Col 1 (second counting) is ID for all model types.
"""
def __init__(self, parent):
QAbstractItemModel.__init__(self, parent)
self.rootItem = outlineItem(self, title="Root", ID="0")
self.nextAvailableID = 1
# Stores removed item, in order to remove them on disk when saving, depending on the file format.
self.removed = []
self._removingRows = False
def requestNewID(self):
newID = self.nextAvailableID
self.nextAvailableID += 1
return str(newID)
# Call this if loading an ID from file rather than assigning a new one.
def updateAvailableIDs(self, addedID):
if int(addedID) >= self.nextAvailableID:
self.nextAvailableID = int(addedID) + 1
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
@ -74,7 +88,7 @@ class abstractModel(QAbstractItemModel):
if len(parent.children()) == 0:
return None
# print(item.title(), [i.title() for i in parent.children()])
#LOGGER.debug("%s: %s", item.title(), [i.title() for i in parent.children()])
row = parent.children().index(item)
col = column
@ -131,7 +145,7 @@ class abstractModel(QAbstractItemModel):
# Check whether the parent is the root, or is otherwise invalid.
# That is to say: no parent or the parent lacks a parent.
if (parentItem == self.rootItem) or \
(parentItem is None) or (parentItem.parent() is None):
(parentItem == None) or (parentItem.parent() == None):
return QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
@ -168,7 +182,7 @@ class abstractModel(QAbstractItemModel):
# self.dataChanged.emit(index.sibling(index.row(), 0),
# index.sibling(index.row(), max([i.value for i in Outline])))
# print("Model emit", index.row(), index.column())
# LOGGER.debug("Model dataChanged emit: %s, %s", index.row(), index.column())
self.dataChanged.emit(index, index)
if index.column() == Outline.type:
@ -283,7 +297,7 @@ class abstractModel(QAbstractItemModel):
# # Gets encoded mime data to retrieve the item
items = self.decodeMimeData(data)
if items is None:
if items == None:
return False
# We check if parent is not a child of one of the items
@ -321,7 +335,7 @@ class abstractModel(QAbstractItemModel):
return None
encodedData = bytes(data.data("application/xml")).decode()
root = ET.XML(encodedData)
if root is None:
if root == None:
return None
if root.tag != "outlineItems":
@ -381,7 +395,7 @@ class abstractModel(QAbstractItemModel):
items = self.decodeMimeData(data)
if items is None:
if items == None:
return False
if column > 0:
@ -415,21 +429,19 @@ class abstractModel(QAbstractItemModel):
# In case of copy actions, items might be duplicates, so we need new IDs.
# But they might not be, if we cut, then paste. Paste is a Copy Action.
# The first paste would not need new IDs. But subsequent ones will.
# Recursively change the existing IDs to new, unique values. No need to strip out the old
# even if they are not duplicated in pasting. There is no practical need for ID conservation.
if action == Qt.CopyAction:
IDs = self.rootItem.listAllIDs()
for item in items:
if item.ID() in IDs:
# Recursively remove ID. So will get a new one when inserted.
def stripID(item):
item.setData(Outline.ID, None)
for c in item.children():
stripID(c)
stripID(item)
item.getUniqueID(recursive=true)
r = self.insertItems(items, beginRow, parent)
return r
################# ADDING AND REMOVING #################
@ -448,13 +460,13 @@ class abstractModel(QAbstractItemModel):
# Insert only if parent is folder
if parentItem.isFolder():
self.beginInsertRows(parent, row, row + len(items) - 1)
self.beginInsertRows(parent, row, row + len(items) - 1) # Create space.
for i in items:
parentItem.insertChild(row + items.index(i), i)
self.endInsertRows()
return True
else:
@ -507,8 +519,9 @@ class abstractModel(QAbstractItemModel):
else:
parentItem = parent.internalPointer()
self._removingRows = True # Views that are updating can easily know
# if this is due to row removal.
self._removingRows = True
# Views that are updating can easily know
# if this is due to row removal.
self.beginRemoveRows(parent, row, row + count - 1)
for i in range(count):
item = parentItem.removeChild(row)

View file

@ -3,11 +3,14 @@
from PyQt5.QtCore import QModelIndex, Qt, QAbstractItemModel, QVariant
from PyQt5.QtGui import QIcon, QPixmap, QColor
from manuskript.functions import randomColor, iconColor, mainWindow
from manuskript.enums import Character as C
from manuskript.functions import randomColor, iconColor, mainWindow, search
from manuskript.enums import Character as C, Model
from manuskript.searchLabels import CharacterSearchLabels
from manuskript.models.searchableModel import searchableModel
from manuskript.models.searchableItem import searchableItem
class characterModel(QAbstractItemModel):
class characterModel(QAbstractItemModel, searchableModel):
def __init__(self, parent):
QAbstractItemModel.__init__(self, parent)
@ -132,6 +135,9 @@ class characterModel(QAbstractItemModel):
def importance(self, row):
return self.character(row).importance()
def pov(self, row):
return self.character(row).pov()
###############################################################################
# MODEL QUERIES
###############################################################################
@ -143,29 +149,36 @@ class characterModel(QAbstractItemModel):
@return: array of array of ´character´, by importance.
"""
r = [[], [], []]
for c in self.characters:
r[2-int(c.importance())].append(c)
return r
def getCharacterByID(self, ID):
if ID is not None:
if ID != None:
ID = str(ID)
for c in self.characters:
if c.ID() == ID:
return c
return None
###############################################################################
# ADDING / REMOVING
###############################################################################
def addCharacter(self):
def addCharacter(self, importance = 0, name="New character"):
"""
Creates a new character
@param importance: the importance level of the character
@return: the character
"""
c = Character(model=self, name=self.tr("New character"))
self.beginInsertRows(QModelIndex(), len(self.characters), len(self.characters))
if not name:
name="New Character"
c = Character(model=self, name=self.tr(name), importance = importance)
self.beginInsertRows(QModelIndex(), len(
self.characters), len(self.characters))
self.characters.append(c)
self.endInsertRows()
return c
@ -177,7 +190,8 @@ class characterModel(QAbstractItemModel):
@return: nothing
"""
c = self.getCharacterByID(ID)
self.beginRemoveRows(QModelIndex(), self.characters.index(c), self.characters.index(c))
self.beginRemoveRows(QModelIndex(), self.characters.index(
c), self.characters.index(c))
self.characters.remove(c)
self.endRemoveRows()
@ -197,7 +211,8 @@ class characterModel(QAbstractItemModel):
def addCharacterInfo(self, ID):
c = self.getCharacterByID(ID)
self.beginInsertRows(c.index(), len(c.infos), len(c.infos))
c.infos.append(CharacterInfo(c, description="Description", value="Value"))
c.infos.append(CharacterInfo(
c, description="Description", value="Value"))
self.endInsertRows()
mainWindow().updatePersoInfoView()
@ -217,12 +232,15 @@ class characterModel(QAbstractItemModel):
c.infos.pop(r)
self.endRemoveRows()
def searchableItems(self):
return self.characters
###############################################################################
# CHARACTER
###############################################################################
class Character():
def __init__(self, model, name="No name"):
class Character(searchableItem):
def __init__(self, model, name="No name", importance = 0):
self._model = model
self.lastPath = ""
@ -230,13 +248,19 @@ class Character():
self._data[C.name.value] = name
self.assignUniqueID()
self.assignRandomColor()
self._data[C.importance.value] = "0"
self._data[C.importance.value] = str(importance)
self._data[C.pov.value] = "True"
self.infos = []
super().__init__(CharacterSearchLabels)
def name(self):
return self._data[C.name.value]
def setName(self, value):
self._data[C.name.value] = value
def importance(self):
return self._data[C.importance.value]
@ -246,6 +270,12 @@ class Character():
def index(self, column=0):
return self._model.indexFromItem(self, column)
def data(self, column):
if column == "Info":
return self.infos
else:
return self._data.get(column, None)
def assignRandomColor(self):
"""
Assigns a random color the the character.
@ -274,6 +304,22 @@ class Character():
"""
return iconColor(self.icon)
def setPOVEnabled(self, enabled):
if enabled != self.pov():
if enabled:
self._data[C.pov.value] = 'True'
else:
self._data[C.pov.value] = 'False'
try:
self._model.dataChanged.emit(self.index(), self.index())
except:
# If it is the initialisation, won't be able to emit
pass
def pov(self):
return self._data[C.pov.value] == 'True'
def assignUniqueID(self, parent=QModelIndex()):
"""Assigns an unused character ID."""
vals = []
@ -292,6 +338,42 @@ class Character():
r.append((i.description, i.value))
return r
def searchTitle(self, column):
return self.name()
def searchOccurrences(self, searchRegex, column):
results = []
data = self.searchData(column)
if isinstance(data, list):
for i in range(0, len(data)):
# For detailed info we will highlight the full row, so we pass the row index
# to the highlighter instead of the (startPos, endPos) of the match itself.
results += [self.wrapSearchOccurrence(column, i, 0, context) for
(startPos, endPos, context) in search(searchRegex, data[i].description)]
results += [self.wrapSearchOccurrence(column, i, 0, context) for
(startPos, endPos, context) in search(searchRegex, data[i].value)]
else:
results += super().searchOccurrences(searchRegex, column)
return results
def searchID(self):
return self.ID()
def searchPath(self, column):
return [self.translate("Characters"), self.name(), self.translate(self.searchColumnLabel(column))]
def searchData(self, column):
if column == C.infos:
return self.infos
else:
return self.data(column)
def searchModel(self):
return Model.Character
class CharacterInfo():
def __init__(self, character, description="", value=""):
self.description = description

View file

@ -0,0 +1,47 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from PyQt5.QtCore import QModelIndex, QSortFilterProxyModel
class characterPOVModel(QSortFilterProxyModel):
def __init__(self, sourceModel, parent=None):
QSortFilterProxyModel.__init__(self, parent)
self.setSourceModel(sourceModel)
if sourceModel:
sourceModel.dataChanged.connect(self.sourceDataChanged)
def filterAcceptsRow(self, sourceRow, sourceParent):
return self.sourceModel().pov(sourceRow)
def rowToSource(self, row):
index = self.index(row, 0)
sourceIndex = self.mapToSource(index)
return sourceIndex.row()
def sourceDataChanged(self, topLeft, bottomRight):
self.invalidateFilter()
###############################################################################
# CHARACTER QUERIES
###############################################################################
def character(self, row):
return self.sourceModel().character(self.rowToSource(row))
def name(self, row):
return self.sourceModel().name(self.rowToSource(row))
def icon(self, row):
return self.sourceModel().icon(self.rowToSource(row))
def ID(self, row):
return self.sourceModel().ID(self.rowToSource(row))
def importance(self, row):
return self.sourceModel().importance(self.rowToSource(row))
def pov(self, row):
return self.sourceModel().pov(self.rowToSource(row))

View file

@ -0,0 +1,53 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from manuskript.enums import FlatData, Model
from manuskript.searchLabels import FlatDataSearchLabels
from manuskript.models.searchableModel import searchableModel
from manuskript.models.searchableItem import searchableItem
"""
All searches are performed on models inheriting from searchableModel, but special metadata such as book summaries
are stored directly on a GUI element (QStandardItemModel). We wrap this GUI element inside this wrapper class
so it exposes the same interface for searches.
"""
class flatDataModelWrapper(searchableModel, searchableItem):
def __init__(self, qstandardItemModel):
self.qstandardItemModel = qstandardItemModel
def searchableItems(self):
return [flatDataItemWrapper(self.qstandardItemModel)]
class flatDataItemWrapper(searchableItem):
def __init__(self, qstandardItemModel):
super().__init__(FlatDataSearchLabels)
self.qstandardItemModel = qstandardItemModel
def searchModel(self):
return Model.FlatData
def searchID(self):
return None
def searchTitle(self, column):
return self.translate(self.searchColumnLabel(column))
def searchPath(self, column):
return [self.translate("Summary"), self.translate(self.searchColumnLabel(column))]
def searchData(self, column):
return self.qstandardItemModel.item(1, self.searchDataIndex(column)).text()
@staticmethod
def searchDataIndex(column):
columnIndices = {
FlatData.summarySituation: 0,
FlatData.summarySentence: 1,
FlatData.summaryPara: 2,
FlatData.summaryPage: 3,
FlatData.summaryFull: 4
}
return columnIndices[column]

View file

@ -8,10 +8,13 @@ from PyQt5.QtGui import QFont, QIcon
from PyQt5.QtWidgets import qApp
from lxml import etree as ET
from manuskript.models.abstractItem import abstractItem
from manuskript.models.searchableItem import searchableItem
from manuskript import enums
from manuskript import functions as F
from manuskript import settings
from manuskript.converters import HTML2PlainText
from manuskript.searchLabels import OutlineSearchLabels
from manuskript.enums import Outline, Model
try:
locale.setlocale(locale.LC_ALL, '')
@ -20,8 +23,10 @@ except:
# number formatting
pass
import logging
LOGGER = logging.getLogger(__name__)
class outlineItem(abstractItem):
class outlineItem(abstractItem, searchableItem):
enum = enums.Outline
@ -30,6 +35,7 @@ class outlineItem(abstractItem):
def __init__(self, model=None, title="", _type="folder", xml=None, parent=None, ID=None):
abstractItem.__init__(self, model, title, _type, xml, parent, ID)
searchableItem.__init__(self, OutlineSearchLabels)
self.defaultTextType = None
if not self._data.get(self.enum.compile):
@ -80,6 +86,9 @@ class outlineItem(abstractItem):
def wordCount(self):
return self._data.get(self.enum.wordCount, 0)
def charCount(self):
return self._data.get(self.enum.charCount, 0)
def __str__(self):
return "{id}: {folder}{title}{children}".format(
id=self.ID(),
@ -89,6 +98,9 @@ class outlineItem(abstractItem):
)
__repr__ = __str__
def charCount(self):
return self._data.get(self.enum.charCount, 0)
#######################################################################
# Data
@ -119,7 +131,7 @@ class outlineItem(abstractItem):
elif role == Qt.FontRole:
f = QFont()
if column == E.wordCount and self.isFolder():
if (column == E.wordCount or column == E.charCount) and self.isFolder():
f.setItalic(True)
elif column == E.goal and self.isFolder() and not self.data(E.setGoal):
f.setItalic(True)
@ -140,7 +152,7 @@ class outlineItem(abstractItem):
# Checking if we will have to recount words
updateWordCount = False
if column in [E.wordCount, E.goal, E.setGoal]:
if column in [E.wordCount, E.charCount, E.goal, E.setGoal]:
updateWordCount = not column in self._data or self._data[column] != data
# Stuff to do before
@ -153,7 +165,9 @@ class outlineItem(abstractItem):
# Stuff to do afterwards
if column == E.text:
wc = F.wordCount(data)
cc = F.charCount(data, settings.countSpaces)
self.setData(E.wordCount, wc)
self.setData(E.charCount, cc)
if column == E.compile:
# Title changes when compile changes
@ -195,9 +209,12 @@ class outlineItem(abstractItem):
else:
wc = 0
cc = 0
for c in self.children():
wc += F.toInt(c.data(self.enum.wordCount))
cc += F.toInt(c.data(self.enum.charCount))
self._data[self.enum.wordCount] = wc
self._data[self.enum.charCount] = cc
setGoal = F.toInt(self.data(self.enum.setGoal))
goal = F.toInt(self.data(self.enum.goal))
@ -218,7 +235,8 @@ class outlineItem(abstractItem):
self.setData(self.enum.goalPercentage, "")
self.emitDataChanged([self.enum.goal, self.enum.setGoal,
self.enum.wordCount, self.enum.goalPercentage])
self.enum.wordCount, self.enum.charCount,
self.enum.goalPercentage])
if self.parent():
self.parent().updateWordCount()
@ -343,8 +361,7 @@ class outlineItem(abstractItem):
return lst
def findItemsContaining(self, text, columns, mainWindow=F.mainWindow(),
caseSensitive=False, recursive=True):
def findItemsContaining(self, text, columns, mainWindow=F.mainWindow(), caseSensitive=False, recursive=True):
"""Returns a list if IDs of all subitems
containing ``text`` in columns ``columns``
(being a list of int).
@ -357,19 +374,17 @@ class outlineItem(abstractItem):
return lst
def itemContains(self, text, columns, mainWindow=F.mainWindow(),
caseSensitive=False):
def itemContains(self, text, columns, mainWindow=F.mainWindow(), caseSensitive=False):
lst = []
text = text.lower() if not caseSensitive else text
for c in columns:
if c == self.enum.POV and self.POV():
c = mainWindow.mdlCharacter.getCharacterByID(self.POV())
if c:
searchIn = c.name()
character = mainWindow.mdlCharacter.getCharacterByID(self.POV())
if character:
searchIn = character.name()
else:
searchIn = ""
print("Character POV not found:", self.POV())
LOGGER.error("Character POV not found: %s", self.POV())
elif c == self.enum.status:
searchIn = mainWindow.mdlStatus.item(F.toInt(self.status()), 0).text()
@ -381,7 +396,6 @@ class outlineItem(abstractItem):
searchIn = self.data(c)
searchIn = searchIn.lower() if not caseSensitive else searchIn
if text in searchIn:
if not self.ID() in lst:
lst.append(self.ID())
@ -467,6 +481,7 @@ class outlineItem(abstractItem):
# We don't want to write some datas (computed)
XMLExclude = [enums.Outline.wordCount,
enums.Outline.charCount,
enums.Outline.goal,
enums.Outline.goalPercentage,
enums.Outline.revisions]
@ -502,3 +517,39 @@ class outlineItem(abstractItem):
for child in root:
if child.tag == "revision":
self.appendRevision(child.attrib["timestamp"], child.attrib["text"])
#######################################################################
# Search
#######################################################################
def searchModel(self):
return Model.Outline
def searchID(self):
return self.data(Outline.ID)
def searchTitle(self, column):
return self.title()
def searchPath(self, column):
return [self.translate("Outline")] + self.path().split(' > ') + [self.translate(self.searchColumnLabel(column))]
def searchData(self, column):
mainWindow = F.mainWindow()
searchData = None
if column == self.enum.POV and self.POV():
character = mainWindow.mdlCharacter.getCharacterByID(self.POV())
if character:
searchData = character.name()
elif column == self.enum.status:
searchData = mainWindow.mdlStatus.item(F.toInt(self.status()), 0).text()
elif column == self.enum.label:
searchData = mainWindow.mdlLabels.item(F.toInt(self.label()), 0).text()
else:
searchData = self.data(column)
return searchData

View file

@ -2,12 +2,31 @@
# --!-- coding: utf8 --!--
from manuskript.models.abstractModel import abstractModel
from manuskript.models.searchableModel import searchableModel
from manuskript.models.outlineItem import outlineItem
class outlineModel(abstractModel):
class outlineModel(abstractModel, searchableModel):
def __init__(self, parent):
abstractModel.__init__(self, parent)
self.rootItem = outlineItem(model=self, title="Root", ID="0")
def findItemsByPOV(self, POV):
"Returns a list of IDs of all items whose POV is ``POV``."
return self.rootItem.findItemsByPOV(POV)
def searchableItems(self):
result = []
for child in self.rootItem.children():
result += self._searchableItems(child)
return result
def _searchableItems(self, item):
result = [item]
for child in item.children():
result += self._searchableItems(child)
return result

View file

@ -8,12 +8,15 @@ from PyQt5.QtGui import QStandardItem
from PyQt5.QtGui import QStandardItemModel
from PyQt5.QtWidgets import QAction, QMenu
from manuskript.enums import Plot
from manuskript.enums import PlotStep
from manuskript.enums import Plot, PlotStep, Model
from manuskript.functions import toInt, mainWindow
from manuskript.models.searchResultModel import searchResultModel
from manuskript.searchLabels import PlotSearchLabels, PLOT_STEP_COLUMNS_OFFSET
from manuskript.functions import search
from manuskript.models.searchableModel import searchableModel
from manuskript.models.searchableItem import searchableItem
class plotModel(QStandardItemModel):
class plotModel(QStandardItemModel, searchableModel):
def __init__(self, parent):
QStandardItemModel.__init__(self, 0, 3, parent)
self.setHorizontalHeaderLabels([i.name for i in Plot])
@ -73,7 +76,7 @@ class plotModel(QStandardItemModel):
if i == row:
importance = self.item(i, Plot.importance).text()
return importance
return "0" # Default to "Minor"
return "0" # Default to "Minor"
def getSubPlotTextsByID(self, plotID, subplotRaw):
"""Returns a tuple (name, summary) for the subplot whose raw in the model
@ -102,12 +105,15 @@ class plotModel(QStandardItemModel):
# ADDING / REMOVING
###############################################################################
def addPlot(self):
p = QStandardItem(self.tr("New plot"))
def addPlot(self, name="New plot"):
if not name:
name="New Plot"
p = QStandardItem(self.tr(name))
_id = QStandardItem(self.getUniqueID())
importance = QStandardItem(str(0))
self.appendRow([p, _id, importance, QStandardItem("Characters"),
QStandardItem(), QStandardItem(), QStandardItem("Resolution steps")])
return p, _id
def getUniqueID(self, parent=QModelIndex()):
"""Returns an unused ID"""
@ -147,8 +153,8 @@ class plotModel(QStandardItemModel):
def data(self, index, role=Qt.DisplayRole):
if index.parent().isValid() and \
index.parent().column() == Plot.steps and \
index.column() == PlotStep.meta:
index.parent().column() == Plot.steps and \
index.column() == PlotStep.meta:
if role == Qt.TextAlignmentRole:
return Qt.AlignRight | Qt.AlignVCenter
elif role == Qt.ForegroundRole:
@ -186,7 +192,8 @@ class plotModel(QStandardItemModel):
# Don't know why, if summary is in third position, then drag/drop deletes it...
parentItem.appendRow([p, _id, QStandardItem(), summary])
# Select last index
self.mw.lstSubPlots.setCurrentIndex(parent.child(self.rowCount(parent) - 1, 0))
self.mw.lstSubPlots.setCurrentIndex(
parent.child(self.rowCount(parent) - 1, 0))
def removeSubPlot(self):
"""
@ -262,3 +269,118 @@ class plotModel(QStandardItemModel):
mpr.mapped.connect(self.addPlotPerso)
self.mw.btnAddPlotPerso.setMenu(menu)
#######################################################################
# Search
#######################################################################
def searchableItems(self):
items = []
for i in range(self.rowCount()):
items.append(plotItemSearchWrapper(i, self.item, self.mw.mdlCharacter.getCharacterByID))
return items
class plotItemSearchWrapper(searchableItem):
def __init__(self, rowIndex, getItem, getCharacterByID):
self.rowIndex = rowIndex
self.getItem = getItem
self.getCharacterByID = getCharacterByID
super().__init__(PlotSearchLabels)
def searchOccurrences(self, searchRegex, column):
results = []
plotName = self.getItem(self.rowIndex, Plot.name).text()
if column >= PLOT_STEP_COLUMNS_OFFSET:
results += self.searchInPlotSteps(self.rowIndex, plotName, column, column - PLOT_STEP_COLUMNS_OFFSET, searchRegex, False)
else:
item_name = self.getItem(self.rowIndex, Plot.name).text()
if column == Plot.characters:
charactersList = self.getItem(self.rowIndex, Plot.characters)
for i in range(charactersList.rowCount()):
characterID = charactersList.child(i).text()
character = self.getCharacterByID(characterID)
if character:
columnText = character.name()
characterResults = search(searchRegex, columnText)
if len(characterResults):
# We will highlight the full character row in the plot characters list, so we
# return the row index instead of the match start and end positions.
results += [
searchResultModel(Model.Plot, self.getItem(self.rowIndex, Plot.ID).text(), column,
self.translate(item_name),
self.searchPath(column),
[(i, 0)], context) for start, end, context in
search(searchRegex, columnText)]
else:
results += super().searchOccurrences(searchRegex, column)
if column == Plot.name:
results += self.searchInPlotSteps(self.rowIndex, plotName, Plot.name, PlotStep.name,
searchRegex, False)
elif column == Plot.summary:
results += self.searchInPlotSteps(self.rowIndex, plotName, Plot.summary, PlotStep.summary,
searchRegex, True)
return results
def searchModel(self):
return Model.Plot
def searchID(self):
return self.getItem(self.rowIndex, Plot.ID).text()
def searchTitle(self, column):
return self.getItem(self.rowIndex, Plot.name).text()
def searchPath(self, column):
def _path(item):
path = []
if item.parent():
path += _path(item.parent())
path.append(item.text())
return path
return [self.translate("Plot")] + _path(self.getItem(self.rowIndex, Plot.name)) + [self.translate(self.searchColumnLabel(column))]
def searchData(self, column):
return self.getItem(self.rowIndex, column).text()
def plotStepPath(self, plotName, plotStepName, column):
return [self.translate("Plot"), plotName, plotStepName, self.translate(self.searchColumnLabel(column))]
def searchInPlotSteps(self, plotIndex, plotName, plotColumn, plotStepColumn, searchRegex, searchInsidePlotStep):
results = []
# Plot step info can be found in two places: the own list of plot steps (this is the case for ie. name and meta
# fields) and "inside" the plot step once it is selected in the list (as it's the case for the summary).
if searchInsidePlotStep:
# We are searching *inside* the plot step, so we return both the row index (for selecting the right plot
# step in the list), and (start, end) positions of the match inside the text field for highlighting it.
getSearchData = lambda rowIndex, start, end, context: ([(rowIndex, 0), (start, end)], context)
else:
# We are searching *in the plot step row*, so we only return the row index for selecting the right plot
# step in the list when highlighting search results.
getSearchData = lambda rowIndex, start, end, context: ([(rowIndex, 0)], context)
item = self.getItem(plotIndex, Plot.steps)
for i in range(item.rowCount()):
if item.child(i, PlotStep.ID):
plotStepName = item.child(i, PlotStep.name).text()
plotStepText = item.child(i, plotStepColumn).text()
# We will highlight the full plot step row in the plot steps list, so we
# return the row index instead of the match start and end positions.
results += [searchResultModel(Model.PlotStep, self.getItem(plotIndex, Plot.ID).text(), plotStepColumn,
self.translate(plotStepName),
self.plotStepPath(plotName, plotStepName, plotColumn),
*getSearchData(i, start, end, context)) for start, end, context in
search(searchRegex, plotStepText)]
return results

View file

@ -3,6 +3,9 @@
import re
import logging
LOGGER = logging.getLogger(__name__)
###############################################################################
# SHORT REFERENCES
###############################################################################
@ -187,7 +190,7 @@ def infos(ref):
elif _type == CharacterLetter:
m = mainWindow().mdlCharacter
c = m.getCharacterByID(int(_ref))
if c is None:
if c == None:
return qApp.translate("references", "Unknown reference: {}.").format(ref)
index = c.index()
@ -627,7 +630,7 @@ def open(ref):
mw.lstCharacters.setCurrentItem(item)
return True
print("Error: Ref {} not found".format(ref))
LOGGER.error("Character reference {} not found.".format(ref))
return False
elif _type == TextLetter:
@ -639,7 +642,7 @@ def open(ref):
mw.mainEditor.setCurrentModelIndex(index, newTab=True)
return True
else:
print("Ref not found")
LOGGER.error("Text reference {} not found.".format(ref))
return False
elif _type == PlotLetter:
@ -651,7 +654,7 @@ def open(ref):
mw.lstPlots.setCurrentItem(item)
return True
print("Ref not found")
LOGGER.error("Plot reference {} not found.".format(ref))
return False
elif _type == WorldLetter:
@ -664,8 +667,8 @@ def open(ref):
mw.mdlWorld.indexFromItem(item))
return True
print("Ref not found")
LOGGER.error("World reference {} not found.".format(ref))
return False
print("Ref not implemented")
LOGGER.error("Unable to identify reference type: {}.".format(ref))
return False

View file

@ -0,0 +1,32 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
class searchFilter:
def __init__(self, label, enabled, modelColumns = None):
if not isinstance(label, str):
raise TypeError("label must be a str")
if not isinstance(enabled, bool):
raise TypeError("enabled must be a bool")
if modelColumns is not None and (not isinstance(modelColumns, list)):
raise TypeError("modelColumns must be a list or None")
self._label = label
self._enabled = enabled
self._modelColumns = modelColumns
if self._modelColumns is None:
self._modelColumns = []
def label(self):
return self._label
def enabled(self):
return self._enabled
def modelColumns(self):
return self._modelColumns
def setEnabled(self, enabled):
self._enabled = enabled

View file

@ -0,0 +1,44 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
class searchResultModel():
def __init__(self, model_type, model_id, column, title, path, pos, context):
self._type = model_type
self._id = model_id
self._column = column
self._title = title
self._path = path
self._pos = pos
self._context = context
def type(self):
return self._type
def id(self):
return self._id
def column(self):
return self._column
def title(self):
return self._title
def path(self):
return self._path
def pos(self):
return self._pos
def context(self):
return self._context
def __repr__(self):
return "(%s, %s, %s, %s, %s, %s, %s)" % (self._type, self._id, self._column, self._title, self._path, self._pos, self._context)
def __eq__(self, other):
return self.type() == other.type() and \
self.id() == other.id() and \
self.column == other.column and \
self.pos() == other.pos() and \
self.context == other.context

View file

@ -0,0 +1,38 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from manuskript.models.searchResultModel import searchResultModel
from manuskript.functions import search
from PyQt5.QtCore import QCoreApplication
class searchableItem():
def __init__(self, searchColumnLabels):
self._searchColumnLabels = searchColumnLabels
def searchOccurrences(self, searchRegex, column):
return [self.wrapSearchOccurrence(column, startPos, endPos, context) for (startPos, endPos, context) in search(searchRegex, self.searchData(column))]
def wrapSearchOccurrence(self, column, startPos, endPos, context):
return searchResultModel(self.searchModel(), self.searchID(), column, self.searchTitle(column), self.searchPath(column), [(startPos, endPos)], context)
def searchModel(self):
raise NotImplementedError
def searchID(self):
raise NotImplementedError
def searchTitle(self, column):
raise NotImplementedError
def searchPath(self, column):
return []
def searchData(self, column):
raise NotImplementedError
def searchColumnLabel(self, column):
return self._searchColumnLabels.get(column, "")
def translate(self, text):
return QCoreApplication.translate("MainWindow", text)

View file

@ -0,0 +1,15 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
class searchableModel():
def searchOccurrences(self, searchRegex, columns):
results = []
for item in self.searchableItems():
for column in columns:
results += item.searchOccurrences(searchRegex, column)
return results
def searchableItems(self):
raise NotImplementedError

View file

@ -1,18 +1,20 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from PyQt5.QtCore import QModelIndex
from PyQt5.QtCore import QSize
from PyQt5.QtCore import QModelIndex, QSize
from PyQt5.QtCore import Qt, QMimeData, QByteArray
from PyQt5.QtGui import QStandardItem, QBrush, QFontMetrics
from PyQt5.QtGui import QStandardItemModel, QColor
from PyQt5.QtWidgets import QMenu, QAction, qApp
from manuskript.enums import World
from manuskript.enums import World, Model
from manuskript.functions import mainWindow
from manuskript.ui import style as S
from manuskript.models.searchableModel import searchableModel
from manuskript.models.searchableItem import searchableItem
from manuskript.searchLabels import WorldSearchLabels
class worldModel(QStandardItemModel):
class worldModel(QStandardItemModel, searchableModel):
def __init__(self, parent):
QStandardItemModel.__init__(self, 0, len(World), parent)
self.mw = mainWindow()
@ -136,6 +138,9 @@ class worldModel(QStandardItemModel):
_id = QStandardItem(self.getUniqueID())
row = [name, _id] + [QStandardItem() for i in range(2, len(World))]
parent.appendRow(row)
self.mw.treeWorld.setExpanded(self.selectedIndex(), True)
self.mw.treeWorld.setCurrentIndex(self.indexFromItem(name))
return name
def getUniqueID(self):
@ -186,7 +191,7 @@ class worldModel(QStandardItemModel):
for index in indexes:
item = self.itemFromIndex(index)
parent = item.parent()
if parent is None:
if parent == None:
parent = self.invisibleRootItem()
row_indexes.append((parent, item.row()))
@ -353,3 +358,51 @@ class worldModel(QStandardItemModel):
return QSize(0, h + 6)
return QStandardItemModel.data(self, index, role)
#######################################################################
# Search
#######################################################################
def searchableItems(self):
def readAll(item):
items = [WorldItemSearchWrapper(item, self.itemID(item), self.indexFromItem(item), self.data)]
for c in self.children(item):
items += readAll(c)
return items
return readAll(self.invisibleRootItem())
class WorldItemSearchWrapper(searchableItem):
def __init__(self, item, itemID, itemIndex, getColumnData):
super().__init__(WorldSearchLabels)
self.item = item
self.itemID = itemID
self.itemIndex = itemIndex
self.getColumnData = getColumnData
def searchModel(self):
return Model.World
def searchID(self):
return self.itemID
def searchTitle(self, column):
return self.item.text()
def searchPath(self, column):
def _path(item):
path = []
if item.parent():
path += _path(item.parent())
path.append(item.text())
return path
return [self.translate("World")] + _path(self.item) + [self.translate(self.searchColumnLabel(column))]
def searchData(self, column):
return self.getColumnData(self.itemIndex.sibling(self.itemIndex.row(), column))

View file

@ -0,0 +1,56 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from manuskript.enums import Outline, Character, FlatData, World, Plot, PlotStep
OutlineSearchLabels = {
Outline.title: "Title",
Outline.text: "Text",
Outline.summarySentence: "One sentence summary",
Outline.summaryFull: "Summary",
Outline.POV: "POV",
Outline.notes: "Notes",
Outline.status: "Status",
Outline.label: "Label"
}
CharacterSearchLabels = {
Character.name: "Name",
Character.motivation: "Motivation",
Character.goal: "Goal",
Character.conflict: "Conflict",
Character.epiphany: "Epiphany",
Character.summarySentence: "One sentence summary",
Character.summaryPara: "One paragraph summary",
Character.summaryFull: "Summary",
Character.notes: "Notes",
Character.infos: "Detailed info"
}
FlatDataSearchLabels = {
FlatData.summarySituation: "Situation",
FlatData.summarySentence: "One sentence summary",
FlatData.summaryPara: "One paragraph summary",
FlatData.summaryPage: "One page summary",
FlatData.summaryFull: "Full summary"
}
WorldSearchLabels = {
World.name: "Name",
World.description: "Description",
World.passion: "Passion",
World.conflict: "Conflict"
}
# Search menu includes one single option for both plot and plotStep models. For plotStep related fields
# (like PlotStep.meta) we add an offset so it is not confused with the Plot enum value mapping to the same integer.
PLOT_STEP_COLUMNS_OFFSET = 30
PlotSearchLabels = {
Plot.name: "Name",
Plot.description: "Description",
Plot.characters: "Characters",
Plot.result: "Result",
Plot.summary: "Summary",
PLOT_STEP_COLUMNS_OFFSET + PlotStep.meta: "Meta"
}

View file

@ -8,6 +8,9 @@ from PyQt5.QtWidgets import qApp
from manuskript.enums import Outline
import logging
LOGGER = logging.getLogger(__name__)
# TODO: move some/all of those settings to application settings and not project settings
# in order to allow a shared project between several writers
@ -47,6 +50,8 @@ corkSizeFactor = 100
folderView = "cork"
lastTab = 0
openIndexes = [""]
progressChars = False
countSpaces = True
autoSave = False
autoSaveDelay = 5
autoSaveNoChanges = True
@ -123,7 +128,7 @@ def initDefaultValues():
def save(filename=None, protocol=None):
global spellcheck, dict, corkSliderFactor, viewSettings, corkSizeFactor, folderView, lastTab, openIndexes, \
autoSave, autoSaveDelay, saveOnQuit, autoSaveNoChanges, autoSaveNoChangesDelay, outlineViewColumns, \
progressChars, autoSave, autoSaveDelay, saveOnQuit, autoSaveNoChanges, autoSaveNoChangesDelay, outlineViewColumns, \
corkBackground, corkStyle, fullScreenTheme, defaultTextType, textEditor, revisions, frequencyAnalyzer, viewMode, \
saveToZip, dontShowDeleteWarning, fullscreenSettings
@ -136,6 +141,8 @@ def save(filename=None, protocol=None):
"folderView": folderView,
"lastTab": lastTab,
"openIndexes": openIndexes,
"progressChars": progressChars,
"countSpaces": countSpaces,
"autoSave":autoSave,
"autoSaveDelay":autoSaveDelay,
# TODO: Settings Cleanup Task -- Rename saveOnQuit to saveOnProjectClose -- see PR #615
@ -183,7 +190,7 @@ def load(string, fromString=False, protocol=None):
allSettings = pickle.load(f)
except:
print("{} doesn't exist, cannot load settings.".format(string))
LOGGER.error("Cannot load settings, {} does not exist.".format(string))
return
else:
if protocol == 0:
@ -235,6 +242,14 @@ def load(string, fromString=False, protocol=None):
global openIndexes
openIndexes = allSettings["openIndexes"]
if "progressChars" in allSettings:
global progressChars
progressChars = allSettings["progressChars"]
if "countSpaces" in allSettings:
global countSpaces
countSpaces = allSettings["countSpaces"]
if "autoSave" in allSettings:
global autoSave
autoSave = allSettings["autoSave"]

View file

@ -59,11 +59,16 @@ class settingsWindow(QWidget, Ui_Settings):
self.lstMenu.setMaximumWidth(140)
self.lstMenu.setMinimumWidth(140)
lowerKeys = [i.lower() for i in list(QStyleFactory.keys())]
# General
self.cmbStyle.addItems(list(QStyleFactory.keys()))
self.cmbStyle.setCurrentIndex(
[i.lower() for i in list(QStyleFactory.keys())]
.index(qApp.style().objectName()))
try:
self.cmbStyle.setCurrentIndex(lowerKeys.index(qApp.style().objectName()))
except ValueError:
self.cmbStyle.setCurrentIndex(0)
self.cmbStyle.currentIndexChanged[str].connect(self.setStyle)
self.cmbTranslation.clear()
@ -111,6 +116,9 @@ class settingsWindow(QWidget, Ui_Settings):
self.spnGeneralFontSize.setValue(f.pointSize())
self.spnGeneralFontSize.valueChanged.connect(self.setAppFontSize)
self.chkProgressChars.setChecked(settings.progressChars);
self.chkProgressChars.stateChanged.connect(self.charSettingsChanged)
self.txtAutoSave.setValidator(QIntValidator(0, 999, self))
self.txtAutoSaveNoChanges.setValidator(QIntValidator(0, 999, self))
self.chkAutoSave.setChecked(settings.autoSave)
@ -164,10 +172,12 @@ class settingsWindow(QWidget, Ui_Settings):
for item, what, value in [
(self.rdoTreeItemCount, "InfoFolder", "Count"),
(self.rdoTreeWC, "InfoFolder", "WC"),
(self.rdoTreeCC, "InfoFolder", "CC"),
(self.rdoTreeProgress, "InfoFolder", "Progress"),
(self.rdoTreeSummary, "InfoFolder", "Summary"),
(self.rdoTreeNothing, "InfoFolder", "Nothing"),
(self.rdoTreeTextWC, "InfoText", "WC"),
(self.rdoTreeTextCC, "InfoText", "CC"),
(self.rdoTreeTextProgress, "InfoText", "Progress"),
(self.rdoTreeTextSummary, "InfoText", "Summary"),
(self.rdoTreeTextNothing, "InfoText", "Nothing"),
@ -180,6 +190,9 @@ class settingsWindow(QWidget, Ui_Settings):
lambda v: self.lblTreeIconSize.setText("{}x{}".format(v, v)))
self.sldTreeIconSize.setValue(settings.viewSettings["Tree"]["iconSize"])
self.chkCountSpaces.setChecked(settings.countSpaces);
self.chkCountSpaces.stateChanged.connect(self.countSpacesChanged)
self.rdoCorkOldStyle.setChecked(settings.corkStyle == "old")
self.rdoCorkNewStyle.setChecked(settings.corkStyle == "new")
self.rdoCorkNewStyle.toggled.connect(self.setCorkStyle)
@ -338,6 +351,11 @@ class settingsWindow(QWidget, Ui_Settings):
sttgs = QSettings(qApp.organizationName(), qApp.applicationName())
sttgs.setValue("appFontSize", val)
def charSettingsChanged(self):
settings.progressChars = True if self.chkProgressChars.checkState() else False
self.mw.mainEditor.updateStats()
def saveSettingsChanged(self):
if self.txtAutoSave.text() in ["", "0"]:
self.txtAutoSave.setText("1")
@ -427,10 +445,12 @@ class settingsWindow(QWidget, Ui_Settings):
for item, what, value in [
(self.rdoTreeItemCount, "InfoFolder", "Count"),
(self.rdoTreeWC, "InfoFolder", "WC"),
(self.rdoTreeCC, "InfoFolder", "CC"),
(self.rdoTreeProgress, "InfoFolder", "Progress"),
(self.rdoTreeSummary, "InfoFolder", "Summary"),
(self.rdoTreeNothing, "InfoFolder", "Nothing"),
(self.rdoTreeTextWC, "InfoText", "WC"),
(self.rdoTreeTextCC, "InfoText", "CC"),
(self.rdoTreeTextProgress, "InfoText", "Progress"),
(self.rdoTreeTextSummary, "InfoText", "Summary"),
(self.rdoTreeTextNothing, "InfoText", "Nothing"),
@ -445,6 +465,11 @@ class settingsWindow(QWidget, Ui_Settings):
self.mw.treeRedacOutline.viewport().update()
def countSpacesChanged(self):
settings.countSpaces = True if self.chkCountSpaces.checkState() else False
self.mw.mainEditor.updateStats()
def setCorkColor(self):
color = QColor(settings.corkBackground["color"])
self.colorDialog = QColorDialog(color, self)

View file

@ -13,7 +13,8 @@ QApplication([])
# Create app and mainWindow
from manuskript import main
app, MW = main.prepare(tests=True)
arguments = main.process_commandline([])
app, MW = main.prepare(arguments, tests=True)
# FIXME: Again, don't know why, but when closing a project and then reopening
# one, we get a `TypeError: connection is not unique` in MainWindow:

View file

@ -12,7 +12,7 @@ def MW():
"""
from manuskript import functions as F
MW = F.mainWindow()
assert MW is not None
assert MW != None
assert MW == F.MW
return MW
@ -23,7 +23,7 @@ def MWNoProject(MW):
Take the MainWindow and close andy possibly open project.
"""
MW.closeProject()
assert MW.currentProject is None
assert MW.currentProject == None
return MW
@pytest.fixture
@ -35,9 +35,9 @@ def MWEmptyProject(MW):
tf = tempfile.NamedTemporaryFile(suffix=".msk")
MW.closeProject()
assert MW.currentProject is None
assert MW.currentProject == None
MW.welcome.createFile(tf.name, overwrite=True)
assert MW.currentProject is not None
assert MW.currentProject != None
return MW
# If using with: @pytest.fixture(scope='session', autouse=True)
@ -67,6 +67,6 @@ def MWSampleProject(MW):
shutil.copyfile(src, tf.name)
shutil.copytree(src[:-4], tf.name[:-4])
MW.loadProject(tf.name)
assert MW.currentProject is not None
assert MW.currentProject != None
return MW

View file

@ -123,16 +123,16 @@ def test_modelStuff(outlineModelBasic):
assert folder.findItemsContaining("VALUE", cols, MW, True) == []
assert folder.findItemsContaining("VALUE", cols, MW, False) == [text2.ID()]
# Model, count and copy
# Model, count and copy
k = folder._model
folder.setModel(14)
assert text2._model == 14
folder.setModel(None)
assert text2._model is None
folder.setModel(k)
assert folder.columnCount() == len(folder.enum)
text1 = text2.copy()
assert text1.ID() is None
assert text1.ID() == None
folder.appendChild(text1)
assert text1.ID() is not None
assert text1.ID() != None
assert folder.childCountRecursive() == 2
assert text1.path() == "Folder > Text"
assert len(text1.pathID()) == 2

View file

@ -39,7 +39,7 @@ def test_references(MWSampleProject):
assert "\n" in Ref.infos(Ref.plotReference(plotID))
assert "Not a ref" in Ref.infos("<invalid>")
assert "Unknown" in Ref.infos(Ref.plotReference("999"))
assert Ref.shortInfos(Ref.plotReference(plotID)) is not None
assert Ref.shortInfos(Ref.plotReference(plotID)) != None
assert Ref.shortInfos(Ref.plotReference("999")) == None
assert Ref.shortInfos("<invalidref>") == -1
@ -50,7 +50,7 @@ def test_references(MWSampleProject):
charID = IDs[0]
assert "\n" in Ref.infos(Ref.characterReference(charID))
assert "Unknown" in Ref.infos(Ref.characterReference("999"))
assert Ref.shortInfos(Ref.characterReference(charID)) is not None
assert Ref.shortInfos(Ref.characterReference(charID)) != None
assert Ref.shortInfos(Ref.characterReference("999")) == None
assert Ref.shortInfos("<invalidref>") == -1
@ -62,7 +62,7 @@ def test_references(MWSampleProject):
assert "\n" in Ref.infos(Ref.textReference(textID))
assert "Unknown" in Ref.infos(Ref.textReference("999"))
assert Ref.shortInfos(Ref.textReference(textID)) is not None
assert Ref.shortInfos(Ref.textReference(textID)) != None
assert Ref.shortInfos(Ref.textReference("999")) == None
assert Ref.shortInfos("<invalidref>") == -1
@ -73,7 +73,7 @@ def test_references(MWSampleProject):
assert "\n" in Ref.infos(Ref.worldReference(worldID))
assert "Unknown" in Ref.infos(Ref.worldReference("999"))
assert Ref.shortInfos(Ref.worldReference(worldID)) is not None
assert Ref.shortInfos(Ref.worldReference(worldID)) != None
assert Ref.shortInfos(Ref.worldReference("999")) == None
assert Ref.shortInfos("<invalidref>") == -1
@ -84,9 +84,9 @@ def test_references(MWSampleProject):
# Titles
for ref in refs:
assert Ref.title(ref) is not None
assert Ref.title("<invalid>") is None
assert Ref.title(Ref.plotReference("999")) is None
assert Ref.title(ref) != None
assert Ref.title("<invalid>") == None
assert Ref.title(Ref.plotReference("999")) == None
# Other stuff
assert Ref.type(Ref.plotReference(plotID)) == Ref.PlotLetter
@ -94,10 +94,10 @@ def test_references(MWSampleProject):
assert "Unknown" in Ref.tooltip(Ref.worldReference("999"))
assert "Not a ref" in Ref.tooltip("<invalid>")
for ref in refs:
assert Ref.tooltip(ref) is not None
assert Ref.tooltip(ref) != None
# Links
assert Ref.refToLink("<invalid>") is None
assert Ref.refToLink("<invalid>") == None
assert Ref.refToLink(Ref.plotReference("999")) == Ref.plotReference("999")
assert Ref.refToLink(Ref.characterReference("999")) == Ref.characterReference("999")
assert Ref.refToLink(Ref.textReference("999")) == Ref.textReference("999")
@ -106,7 +106,7 @@ def test_references(MWSampleProject):
assert "<a href" in Ref.refToLink(ref)
# Open
assert Ref.open("<invalid>") is None
assert Ref.open("<invalid>") == None
assert Ref.open(Ref.plotReference("999")) == False
assert Ref.open(Ref.characterReference("999")) == False
assert Ref.open(Ref.textReference("999")) == False

View file

@ -0,0 +1,41 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
import pytest
from manuskript.models.searchFilter import searchFilter
def test_searchFilter_constructionOk():
filter = searchFilter("label", True, [3])
assert filter.label() == "label"
assert filter.enabled() is True
assert filter.modelColumns() == [3]
def test_searchFilter_constructionOkWithNoneModelColumn():
filter = searchFilter("label", True)
assert filter.label() == "label"
assert filter.enabled() is True
assert filter.modelColumns() == []
def test_searchFilter_constructionBadLabelType():
with pytest.raises(TypeError, match=r".*label must be a str.*"):
searchFilter(13, True, [3])
def test_searchFilter_constructionBadEnabledType():
with pytest.raises(TypeError, match=r".*enabled must be a bool.*"):
searchFilter("label", 3, [3])
def test_searchFilter_constructionBadModelColumnType():
with pytest.raises(TypeError, match=r".*modelColumns must be a list or None.*"):
searchFilter("label", False, True)
def test_searchFilter_setEnabled():
filter = searchFilter("label", True, [3])
assert filter.enabled() is True
filter.setEnabled(False)
assert filter.enabled() is False

View file

@ -0,0 +1,16 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from manuskript.models.searchResultModel import searchResultModel
from manuskript.enums import Character
def test_searchResultModel_constructionOk():
searchResult = searchResultModel("Character", "3", Character.notes, "Lucas", "A > B > C", (15, 18), "This is <b>Lucas</b>")
assert searchResult.id() == "3"
assert searchResult.column() == Character.notes
assert searchResult.title() == "Lucas"
assert searchResult.path() == "A > B > C"
assert searchResult.pos() == (15, 18)
assert searchResult.context() == "This is <b>Lucas</b>"

View file

@ -3,6 +3,7 @@
"""Tests for functions"""
import re
from manuskript import functions as F
def test_wordCount():
@ -46,8 +47,8 @@ def test_several():
assert F.iconColor(icon).name().lower() == "#ff0000"
# themeIcon
assert F.themeIcon("text") is not None
assert F.themeIcon("nonexistingname") is not None
assert F.themeIcon("text") != None
assert F.themeIcon("nonexistingname") != None
# randomColor
c1 = F.randomColor()
@ -75,10 +76,10 @@ def test_outlineItemColors():
def test_paths():
assert F.appPath() is not None
assert F.writablePath is not None
assert F.appPath() != None
assert F.writablePath != None
assert len(F.allPaths("suffix")) == 2
assert F.tempFile("yop") is not None
assert F.tempFile("yop") != None
f = F.findBackground("spacedreams.jpg")
assert "resources/backgrounds/spacedreams.jpg" in f
assert len(F.customIcons()) > 1
@ -87,10 +88,59 @@ def test_mainWindow():
from PyQt5.QtWidgets import QWidget, QLCDNumber
assert F.mainWindow() is not None
assert F.MW is not None
assert F.mainWindow() != None
assert F.MW != None
F.statusMessage("Test")
F.printObjects()
assert len(F.findWidgetsOfClass(QWidget)) > 0
assert len(F.findWidgetsOfClass(QLCDNumber)) == 0
def test_search_noMatch():
assert F.search(re.compile("text"), "foo") == []
def test_search_singleLine_fullMatch():
assert F.search(re.compile("text"), "text") == [(0, 4, "<b>text</b>")]
def test_search_singleLine_start():
assert F.search(re.compile("text"), "text is this") == [(0, 4, "<b>text</b> is this")]
def test_search_singleLine_end():
assert F.search(re.compile("text"), "This is text") == [(8, 12, "This is <b>text</b>")]
def test_search_multipleLines_fullMatch():
assert F.search(re.compile("text"), "This is\ntext\nOK") == [(8, 12, "[...] <b>text</b> [...]")]
def test_search_multipleLines_start():
assert F.search(re.compile("text"), "This is\ntext oh yeah\nOK") == [(8, 12, "[...] <b>text</b> oh yeah [...]")]
def test_search_multipleLines_end():
assert F.search(re.compile("text"), "This is\nsome text\nOK") == [(13, 17, "[...] some <b>text</b> [...]")]
def test_search_multipleLines_full():
assert F.search(re.compile("text"), "This is\ntext\nOK") == [(8, 12, "[...] <b>text</b> [...]")]
def test_search_multiple_strMatches():
assert F.search(re.compile("text"), "text, text and more text") == [
(0, 4, "<b>text</b>, text and more text"),
(6, 10, "text, <b>text</b> and more text"),
(20, 24, "text, text and more <b>text</b>")
]
def test_search_multiple_strMatches_caseSensitive():
assert F.search(re.compile("text"), "TeXt, TEXT and more text") == [(20, 24, "TeXt, TEXT and more <b>text</b>")]
assert F.search(re.compile("text", re.IGNORECASE), "TeXt, TEXT and more text") == [
(0, 4, "<b>TeXt</b>, TEXT and more text"),
(6, 10, "TeXt, <b>TEXT</b> and more text"),
(20, 24, "TeXt, TEXT and more <b>text</b>")
]

View file

@ -55,7 +55,7 @@ def test_general(MWSampleProject):
state = settings()
assert chk.isChecked() == state
chk.setChecked(not state)
assert chk.isChecked() is not state
assert chk.isChecked() != state
# Loading and Saving
SW.txtAutoSave.setText("0")
@ -86,7 +86,7 @@ def test_general(MWSampleProject):
SW.chkOutlineTitle.setChecked(Qt.Unchecked)
SW.chkOutlineTitle.setChecked(Qt.Checked)
# Can't test because of the dialog
# assert SW.setCorkColor() is None
# assert SW.setCorkColor() == None
SW.sldTreeIconSize.setValue(SW.sldTreeIconSize.value() + 1)
SW.rdoCorkNewStyle.toggled.emit(True)
SW.cmbCorkImage.currentIndexChanged.emit(0)
@ -98,7 +98,7 @@ def test_general(MWSampleProject):
# Test editor
switchCheckBoxAndAssert(SW.chkEditorBackgroundTransparent,
lambda: S.textEditor["backgroundTransparent"])
assert SW.restoreEditorColors() is None
assert SW.restoreEditorColors() == None
switchCheckBoxAndAssert(SW.chkEditorNoBlinking,
lambda: S.textEditor["cursorNotBlinking"])
# Twice on purpose: set and restore
@ -108,7 +108,7 @@ def test_general(MWSampleProject):
SW.updateAllWidgets()
# Labels
assert SW.updateLabelColor(MW.mdlLabels.item(1).index()) is None
assert SW.updateLabelColor(MW.mdlLabels.item(1).index()) == None
rc = MW.mdlLabels.rowCount()
SW.addLabel()
SW.lstLabels.setCurrentIndex(
@ -150,7 +150,7 @@ def test_general(MWSampleProject):
for i in range(4):
SW.updateLineSpacing(i)
SW.updateUIFromTheme() # No time to wait on timer
assert SW._editingTheme is not None
assert SW._editingTheme != None
SW.resize(SW.geometry().size()) # resizeEvent
#TODO: other edit test (see SW.loadTheme
SW.saveTheme()

View file

@ -0,0 +1,56 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from manuskript.ui.searchMenu import searchMenu
from manuskript.enums import Outline, Character, FlatData, World, Plot, PlotStep, Model
from manuskript.searchLabels import PLOT_STEP_COLUMNS_OFFSET
def triggerFilter(filterKey, actions):
list(filter(lambda action: action.data() == filterKey, actions))[0].trigger()
def test_searchMenu_defaultColumns():
"""
By default all model columns are selected.
"""
search_menu = searchMenu()
assert set(search_menu.columns(Model.Outline)) == {
Outline.title, Outline.text, Outline.summaryFull,
Outline.summarySentence, Outline.notes, Outline.POV,
Outline.status, Outline.label
}
assert set(search_menu.columns(Model.Character)) == {
Character.name, Character.motivation, Character.goal, Character.conflict,
Character.epiphany, Character.summarySentence, Character.summaryPara,
Character.summaryFull, Character.notes, Character.infos
}
assert set(search_menu.columns(Model.FlatData)) == {
FlatData.summarySituation, FlatData.summarySentence, FlatData.summaryPara,
FlatData.summaryPage, FlatData.summaryFull
}
assert set(search_menu.columns(Model.World)) == {
World.name, World.description, World.passion, World.conflict
}
assert set(search_menu.columns(Model.Plot)) == {
Plot.name, Plot.description, Plot.characters, Plot.result,
Plot.summary, PLOT_STEP_COLUMNS_OFFSET + PlotStep.meta
}
def test_searchMenu_someColumns():
"""
When deselecting some filters the columns associated to those filters are not returned.
"""
search_menu = searchMenu()
triggerFilter(Model.Outline, search_menu.actions())
triggerFilter(Model.Character, search_menu.actions())
assert set(search_menu.columns(Model.Outline)) == set()
assert set(search_menu.columns(Model.Character)) == set()

View file

@ -96,7 +96,7 @@ class collapsibleDockWidgets(QToolBar):
def setCurrentGroup(self, group):
self.currentGroup = group
for btn, action, widget, grp in self.otherWidgets:
if not grp == group or grp is None:
if not grp == group or grp == None:
action.setVisible(False)
else:
action.setVisible(True)

View file

@ -6,6 +6,8 @@ from PyQt5.QtWidgets import QSizePolicy, QGroupBox, QWidget, QStylePainter, QSty
QStyle, QStyleOptionFrame, QStyleOptionFocusRect
from manuskript.ui import style as S
import logging
LOGGER = logging.getLogger(__name__)
class collapsibleGroupBox(QGroupBox):
def __init__(self, parent=None):
@ -25,7 +27,7 @@ class collapsibleGroupBox(QGroupBox):
self.tempWidget.setLayout(self.layout())
# Set empty layout
l = QVBoxLayout()
# print(l.contentsMargins().left(), l.contentsMargins().bottom(), l.contentsMargins().top(), )
# LOGGER.debug("Bounds: %s, %s, %s, %s", l.contentsMargins().left(), l.contentsMargins().bottom(), l.contentsMargins().top(), l.contentsMargins().right())
l.setContentsMargins(0, 0, 0, 0)
self.setLayout(l)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)

View file

@ -6,6 +6,8 @@ import re
from PyQt5.QtCore import QRegExp
from PyQt5.QtGui import QTextCursor
import logging
LOGGER = logging.getLogger(__name__)
def MDFormatSelection(editor, style):
"""
@ -15,5 +17,5 @@ def MDFormatSelection(editor, style):
1: italic
2: code
"""
print("Formatting:", style, " (Unimplemented yet !)")
LOGGER.error("Formatting: %s (Not implemented!)", style)
# FIXME

View file

@ -8,7 +8,7 @@ class blockUserData(QTextBlockUserData):
def getUserData(block):
"""Returns userData if it exists, or a blank one."""
data = block.userData()
if data is None:
if data == None:
data = blockUserData()
return data

View file

@ -2,11 +2,11 @@
# --!-- coding: utf8 --!--
import os
from PyQt5.QtCore import Qt, QSize, QPoint, QRect, QEvent, QTime, QTimer
from PyQt5.QtCore import Qt, QSize, QPoint, QRect, QEvent, QTime, QTimer, pyqtSignal
from PyQt5.QtGui import QFontMetrics, QColor, QBrush, QPalette, QPainter, QPixmap, QCursor
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QFrame, QWidget, QPushButton, qApp, QStyle, QComboBox, QLabel, QScrollBar, \
QStyleOptionSlider, QHBoxLayout, QVBoxLayout, QMenu, QAction
QStyleOptionSlider, QHBoxLayout, QVBoxLayout, QMenu, QAction, QDesktopWidget
# Spell checker support
from manuskript import settings
@ -19,9 +19,13 @@ from manuskript.ui.editors.themes import loadThemeDatas
from manuskript.ui.views.MDEditView import MDEditView
from manuskript.functions import Spellchecker
import logging
LOGGER = logging.getLogger(__name__)
class fullScreenEditor(QWidget):
def __init__(self, index, parent=None):
exited = pyqtSignal()
def __init__(self, index, parent=None, screenNumber=None):
QWidget.__init__(self, parent)
self.setAttribute(Qt.WA_DeleteOnClose, True)
self._background = None
@ -162,6 +166,12 @@ class fullScreenEditor(QWidget):
self.topPanel.setAutoHideVariable('autohide-top')
self.leftPanel.setAutoHideVariable('autohide-left')
# Set the screen to the same screen as the main window
if screenNumber is not None:
screenres = QDesktopWidget().screenGeometry(screenNumber);
self.move(QPoint(screenres.x(), screenres.y()));
self.resize(screenres.width(), screenres.height());
# Connection
self._index.model().dataChanged.connect(self.dataChanged)
@ -170,13 +180,13 @@ class fullScreenEditor(QWidget):
# self.showMaximized()
# self.show()
def __del__(self):
# print("Leaving fullScreenEditor via Destructor event", flush=True)
self.showNormal()
self.close()
def leaveFullscreen(self):
self.__exit__("Leaving fullScreenEditor via leaveFullScreen.")
def __exit__(self, message):
LOGGER.debug(message)
self.showNormal()
self.exited.emit()
self.close()
def setLocked(self, val):
@ -280,9 +290,7 @@ class fullScreenEditor(QWidget):
def keyPressEvent(self, event):
if event.key() in [Qt.Key_Escape, Qt.Key_F11] and \
not self._locked:
# print("Leaving fullScreenEditor via keyPressEvent", flush=True)
self.showNormal()
self.close()
self.__exit__("Leaving fullScreenEditor via keyPressEvent.")
elif (event.modifiers() & Qt.AltModifier) and \
event.key() in [Qt.Key_PageUp, Qt.Key_PageDown, Qt.Key_Left, Qt.Key_Right]:
if event.key() in [Qt.Key_PageUp, Qt.Key_Left]:
@ -338,8 +346,8 @@ class fullScreenEditor(QWidget):
item = self._index.internalPointer()
previousItem = self.previousTextItem(item)
nextItem = self.nextTextItem(item)
self.btnPrevious.setEnabled(previousItem is not None)
self.btnNext.setEnabled(nextItem is not None)
self.btnPrevious.setEnabled(previousItem != None)
self.btnNext.setEnabled(nextItem != None)
self.wPath.setItem(item)
def updateStatusBar(self):
@ -572,11 +580,11 @@ class myPanel(QWidget):
def addWidgetSetting(self, label, config_name, widgets):
setting = (label, config_name, widgets)
self._settings.append(setting)
if settings.fullscreenSettings.get(config_name, None) is not None:
if settings.fullscreenSettings.get(config_name, None) != None:
self._setSettingValue(setting, settings.fullscreenSettings[config_name])
def addSetting(self, label, config_name, default=True):
if settings.fullscreenSettings.get(config_name, None) is None:
if settings.fullscreenSettings.get(config_name, None) == None:
self._setConfig(config_name, default)
self.addWidgetSetting(label, config_name, None)
@ -651,7 +659,7 @@ class myPath(QWidget):
if i == item:
a.setIcon(QIcon.fromTheme("stock_yes"))
a.setEnabled(False)
elif self.editor.firstTextItem(i) is None:
elif self.editor.firstTextItem(i) == None:
a.setEnabled(False)
else:
a.triggered.connect(gen_cb(i))

View file

@ -105,6 +105,6 @@ class locker(QWidget, Ui_locker):
text))
# Word locked
elif self._target is not None:
elif self._target != None:
self.btnLock.setText(self.tr("{} words remaining").format(
self._target - self._words))

View file

@ -5,7 +5,7 @@ import locale
from PyQt5.QtCore import QModelIndex, QRect, QPoint
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap, QPainter, QIcon
from PyQt5.QtWidgets import QWidget, qApp
from PyQt5.QtWidgets import QWidget, qApp, QDesktopWidget
from manuskript import settings
from manuskript.enums import Outline
@ -20,6 +20,9 @@ try:
except:
pass
import logging
LOGGER = logging.getLogger(__name__)
class mainEditor(QWidget, Ui_mainEditor):
"""
`mainEditor` is responsible for opening `outlineItem`s and offering information
@ -64,6 +67,7 @@ class mainEditor(QWidget, Ui_mainEditor):
QWidget.__init__(self, parent)
self.setupUi(self)
self._updating = False
self._fullScreen = None
self.mw = mainWindow()
@ -120,7 +124,7 @@ class mainEditor(QWidget, Ui_mainEditor):
return self.tabSplitter.tab
def currentEditor(self, tabWidget=None):
if tabWidget is None:
if tabWidget == None:
tabWidget = self.currentTabWidget()
return tabWidget.currentWidget()
# return self.tab.currentWidget()
@ -151,9 +155,13 @@ class mainEditor(QWidget, Ui_mainEditor):
for ts in reversed(self.allTabSplitters()):
ts.closeSplit()
def close(self):
if self._fullScreen is not None:
self._fullScreen.leaveFullscreen()
def allTabs(self, tabWidget=None):
"""Returns all the tabs from the given tabWidget. If tabWidget is None, from the current tabWidget."""
if tabWidget is None:
if tabWidget == None:
tabWidget = self.currentTabWidget()
return [tabWidget.widget(i) for i in range(tabWidget.count())]
@ -205,7 +213,7 @@ class mainEditor(QWidget, Ui_mainEditor):
title = self.getIndexTitle(index)
if tabWidget is None:
if tabWidget == None:
tabWidget = self.currentTabWidget()
# Checking if tab is already opened
@ -292,6 +300,7 @@ class mainEditor(QWidget, Ui_mainEditor):
return
index = self.currentEditor().currentIndex
if index.isValid():
item = index.internalPointer()
else:
@ -300,15 +309,21 @@ class mainEditor(QWidget, Ui_mainEditor):
if not item:
item = self.mw.mdlOutline.rootItem
cc = item.data(Outline.charCount)
wc = item.data(Outline.wordCount)
goal = item.data(Outline.goal)
chars = item.data(Outline.charCount) # len(item.data(Outline.text))
progress = item.data(Outline.goalPercentage)
goal = uiParse(goal, None, int, lambda x: x>=0)
progress = uiParse(progress, 0.0, float)
if not cc:
cc = 0
if not wc:
wc = 0
if goal:
self.lblRedacProgress.show()
rect = self.lblRedacProgress.geometry()
@ -319,13 +334,31 @@ class mainEditor(QWidget, Ui_mainEditor):
drawProgress(p, rect, progress, 2)
del p
self.lblRedacProgress.setPixmap(self.px)
self.lblRedacWC.setText(self.tr("{} words / {} ").format(
locale.format_string("%d", wc, grouping=True),
locale.format_string("%d", goal, grouping=True)))
if settings.progressChars:
self.lblRedacWC.setText(self.tr("({} chars) {} words / {} ").format(
locale.format("%d", cc, grouping=True),
locale.format("%d", wc, grouping=True),
locale.format("%d", goal, grouping=True)))
self.lblRedacWC.setToolTip("")
else:
self.lblRedacWC.setText(self.tr("{} words / {} ").format(
locale.format("%d", wc, grouping=True),
locale.format("%d", goal, grouping=True)))
self.lblRedacWC.setToolTip(self.tr("{} chars").format(
locale.format("%d", cc, grouping=True)))
else:
self.lblRedacProgress.hide()
self.lblRedacWC.setText(self.tr("{} words ").format(
locale.format_string("%d", wc, grouping=True)))
if settings.progressChars:
self.lblRedacWC.setText(self.tr("{} chars ").format(
locale.format("%d", cc, grouping=True)))
self.lblRedacWC.setToolTip("")
else:
self.lblRedacWC.setText(self.tr("{} words ").format(
locale.format("%d", wc, grouping=True)))
self.lblRedacWC.setToolTip(self.tr("{} chars").format(
locale.format("%d", cc, grouping=True)))
###############################################################################
# VIEWS
@ -354,14 +387,21 @@ class mainEditor(QWidget, Ui_mainEditor):
def showFullScreen(self):
if self.currentEditor():
self._fullScreen = fullScreenEditor(self.currentEditor().currentIndex)
currentScreenNumber = QDesktopWidget().screenNumber(widget=self)
self._fullScreen = fullScreenEditor(
self.currentEditor().currentIndex,
screenNumber=currentScreenNumber)
# Clean the variable when closing fullscreen prevent errors
self._fullScreen.exited.connect(self.clearFullScreen)
def clearFullScreen(self):
self._fullScreen = None
###############################################################################
# DICT AND STUFF LIKE THAT
###############################################################################
def setDict(self, dict):
print(dict)
for w in self.allAllTabs():
w.setDict(dict)

View file

@ -10,6 +10,8 @@ from manuskript.functions import mainWindow, appPath
from manuskript.ui import style
from manuskript.ui.editors.tabSplitter_ui import Ui_tabSplitter
import logging
LOGGER = logging.getLogger(__name__)
class tabSplitter(QWidget, Ui_tabSplitter):
"""
@ -39,7 +41,7 @@ class tabSplitter(QWidget, Ui_tabSplitter):
# try:
# self.tab.setTabBarAutoHide(True)
# except AttributeError:
# print("Info: install Qt 5.4 or higher to use tab bar auto-hide in editor.")
# LOGGER.info("Install Qt 5.4 or higher to use tab bar auto-hide in editor.")
# Button to split
self.btnSplit = QPushButton(self)
@ -145,8 +147,8 @@ class tabSplitter(QWidget, Ui_tabSplitter):
def split(self, toggled=None, state=None):
if state is None and self.splitState == 0 or state == 1:
if self.secondTab is None:
if state == None and self.splitState == 0 or state == 1:
if self.secondTab == None:
self.addSecondTab()
self.splitState = 1
@ -155,8 +157,8 @@ class tabSplitter(QWidget, Ui_tabSplitter):
self.btnSplit.setIcon(QIcon.fromTheme("split-vertical"))
self.btnSplit.setToolTip(self.tr("Split horizontally"))
elif state is None and self.splitState == 1 or state == 2:
if self.secondTab is None:
elif state == None and self.splitState == 1 or state == 2:
if self.secondTab == None:
self.addSecondTab()
self.splitter.setOrientation(Qt.Vertical)
@ -212,7 +214,7 @@ class tabSplitter(QWidget, Ui_tabSplitter):
# self.btnSplit.setGeometry(QRect(0, 0, 24, 24))
def focusChanged(self, old, new):
if self.secondTab is None or new is None:
if self.secondTab == None or new == None:
return
oldFT = self.focusTab

View file

@ -12,13 +12,14 @@ import manuskript.ui.style as S
from manuskript import settings
from manuskript import functions as F
import logging
LOGGER = logging.getLogger(__name__)
class BasicHighlighter(QSyntaxHighlighter):
def __init__(self, editor):
QSyntaxHighlighter.__init__(self, editor.document())
self.editor = editor
self._misspelledColor = Qt.red
self._defaultBlockFormat = QTextBlockFormat()
self._defaultCharFormat = QTextCharFormat()
self.defaultTextColor = QColor(S.text)
@ -27,6 +28,40 @@ class BasicHighlighter(QSyntaxHighlighter):
self.linkColor = QColor(S.link)
self.spellingErrorColor = QColor(Qt.red)
# Matches during checking can be separated by their type (all of them listed here):
# https://languagetool.org/development/api/org/languagetool/rules/ITSIssueType.html
#
# These are the colors for actual spell-, grammar- and style-checking:
self._errorColors = {
'addition' : QColor(255, 215, 0), # gold
'characters' : QColor(135, 206, 235), # sky blue
'duplication' : QColor(0, 255, 255), # cyan / aqua
'formatting' : QColor(0, 128, 128), # teal
'grammar' : QColor(0, 0, 255), # blue
'inconsistency' : QColor(128, 128, 0), # olive
'inconsistententities' : QColor(46, 139, 87), # sea green
'internationalization' : QColor(255, 165, 0), # orange
'legal' : QColor(255, 69, 0), # orange red
'length' : QColor(47, 79, 79), # dark slate gray
'localespecificcontent' : QColor(188, 143, 143),# rosy brown
'localeviolation' : QColor(128, 0, 0), # maroon
'markup' : QColor(128, 0, 128), # purple
'misspelling' : QColor(255, 0, 0), # red
'mistranslation' : QColor(255, 0, 255), # magenta / fuchsia
'nonconformance' : QColor(255, 218, 185), # peach puff
'numbers' : QColor(65, 105, 225), # royal blue
'omission' : QColor(255, 20, 147), # deep pink
'other' : QColor(138, 43, 226), # blue violet
'patternproblem' : QColor(0, 128, 0), # green
'register' : QColor(112,128,144), # slate gray
'style' : QColor(0, 255, 0), # lime
'terminology' : QColor(0, 0, 128), # navy
'typographical' : QColor(255, 255, 0), # yellow
'uncategorized' : QColor(128, 128, 128), # gray
'untranslated' : QColor(210, 105, 30), # chocolate
'whitespace' : QColor(192, 192, 192) # silver
}
def setDefaultBlockFormat(self, bf):
self._defaultBlockFormat = bf
self.rehighlight()
@ -36,7 +71,7 @@ class BasicHighlighter(QSyntaxHighlighter):
self.rehighlight()
def setMisspelledColor(self, color):
self._misspelledColor = color
self._errorColors['misspelled'] = color
def updateColorScheme(self, rehighlight=True):
"""
@ -97,14 +132,14 @@ class BasicHighlighter(QSyntaxHighlighter):
before you do any custom highlighting. Or implement doHighlightBlock.
"""
#print(">", self.currentBlock().document().availableUndoSteps())
#LOGGER.debug("undoSteps before: %s", self.currentBlock().document().availableUndoSteps())
c = QTextCursor(self.currentBlock())
#c.joinPreviousEditBlock()
bf = QTextBlockFormat(self._defaultBlockFormat)
if bf != c.blockFormat():
c.setBlockFormat(bf)
#c.endEditBlock()
#print(" ", self.currentBlock().document().availableUndoSteps())
#LOGGER.debug("undoSteps after: %s", self.currentBlock().document().availableUndoSteps())
# self.setFormat(0, len(text), self._defaultCharFormat)
@ -134,32 +169,25 @@ class BasicHighlighter(QSyntaxHighlighter):
txt.end() - txt.start(),
fmt)
# Spell checking
if hasattr(self.editor, "spellcheck") and self.editor.spellcheck and self.editor._dict:
# Spell checking
# Following algorithm would not check words at the end of line.
# This hacks adds a space to every line where the text cursor is not
# So that it doesn't spellcheck while typing, but still spellchecks at
# end of lines. See github's issue #166.
textedText = text
if self.currentBlock().position() + len(text) != \
self.editor.textCursor().position():
textedText = text + " "
# Following algorithm would not check words at the end of line.
# This hacks adds a space to every line where the text cursor is not
# So that it doesn't spellcheck while typing, but still spellchecks at
# end of lines. See github's issue #166.
textedText = text
if self.currentBlock().position() + len(text) != \
self.editor.textCursor().position():
textedText = text + " "
# Based on http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/
WORDS = r'(?iu)((?:[^_\W]|\')+)[^A-Za-z0-9\']'
# (?iu) means case insensitive and Unicode
# ((?:[^_\W]|\')+) means words exclude underscores but include apostrophes
# [^A-Za-z0-9\'] used with above hack to prevent spellcheck while typing word
#
# See also https://stackoverflow.com/questions/2062169/regex-w-in-utf-8
if hasattr(self.editor, "spellcheck") and self.editor.spellcheck:
for word_object in re.finditer(WORDS, textedText):
if (self.editor._dict
and self.editor._dict.isMisspelled(word_object.group(1))):
format = self.format(word_object.start(1))
format.setUnderlineColor(self._misspelledColor)
# The text should only be checked once as a whole
for match in self.editor._dict.checkText(textedText):
if match.locqualityissuetype in self._errorColors:
highlight_color = self._errorColors[match.locqualityissuetype]
format = self.format(match.start)
format.setUnderlineColor(highlight_color)
# SpellCheckUnderline fails with some fonts
format.setUnderlineStyle(QTextCharFormat.WaveUnderline)
self.setFormat(word_object.start(1),
word_object.end(1) - word_object.start(1),
format)
self.setFormat(match.start, match.end - match.start, format)

View file

@ -713,7 +713,7 @@ class MarkdownHighlighter(BasicHighlighter):
# FIXME: TypeError: could not convert 'TextBlockData' to 'QTextBlockUserData'
# blockData = self.currentBlockUserData()
# if blockData is None:
# if blockData == None:
# blockData = TextBlockData(self.document(), self.currentBlock())
#
# self.setCurrentBlockUserData(blockData)

View file

@ -9,6 +9,9 @@ from PyQt5.QtWidgets import *
from manuskript.ui.highlighters import MarkdownState as MS
from manuskript.ui.highlighters import MarkdownTokenType as MTT
import logging
LOGGER = logging.getLogger(__name__)
# This file is simply a python translation of GhostWriter's Tokenizer.
# http://wereturtle.github.io/ghostwriter/
# GPLV3+.
@ -56,7 +59,7 @@ class HighlightTokenizer:
self.tokens.append(token)
if token.type == -1:
print("Error here", token.position, token.length)
LOGGER.error("Token type invalid: position %s, length %s.", token.position, token.length)
def setState(self, state):
self.state = state

View file

@ -0,0 +1,2 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--

View file

@ -0,0 +1,13 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
class abstractSearchResultHighlighter():
"""
Interface for all classes highlighting search results on widgets.
"""
def __init__(self):
pass
def highlightSearchResult(self, searchResult):
raise NotImplementedError

View file

@ -0,0 +1,24 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from manuskript.ui.highlighters.searchResultHighlighters.widgetSelectionHighlighter import widgetSelectionHighlighter
class abstractSearchResultHighlighter():
def __init__(self):
self._widgetSelectionHighlighter = widgetSelectionHighlighter()
def highlightSearchResult(self, searchResult):
self.openView(searchResult)
widgets = self.retrieveWidget(searchResult)
if not isinstance(widgets, list):
widgets = [widgets]
for i in range(len(widgets)):
self._widgetSelectionHighlighter.highlight_widget_selection(widgets[i], searchResult.pos()[i][0], searchResult.pos()[i][1], i == len(widgets) - 1)
def openView(self, searchResult):
raise RuntimeError
def retrieveWidget(self, searchResult):
raise RuntimeError

View file

@ -0,0 +1,38 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from manuskript.models import references as Ref
from manuskript.functions import mainWindow
from manuskript.enums import Character
from PyQt5.QtWidgets import QTextEdit, QTableView, QLineEdit
from manuskript.ui.highlighters.searchResultHighlighters.abstractSpecificSearchResultHighlighter import abstractSearchResultHighlighter
class characterSearchResultHighlighter(abstractSearchResultHighlighter):
def __init__(self):
super().__init__()
def openView(self, searchResult):
r = Ref.characterReference(searchResult.id())
Ref.open(r)
mainWindow().tabPersos.setEnabled(True)
def retrieveWidget(self, searchResult):
textEditMap = {
Character.name: (0, "txtPersoName", QLineEdit),
Character.goal: (0, "txtPersoGoal", QTextEdit),
Character.motivation: (0, "txtPersoMotivation", QTextEdit),
Character.conflict: (0, "txtPersoConflict", QTextEdit),
Character.epiphany: (0, "txtPersoEpiphany", QTextEdit),
Character.summarySentence: (0, "txtPersoSummarySentence", QTextEdit),
Character.summaryPara: (0, "txtPersoSummaryPara", QTextEdit),
Character.summaryFull: (1, "txtPersoSummaryFull", QTextEdit),
Character.notes: (2, "txtPersoNotes", QTextEdit),
Character.infos: (3, "tblPersoInfos", QTableView)
}
characterTabIndex, characterWidgetName, characterWidgetClass = textEditMap[searchResult.column()]
mainWindow().tabPersos.setCurrentIndex(characterTabIndex)
return mainWindow().tabPersos.findChild(characterWidgetClass, characterWidgetName)

View file

@ -0,0 +1,29 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from manuskript.functions import mainWindow
from manuskript.enums import FlatData
from PyQt5.QtWidgets import QTextEdit, QLineEdit
from manuskript.ui.highlighters.searchResultHighlighters.abstractSpecificSearchResultHighlighter import abstractSearchResultHighlighter
class flatDataSearchResultHighlighter(abstractSearchResultHighlighter):
def __init__(self):
super().__init__()
def openView(self, searchResult):
mainWindow().tabMain.setCurrentIndex(mainWindow().TabSummary)
def retrieveWidget(self, searchResult):
editors = {
FlatData.summarySituation: (0, "txtSummarySituation", QLineEdit, mainWindow()),
FlatData.summarySentence: (0, "txtSummarySentence", QTextEdit, mainWindow().tabSummary),
FlatData.summaryPara: (1, "txtSummaryPara", QTextEdit, mainWindow().tabSummary),
FlatData.summaryPage: (2, "txtSummaryPage", QTextEdit, mainWindow().tabSummary),
FlatData.summaryFull: (3, "txtSummaryFull", QTextEdit, mainWindow().tabSummary)
}
stackIndex, editorName, editorClass, rootWidget = editors[searchResult.column()]
mainWindow().tabSummary.setCurrentIndex(stackIndex)
return rootWidget.findChild(editorClass, editorName)

View file

@ -0,0 +1,47 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from manuskript.models import references as Ref
from manuskript.enums import Outline
from manuskript.ui.highlighters.searchResultHighlighters.abstractSpecificSearchResultHighlighter import abstractSearchResultHighlighter
from manuskript.functions import mainWindow
from PyQt5.QtWidgets import QTextEdit, QLineEdit, QLabel
from manuskript.ui.views.metadataView import metadataView
from manuskript.ui.collapsibleGroupBox2 import collapsibleGroupBox2
class outlineSearchResultHighlighter(abstractSearchResultHighlighter):
def __init__(self):
super().__init__()
self.outline_index = None
def openView(self, searchResult):
r = Ref.textReference(searchResult.id())
Ref.open(r)
def retrieveWidget(self, searchResult):
editors = {
Outline.text: ("txtRedacText", QTextEdit, None),
Outline.title: ("txtTitle", QLineEdit, "grpProperties"),
Outline.summarySentence: ("txtSummarySentence", QLineEdit, "grpSummary"),
Outline.summaryFull: ("txtSummaryFull", QTextEdit, "grpSummary"),
Outline.notes: ("txtNotes", QTextEdit, "grpNotes"),
# TODO: Tried to highlight the combo box themselves (ie. cmbPOV) but didn't succeed.
Outline.POV: ("lblPOV", QLabel, "grpProperties"),
Outline.status: ("lblStatus", QLabel, "grpProperties"),
Outline.label: ("lblLabel", QLabel, "grpProperties")
}
editorName, editorClass, parentName = editors[searchResult.column()]
# Metadata columns are inside a splitter widget that my be hidden, so we show them.
if parentName:
metadataViewWidget = mainWindow().findChild(metadataView, "redacMetadata")
metadataViewWidget.show()
metadataViewWidget.findChild(collapsibleGroupBox2, parentName).button.setChecked(True)
widget = metadataViewWidget.findChild(editorClass, editorName)
else:
widget = mainWindow().mainEditor.currentEditor().findChild(editorClass, editorName)
return widget

View file

@ -0,0 +1,32 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from manuskript.models import references as Ref
from manuskript.functions import mainWindow
from manuskript.enums import Plot
from PyQt5.QtWidgets import QTextEdit, QLineEdit, QListView
from manuskript.ui.highlighters.searchResultHighlighters.abstractSpecificSearchResultHighlighter import abstractSearchResultHighlighter
class plotSearchResultHighlighter(abstractSearchResultHighlighter):
def __init__(self):
super().__init__()
def openView(self, searchResult):
r = Ref.plotReference(searchResult.id())
Ref.open(r)
mainWindow().tabPlot.setEnabled(True)
def retrieveWidget(self, searchResult):
textEditMap = {
Plot.name: (0, "txtPlotName", QLineEdit),
Plot.description: (0, "txtPlotDescription", QTextEdit),
Plot.characters: (0, "lstPlotPerso", QListView),
Plot.result: (0, "txtPlotResult", QTextEdit)
}
tabIndex, widgetName, widgetClass = textEditMap[searchResult.column()]
mainWindow().tabPlot.setCurrentIndex(tabIndex)
return mainWindow().tabPlot.findChild(widgetClass, widgetName)

View file

@ -0,0 +1,35 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from manuskript.models import references as Ref
from manuskript.functions import mainWindow
from manuskript.enums import PlotStep
from PyQt5.QtWidgets import QTableView, QTextEdit
from manuskript.ui.highlighters.searchResultHighlighters.abstractSpecificSearchResultHighlighter import abstractSearchResultHighlighter
class plotStepSearchResultHighlighter(abstractSearchResultHighlighter):
def __init__(self):
super().__init__()
def openView(self, searchResult):
r = Ref.plotReference(searchResult.id())
Ref.open(r)
mainWindow().tabPlot.setEnabled(True)
def retrieveWidget(self, searchResult):
textEditMap = {
PlotStep.name: [(1, "lstSubPlots", QTableView)],
PlotStep.meta: [(1, "lstSubPlots", QTableView)],
PlotStep.summary: [(1, "lstSubPlots", QTableView), (1, "txtSubPlotSummary", QTextEdit)]
}
map = textEditMap[searchResult.column()]
widgets = []
for tabIndex, widgetName, widgetClass in map:
mainWindow().tabPlot.setCurrentIndex(tabIndex)
widgets.append(mainWindow().tabPlot.findChild(widgetClass, widgetName))
return widgets

View file

@ -0,0 +1,34 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from manuskript.ui.highlighters.searchResultHighlighters.abstractSearchResultHighlighter import abstractSearchResultHighlighter
from manuskript.ui.highlighters.searchResultHighlighters.characterSearchResultHighlighter import characterSearchResultHighlighter
from manuskript.ui.highlighters.searchResultHighlighters.flatDataSearchResultHighlighter import flatDataSearchResultHighlighter
from manuskript.ui.highlighters.searchResultHighlighters.outlineSearchResultHighlighter import outlineSearchResultHighlighter
from manuskript.ui.highlighters.searchResultHighlighters.worldSearchResultHighlighter import worldSearchResultHighlighter
from manuskript.ui.highlighters.searchResultHighlighters.plotSearchResultHighlighter import plotSearchResultHighlighter
from manuskript.ui.highlighters.searchResultHighlighters.plotStepSearchResultHighlighter import plotStepSearchResultHighlighter
from manuskript.enums import Model
class searchResultHighlighter(abstractSearchResultHighlighter):
def __init__(self):
super().__init__()
def highlightSearchResult(self, searchResult):
if searchResult.type() == Model.Character:
highlighter = characterSearchResultHighlighter()
elif searchResult.type() == Model.FlatData:
highlighter = flatDataSearchResultHighlighter()
elif searchResult.type() == Model.Outline:
highlighter = outlineSearchResultHighlighter()
elif searchResult.type() == Model.World:
highlighter = worldSearchResultHighlighter()
elif searchResult.type() == Model.Plot:
highlighter = plotSearchResultHighlighter()
elif searchResult.type() == Model.PlotStep:
highlighter = plotStepSearchResultHighlighter()
else:
raise NotImplementedError
highlighter.highlightSearchResult(searchResult)

View file

@ -0,0 +1,91 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from PyQt5.QtGui import QTextCursor
from PyQt5.QtWidgets import QTextEdit, QTableView, QListView, QLineEdit, QPlainTextEdit, QLabel
class widgetSelectionHighlighter():
"""
Utility class for highlighting a search result on a widget.
"""
def __init__(self):
pass
def highlight_widget_selection(self, widget, startPos, endPos, clearOnFocusOut=True):
if isinstance(widget, QTextEdit) or isinstance(widget, QPlainTextEdit):
self._highlightTextEditSearchResult(widget, startPos, endPos, clearOnFocusOut)
elif isinstance(widget, QLineEdit):
self._highlightLineEditSearchResult(widget, startPos, endPos, clearOnFocusOut)
elif isinstance(widget, QTableView):
self._highlightTableViewSearchResult(widget, startPos, clearOnFocusOut)
elif isinstance(widget, QListView):
self._highlightListViewSearchResult(widget, startPos, clearOnFocusOut)
elif isinstance(widget, QLabel):
self._highlightLabelSearchResult(widget, clearOnFocusOut)
else:
raise NotImplementedError
widget.setFocus(True)
@staticmethod
def generateClearHandler(widget, clearCallback):
"""
Generates a clear handler to be run when the given widget loses focus.
:param widget: widget we want to attach the handler to
:param clearCallback: callback to be called when the given widget loses focus.
:return:
"""
def clearHandler(_widget, previous_on_focus_out_event):
clearCallback(_widget)
_widget.focusOutEvent = previous_on_focus_out_event
widget.focusOutEvent = lambda e: clearHandler(widget, widget.focusOutEvent)
def _highlightTextEditSearchResult(self, textEdit, startPos, endPos, clearOnFocusOut):
# On focus out, clear text edit selection.
oldTextCursor = textEdit.textCursor()
if clearOnFocusOut:
self.generateClearHandler(textEdit, lambda widget: widget.setTextCursor(oldTextCursor))
# Highlight search result on the text edit.
c = textEdit.textCursor()
c.setPosition(startPos)
c.setPosition(endPos, QTextCursor.KeepAnchor)
textEdit.setTextCursor(c)
def _highlightLineEditSearchResult(self, lineEdit, startPos, endPos, clearOnFocusOut):
# On focus out, clear line edit selection.
if clearOnFocusOut:
self.generateClearHandler(lineEdit, lambda widget: widget.deselect())
# Highlight search result on line edit.
lineEdit.setCursorPosition(startPos)
lineEdit.cursorForward(True, endPos - startPos)
def _highlightTableViewSearchResult(self, tableView, startPos, clearOnFocusOut):
# On focus out, clear table selection.
if clearOnFocusOut:
self.generateClearHandler(tableView, lambda widget: widget.clearSelection())
# Highlight table row containing search result.
tableView.selectRow(startPos)
def _highlightListViewSearchResult(self, listView, startPos, clearOnFocusOut):
# On focus out, clear table selection.
if clearOnFocusOut:
self.generateClearHandler(listView, lambda widget: widget.selectionModel().clearSelection())
# Highlight list item containing search result.
listView.setCurrentIndex(listView.model().index(startPos, 0, listView.rootIndex()))
def _highlightLabelSearchResult(self, label, clearOnFocusOut):
# On focus out, clear label selection.
# FIXME: This would overwrite all styles!
oldStyle = label.styleSheet()
if clearOnFocusOut:
self.generateClearHandler(label, lambda widget: widget.setStyleSheet(oldStyle))
# Highlight search result on label.
label.setStyleSheet("background-color: steelblue")

View file

@ -0,0 +1,32 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from manuskript.models import references as Ref
from manuskript.functions import mainWindow
from manuskript.enums import World
from PyQt5.QtWidgets import QTextEdit, QLineEdit
from manuskript.ui.highlighters.searchResultHighlighters.abstractSpecificSearchResultHighlighter import abstractSearchResultHighlighter
class worldSearchResultHighlighter(abstractSearchResultHighlighter):
def __init__(self):
super().__init__()
def openView(self, searchResult):
r = Ref.worldReference(searchResult.id())
Ref.open(r)
mainWindow().tabWorld.setEnabled(True)
def retrieveWidget(self, searchResult):
textEditMap = {
World.name: (0, "txtWorldName", QLineEdit),
World.description: (0, "txtWorldDescription", QTextEdit),
World.passion: (1, "txtWorldPassion", QTextEdit),
World.conflict: (1, "txtWorldConflict", QTextEdit),
}
tabIndex, widgetName, widgetClass = textEditMap[searchResult.column()]
mainWindow().tabWorld.setCurrentIndex(tabIndex)
return mainWindow().tabWorld.findChild(widgetClass, widgetName)

View file

@ -2,12 +2,15 @@
# Form implementation generated from reading ui file 'manuskript/ui/mainWindow.ui'
#
# Created by: PyQt5 UI code generator 5.5.1
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
@ -378,69 +381,11 @@ class Ui_MainWindow(object):
self.scrollAreaPersoInfos.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
self.scrollAreaPersoInfos.setObjectName("scrollAreaPersoInfos")
self.scrollAreaPersoInfosWidget = QtWidgets.QWidget()
self.scrollAreaPersoInfosWidget.setGeometry(QtCore.QRect(0, 0, 204, 606))
self.scrollAreaPersoInfosWidget.setGeometry(QtCore.QRect(0, 0, 429, 719))
self.scrollAreaPersoInfosWidget.setObjectName("scrollAreaPersoInfosWidget")
self.formLayout_8 = QtWidgets.QFormLayout(self.scrollAreaPersoInfosWidget)
self.formLayout_8.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow)
self.formLayout_8.setObjectName("formLayout_8")
self.label_4 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_4.setObjectName("label_4")
self.formLayout_8.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.label_4)
self.txtPersoMotivation = MDEditCompleter(self.scrollAreaPersoInfosWidget)
self.txtPersoMotivation.setObjectName("txtPersoMotivation")
self.formLayout_8.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.txtPersoMotivation)
self.label_5 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_5.setObjectName("label_5")
self.formLayout_8.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.label_5)
self.txtPersoGoal = MDEditCompleter(self.scrollAreaPersoInfosWidget)
self.txtPersoGoal.setObjectName("txtPersoGoal")
self.formLayout_8.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.txtPersoGoal)
self.label_6 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_6.setObjectName("label_6")
self.formLayout_8.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.label_6)
self.txtPersoConflict = MDEditCompleter(self.scrollAreaPersoInfosWidget)
self.txtPersoConflict.setObjectName("txtPersoConflict")
self.formLayout_8.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.txtPersoConflict)
self.label_7 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_7.setObjectName("label_7")
self.formLayout_8.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.label_7)
self.txtPersoEpiphany = MDEditCompleter(self.scrollAreaPersoInfosWidget)
self.txtPersoEpiphany.setObjectName("txtPersoEpiphany")
self.formLayout_8.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.txtPersoEpiphany)
self.label_24 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_24.setObjectName("label_24")
self.formLayout_8.setWidget(8, QtWidgets.QFormLayout.LabelRole, self.label_24)
self.txtPersoSummarySentence = MDEditCompleter(self.scrollAreaPersoInfosWidget)
self.txtPersoSummarySentence.setObjectName("txtPersoSummarySentence")
self.formLayout_8.setWidget(8, QtWidgets.QFormLayout.FieldRole, self.txtPersoSummarySentence)
self.label_8 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_8.setObjectName("label_8")
self.formLayout_8.setWidget(9, QtWidgets.QFormLayout.LabelRole, self.label_8)
self.txtPersoSummaryPara = MDEditCompleter(self.scrollAreaPersoInfosWidget)
self.txtPersoSummaryPara.setObjectName("txtPersoSummaryPara")
self.formLayout_8.setWidget(9, QtWidgets.QFormLayout.FieldRole, self.txtPersoSummaryPara)
self.horizontalLayout_21 = QtWidgets.QHBoxLayout()
self.horizontalLayout_21.setObjectName("horizontalLayout_21")
spacerItem10 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_21.addItem(spacerItem10)
self.btnStepFour = QtWidgets.QPushButton(self.scrollAreaPersoInfosWidget)
icon = QtGui.QIcon.fromTheme("go-next")
self.btnStepFour.setIcon(icon)
self.btnStepFour.setFlat(True)
self.btnStepFour.setObjectName("btnStepFour")
self.horizontalLayout_21.addWidget(self.btnStepFour)
self.formLayout_8.setLayout(10, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_21)
self.label_18 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_18.setObjectName("label_18")
self.formLayout_8.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_18)
self.sldPersoImportance = sldImportance(self.scrollAreaPersoInfosWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.sldPersoImportance.sizePolicy().hasHeightForWidth())
self.sldPersoImportance.setSizePolicy(sizePolicy)
self.sldPersoImportance.setObjectName("sldPersoImportance")
self.formLayout_8.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.sldPersoImportance)
self.label_3 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_3.setObjectName("label_3")
self.formLayout_8.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_3)
@ -454,6 +399,73 @@ class Ui_MainWindow(object):
self.btnPersoColor.setObjectName("btnPersoColor")
self.horizontalLayout_3.addWidget(self.btnPersoColor)
self.formLayout_8.setLayout(2, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_3)
self.horizontalLayout_20 = QtWidgets.QHBoxLayout()
self.horizontalLayout_20.setObjectName("horizontalLayout_20")
self.sldPersoImportance = sldImportance(self.scrollAreaPersoInfosWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.sldPersoImportance.sizePolicy().hasHeightForWidth())
self.sldPersoImportance.setSizePolicy(sizePolicy)
self.sldPersoImportance.setObjectName("sldPersoImportance")
self.horizontalLayout_20.addWidget(self.sldPersoImportance)
self.chkPersoPOV = QtWidgets.QCheckBox(self.scrollAreaPersoInfosWidget)
self.chkPersoPOV.setChecked(False)
self.chkPersoPOV.setAutoRepeat(False)
self.chkPersoPOV.setTristate(False)
self.chkPersoPOV.setObjectName("chkPersoPOV")
self.horizontalLayout_20.addWidget(self.chkPersoPOV)
self.formLayout_8.setLayout(4, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_20)
self.label_4 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_4.setObjectName("label_4")
self.formLayout_8.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.label_4)
self.txtPersoMotivation = MDEditCompleter(self.scrollAreaPersoInfosWidget)
self.txtPersoMotivation.setObjectName("txtPersoMotivation")
self.formLayout_8.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.txtPersoMotivation)
self.label_5 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_5.setObjectName("label_5")
self.formLayout_8.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.label_5)
self.txtPersoGoal = MDEditCompleter(self.scrollAreaPersoInfosWidget)
self.txtPersoGoal.setObjectName("txtPersoGoal")
self.formLayout_8.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.txtPersoGoal)
self.label_6 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_6.setObjectName("label_6")
self.formLayout_8.setWidget(8, QtWidgets.QFormLayout.LabelRole, self.label_6)
self.txtPersoConflict = MDEditCompleter(self.scrollAreaPersoInfosWidget)
self.txtPersoConflict.setObjectName("txtPersoConflict")
self.formLayout_8.setWidget(8, QtWidgets.QFormLayout.FieldRole, self.txtPersoConflict)
self.label_7 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_7.setObjectName("label_7")
self.formLayout_8.setWidget(9, QtWidgets.QFormLayout.LabelRole, self.label_7)
self.txtPersoEpiphany = MDEditCompleter(self.scrollAreaPersoInfosWidget)
self.txtPersoEpiphany.setObjectName("txtPersoEpiphany")
self.formLayout_8.setWidget(9, QtWidgets.QFormLayout.FieldRole, self.txtPersoEpiphany)
self.label_24 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_24.setObjectName("label_24")
self.formLayout_8.setWidget(10, QtWidgets.QFormLayout.LabelRole, self.label_24)
self.txtPersoSummarySentence = MDEditCompleter(self.scrollAreaPersoInfosWidget)
self.txtPersoSummarySentence.setObjectName("txtPersoSummarySentence")
self.formLayout_8.setWidget(10, QtWidgets.QFormLayout.FieldRole, self.txtPersoSummarySentence)
self.label_8 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_8.setObjectName("label_8")
self.formLayout_8.setWidget(11, QtWidgets.QFormLayout.LabelRole, self.label_8)
self.txtPersoSummaryPara = MDEditCompleter(self.scrollAreaPersoInfosWidget)
self.txtPersoSummaryPara.setObjectName("txtPersoSummaryPara")
self.formLayout_8.setWidget(11, QtWidgets.QFormLayout.FieldRole, self.txtPersoSummaryPara)
self.horizontalLayout_21 = QtWidgets.QHBoxLayout()
self.horizontalLayout_21.setObjectName("horizontalLayout_21")
spacerItem10 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_21.addItem(spacerItem10)
self.btnStepFour = QtWidgets.QPushButton(self.scrollAreaPersoInfosWidget)
icon = QtGui.QIcon.fromTheme("go-next")
self.btnStepFour.setIcon(icon)
self.btnStepFour.setFlat(True)
self.btnStepFour.setObjectName("btnStepFour")
self.horizontalLayout_21.addWidget(self.btnStepFour)
self.formLayout_8.setLayout(12, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_21)
self.label_18 = QtWidgets.QLabel(self.scrollAreaPersoInfosWidget)
self.label_18.setObjectName("label_18")
self.formLayout_8.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.label_18)
self.scrollAreaPersoInfos.setWidget(self.scrollAreaPersoInfosWidget)
self.verticalLayout_20.addWidget(self.scrollAreaPersoInfos)
self.tabPersos.addTab(self.info, "")
@ -745,7 +757,7 @@ class Ui_MainWindow(object):
self.treeWorld.setRootIsDecorated(False)
self.treeWorld.setObjectName("treeWorld")
self.treeWorld.header().setVisible(False)
self.treeWorld.header().setDefaultSectionSize(0)
self.treeWorld.header().setDefaultSectionSize(35)
self.verticalLayout_32.addWidget(self.treeWorld)
self.horizontalLayout_19 = QtWidgets.QHBoxLayout()
self.horizontalLayout_19.setObjectName("horizontalLayout_19")
@ -833,6 +845,7 @@ class Ui_MainWindow(object):
self.layoutWidget = QtWidgets.QWidget(self.splitterOutlineH)
self.layoutWidget.setObjectName("layoutWidget")
self.verticalLayout_14 = QtWidgets.QVBoxLayout(self.layoutWidget)
self.verticalLayout_14.setContentsMargins(0, 0, 0, 0)
self.verticalLayout_14.setObjectName("verticalLayout_14")
self.splitterOutlineV = QtWidgets.QSplitter(self.layoutWidget)
self.splitterOutlineV.setOrientation(QtCore.Qt.Vertical)
@ -1029,7 +1042,7 @@ class Ui_MainWindow(object):
self.horizontalLayout_2.addWidget(self.stack)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1112, 30))
self.menubar.setGeometry(QtCore.QRect(0, 0, 1112, 21))
self.menubar.setObjectName("menubar")
self.menuFile = QtWidgets.QMenu(self.menubar)
self.menuFile.setObjectName("menuFile")
@ -1270,6 +1283,14 @@ class Ui_MainWindow(object):
self.actFormatList.setObjectName("actFormatList")
self.actFormatBlockquote = QtWidgets.QAction(MainWindow)
self.actFormatBlockquote.setObjectName("actFormatBlockquote")
self.actSearch = QtWidgets.QAction(MainWindow)
icon = QtGui.QIcon.fromTheme("edit-find")
self.actSearch.setIcon(icon)
self.actSearch.setObjectName("actSearch")
self.actSupport = QtWidgets.QAction(MainWindow)
self.actSupport.setObjectName("actSupport")
self.actLocateLog = QtWidgets.QAction(MainWindow)
self.actLocateLog.setObjectName("actLocateLog")
self.menuFile.addAction(self.actOpen)
self.menuFile.addAction(self.menuRecents.menuAction())
self.menuFile.addAction(self.actSave)
@ -1281,6 +1302,10 @@ class Ui_MainWindow(object):
self.menuFile.addSeparator()
self.menuFile.addAction(self.actQuit)
self.menuHelp.addAction(self.actShowHelp)
self.menuHelp.addSeparator()
self.menuHelp.addAction(self.actSupport)
self.menuHelp.addAction(self.actLocateLog)
self.menuHelp.addSeparator()
self.menuHelp.addAction(self.actAbout)
self.menuTools.addAction(self.actSpellcheck)
self.menuTools.addAction(self.actToolFrequency)
@ -1313,6 +1338,7 @@ class Ui_MainWindow(object):
self.menuEdit.addAction(self.actCopy)
self.menuEdit.addAction(self.actPaste)
self.menuEdit.addAction(self.actDelete)
self.menuEdit.addAction(self.actSearch)
self.menuEdit.addAction(self.actRename)
self.menuEdit.addSeparator()
self.menuEdit.addAction(self.mnuFormat.menuAction())
@ -1339,7 +1365,7 @@ class Ui_MainWindow(object):
self.retranslateUi(MainWindow)
self.stack.setCurrentIndex(1)
self.tabMain.setCurrentIndex(0)
self.tabMain.setCurrentIndex(2)
self.tabSummary.setCurrentIndex(0)
self.tabPersos.setCurrentIndex(0)
self.tabPlot.setCurrentIndex(0)
@ -1484,6 +1510,8 @@ class Ui_MainWindow(object):
self.tabMain.setTabText(self.tabMain.indexOf(self.lytTabSummary), _translate("MainWindow", "Summary"))
self.groupBox.setTitle(_translate("MainWindow", "Names"))
self.txtPersosFilter.setPlaceholderText(_translate("MainWindow", "Filter"))
self.label_3.setText(_translate("MainWindow", "Name"))
self.chkPersoPOV.setText(_translate("MainWindow", "Allow POV"))
self.label_4.setText(_translate("MainWindow", "Motivation"))
self.label_5.setText(_translate("MainWindow", "Goal"))
self.label_6.setText(_translate("MainWindow", "Conflict"))
@ -1492,7 +1520,6 @@ class Ui_MainWindow(object):
self.label_8.setText(_translate("MainWindow", "<html><head/><body><p align=\"right\">One paragraph<br/>summary</p></body></html>"))
self.btnStepFour.setText(_translate("MainWindow", "Next"))
self.label_18.setText(_translate("MainWindow", "Importance"))
self.label_3.setText(_translate("MainWindow", "Name"))
self.tabPersos.setTabText(self.tabPersos.indexOf(self.info), _translate("MainWindow", "Basic info"))
self.btnStepSix.setText(_translate("MainWindow", "Next"))
self.tabPersos.setTabText(self.tabPersos.indexOf(self.tab_11), _translate("MainWindow", "Summary"))
@ -1635,7 +1662,15 @@ class Ui_MainWindow(object):
self.actFormatOrderedList.setText(_translate("MainWindow", "&Ordered list"))
self.actFormatList.setText(_translate("MainWindow", "&Unordered list"))
self.actFormatBlockquote.setText(_translate("MainWindow", "B&lockquote"))
self.actSearch.setText(_translate("MainWindow", "Search"))
self.actSearch.setShortcut(_translate("MainWindow", "Ctrl+F"))
self.actSupport.setText(_translate("MainWindow", "&Technical Support"))
self.actSupport.setToolTip(_translate("MainWindow", "How to obtain technical support for Manuskript."))
self.actSupport.setShortcut(_translate("MainWindow", "F1"))
self.actLocateLog.setText(_translate("MainWindow", "&Locate log file..."))
self.actLocateLog.setIconText(_translate("MainWindow", "Locate log file"))
self.actLocateLog.setToolTip(_translate("MainWindow", "Locate the diagnostic log file used for this session."))
self.actLocateLog.setShortcut(_translate("MainWindow", "Shift+F1"))
from manuskript.ui.cheatSheet import cheatSheet
from manuskript.ui.editors.mainEditor import mainEditor
from manuskript.ui.search import search

View file

@ -124,7 +124,7 @@
<enum>QTabWidget::Rounded</enum>
</property>
<property name="currentIndex">
<number>0</number>
<number>2</number>
</property>
<property name="documentMode">
<bool>true</bool>
@ -815,75 +815,126 @@
<rect>
<x>0</x>
<y>0</y>
<width>204</width>
<height>606</height>
<width>429</width>
<height>719</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout_8">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="4" column="0">
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="lineEditView" name="txtPersoName"/>
</item>
<item>
<widget class="QPushButton" name="btnPersoColor">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_20">
<item>
<widget class="sldImportance" name="sldPersoImportance" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="chkPersoPOV">
<property name="text">
<string>Allow POV</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
<property name="autoRepeat">
<bool>false</bool>
</property>
<property name="tristate">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Motivation</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="6" column="1">
<widget class="MDEditCompleter" name="txtPersoMotivation"/>
</item>
<item row="5" column="0">
<item row="7" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Goal</string>
</property>
</widget>
</item>
<item row="5" column="1">
<item row="7" column="1">
<widget class="MDEditCompleter" name="txtPersoGoal"/>
</item>
<item row="6" column="0">
<item row="8" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Conflict</string>
</property>
</widget>
</item>
<item row="6" column="1">
<item row="8" column="1">
<widget class="MDEditCompleter" name="txtPersoConflict"/>
</item>
<item row="7" column="0">
<item row="9" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Epiphany</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="9" column="1">
<widget class="MDEditCompleter" name="txtPersoEpiphany"/>
</item>
<item row="8" column="0">
<item row="10" column="0">
<widget class="QLabel" name="label_24">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;right&quot;&gt;One sentence&lt;br/&gt;summary&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="8" column="1">
<item row="10" column="1">
<widget class="MDEditCompleter" name="txtPersoSummarySentence"/>
</item>
<item row="9" column="0">
<item row="11" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;right&quot;&gt;One paragraph&lt;br/&gt;summary&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="9" column="1">
<item row="11" column="1">
<widget class="MDEditCompleter" name="txtPersoSummaryPara"/>
</item>
<item row="10" column="1">
<item row="12" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_21">
<item>
<spacer name="horizontalSpacer_3">
@ -914,44 +965,13 @@
</item>
</layout>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label_18">
<property name="text">
<string>Importance</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="sldImportance" name="sldPersoImportance" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="lineEditView" name="txtPersoName"/>
</item>
<item>
<widget class="QPushButton" name="btnPersoColor">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
@ -1547,7 +1567,7 @@
<bool>false</bool>
</attribute>
<attribute name="headerDefaultSectionSize">
<number>0</number>
<number>35</number>
</attribute>
</widget>
</item>
@ -2095,7 +2115,7 @@
<x>0</x>
<y>0</y>
<width>1112</width>
<height>30</height>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
@ -2127,6 +2147,10 @@
<string>&amp;Help</string>
</property>
<addaction name="actShowHelp"/>
<addaction name="separator"/>
<addaction name="actSupport"/>
<addaction name="actLocateLog"/>
<addaction name="separator"/>
<addaction name="actAbout"/>
</widget>
<widget class="QMenu" name="menuTools">
@ -2183,6 +2207,7 @@
<addaction name="actCopy"/>
<addaction name="actPaste"/>
<addaction name="actDelete"/>
<addaction name="actSearch"/>
<addaction name="actRename"/>
<addaction name="separator"/>
<addaction name="mnuFormat"/>
@ -2818,6 +2843,43 @@
<string>B&amp;lockquote</string>
</property>
</action>
<action name="actSearch">
<property name="icon">
<iconset theme="edit-find">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Search</string>
</property>
<property name="shortcut">
<string>Ctrl+F</string>
</property>
</action>
<action name="actSupport">
<property name="text">
<string>&amp;Technical Support</string>
</property>
<property name="toolTip">
<string>How to obtain technical support for Manuskript.</string>
</property>
<property name="shortcut">
<string>F1</string>
</property>
</action>
<action name="actLocateLog">
<property name="text">
<string>&amp;Locate log file...</string>
</property>
<property name="iconText">
<string>Locate log file</string>
</property>
<property name="toolTip">
<string>Locate the diagnostic log file used for this session.</string>
</property>
<property name="shortcut">
<string>Shift+F1</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View file

@ -1,147 +1,151 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from PyQt5.QtCore import Qt, QRect
from PyQt5.QtGui import QPalette, QFontMetrics
from PyQt5.QtWidgets import QWidget, QMenu, QAction, qApp, QListWidgetItem, QStyledItemDelegate, QStyle
from PyQt5.QtCore import Qt, QRect, QEvent, QCoreApplication
from PyQt5.QtGui import QPalette, QFontMetrics, QKeySequence
from PyQt5.QtWidgets import QWidget, qApp, QListWidgetItem, QStyledItemDelegate, QStyle, QLabel, QToolTip, QShortcut
from manuskript.enums import Outline
from manuskript.functions import mainWindow
from manuskript.ui import style
from manuskript.ui.search_ui import Ui_search
from manuskript.models import references as Ref
from manuskript.enums import Model
from manuskript.models.flatDataModelWrapper import flatDataModelWrapper
from manuskript.ui.searchMenu import searchMenu
from manuskript.ui.highlighters.searchResultHighlighters.searchResultHighlighter import searchResultHighlighter
class search(QWidget, Ui_search):
def __init__(self, parent=None):
_translate = QCoreApplication.translate
QWidget.__init__(self, parent)
self.setupUi(self)
self.options = {
"All": True,
"Title": True,
"Text": True,
"Summary": False,
"Notes": False,
"POV": False,
"Status": False,
"Label": False,
"CS": True
}
self.searchTextInput.returnPressed.connect(self.search)
self.text.returnPressed.connect(self.search)
self.generateOptionMenu()
self.searchMenu = searchMenu()
self.btnOptions.setMenu(self.searchMenu)
self.delegate = listResultDelegate(self)
self.result.setItemDelegate(self.delegate)
self.result.setMouseTracking(True)
self.result.itemClicked.connect(self.openItem)
self.result.setStyleSheet(style.searchResultSS())
self.text.setStyleSheet(style.lineEditSS())
self.searchTextInput.setStyleSheet(style.lineEditSS())
def generateOptionMenu(self):
self.menu = QMenu(self)
a = QAction(self.tr("Search in:"), self.menu)
a.setEnabled(False)
self.menu.addAction(a)
for i, d in [
(self.tr("All"), "All"),
(self.tr("Title"), "Title"),
(self.tr("Text"), "Text"),
(self.tr("Summary"), "Summary"),
(self.tr("Notes"), "Notes"),
(self.tr("POV"), "POV"),
(self.tr("Status"), "Status"),
(self.tr("Label"), "Label"),
]:
a = QAction(i, self.menu)
a.setCheckable(True)
a.setChecked(self.options[d])
a.setData(d)
a.triggered.connect(self.updateOptions)
self.menu.addAction(a)
self.menu.addSeparator()
self.searchResultHighlighter = searchResultHighlighter()
a = QAction(self.tr("Options:"), self.menu)
a.setEnabled(False)
self.menu.addAction(a)
for i, d in [
(self.tr("Case sensitive"), "CS"),
]:
a = QAction(i, self.menu)
a.setCheckable(True)
a.setChecked(self.options[d])
a.setData(d)
a.triggered.connect(self.updateOptions)
self.menu.addAction(a)
self.menu.addSeparator()
self.noResultsLabel = QLabel(_translate("Search", "No results found"), self.result)
self.noResultsLabel.setVisible(False)
self.noResultsLabel.setStyleSheet("QLabel {color: gray;}")
self.btnOptions.setMenu(self.menu)
# Add shortcuts for navigating through search results
QShortcut(QKeySequence(_translate("MainWindow", "F3")), self.searchTextInput, self.nextSearchResult)
QShortcut(QKeySequence(_translate("MainWindow", "Shift+F3")), self.searchTextInput, self.previousSearchResult)
def updateOptions(self):
a = self.sender()
self.options[a.data()] = a.isChecked()
# These texts are already included in translation files but including ":" at the end. We force here the
# translation for them without ":"
_translate("MainWindow", "Situation")
_translate("MainWindow", "Status")
def nextSearchResult(self):
if self.result.currentRow() < self.result.count() - 1:
self.result.setCurrentRow(self.result.currentRow() + 1)
else:
self.result.setCurrentRow(0)
if 0 < self.result.currentRow() < self.result.count():
self.openItem(self.result.currentItem())
def previousSearchResult(self):
if self.result.currentRow() > 0:
self.result.setCurrentRow(self.result.currentRow() - 1)
else:
self.result.setCurrentRow(self.result.count() - 1)
if 0 < self.result.currentRow() < self.result.count():
self.openItem(self.result.currentItem())
def prepareRegex(self, searchText):
import re
flags = re.UNICODE
if self.searchMenu.caseSensitive() is False:
flags |= re.IGNORECASE
if self.searchMenu.regex() is False:
searchText = re.escape(searchText)
if self.searchMenu.matchWords() is True:
# Source: https://stackoverflow.com/a/15863102
searchText = r'\b' + searchText + r'\b'
return re.compile(searchText, flags)
def search(self):
text = self.text.text()
# Choosing the right columns
lstColumns = [
("Title", Outline.title),
("Text", Outline.text),
("Summary", Outline.summarySentence),
("Summary", Outline.summaryFull),
("Notes", Outline.notes),
("POV", Outline.POV),
("Status", Outline.status),
("Label", Outline.label),
]
columns = [c[1] for c in lstColumns if self.options[c[0]] or self.options["All"]]
# Setting override cursor
qApp.setOverrideCursor(Qt.WaitCursor)
# Searching
model = mainWindow().mdlOutline
results = model.findItemsContaining(text, columns, self.options["CS"])
# Showing results
self.result.clear()
for r in results:
index = model.getIndexByID(r)
if not index.isValid():
continue
item = index.internalPointer()
i = QListWidgetItem(item.title(), self.result)
i.setData(Qt.UserRole, r)
i.setData(Qt.UserRole + 1, item.path())
self.result.addItem(i)
self.result.setCurrentRow(0)
# Removing override cursor
qApp.restoreOverrideCursor()
searchText = self.searchTextInput.text()
if len(searchText) > 0:
searchRegex = self.prepareRegex(searchText)
results = []
# Set override cursor
qApp.setOverrideCursor(Qt.WaitCursor)
for model, modelName in [
(mainWindow().mdlOutline, Model.Outline),
(mainWindow().mdlCharacter, Model.Character),
(flatDataModelWrapper(mainWindow().mdlFlatData), Model.FlatData),
(mainWindow().mdlWorld, Model.World),
(mainWindow().mdlPlots, Model.Plot)
]:
filteredColumns = self.searchMenu.columns(modelName)
# Searching
if len(filteredColumns):
results += model.searchOccurrences(searchRegex, filteredColumns)
# Showing results
self.generateResultsLists(results)
# Remove override cursor
qApp.restoreOverrideCursor()
def generateResultsLists(self, results):
self.noResultsLabel.setVisible(len(results) == 0)
for result in results:
item = QListWidgetItem(result.title(), self.result)
item.setData(Qt.UserRole, result)
item.setData(Qt.UserRole + 1, ' > '.join(result.path()))
item.setData(Qt.UserRole + 2, result.context())
self.result.addItem(item)
def openItem(self, item):
r = Ref.textReference(item.data(Qt.UserRole))
Ref.open(r)
# mw = mainWindow()
# index = mw.mdlOutline.getIndexByID(item.data(Qt.UserRole))
# mw.mainEditor.setCurrentModelIndex(index, newTab=True)
self.searchResultHighlighter.highlightSearchResult(item.data(Qt.UserRole))
def leaveEvent(self, event):
self.delegate.mouseLeave()
class listResultDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
QStyledItemDelegate.__init__(self, parent)
self._tooltipRowIndex = -1
def paint(self, painter, option, index):
extra = index.data(Qt.UserRole + 1)
if not extra:
return QStyledItemDelegate.paint(self, painter, option, index)
else:
if option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.color(QPalette.Highlight))
title = index.data()
extra = " - {}".format(extra)
painter.drawText(option.rect.adjusted(2, 1, 0, 0), Qt.AlignLeft, title)
fm = QFontMetrics(option.font)
@ -153,5 +157,18 @@ class listResultDelegate(QStyledItemDelegate):
painter.setPen(Qt.white)
else:
painter.setPen(Qt.gray)
painter.drawText(r.adjusted(2, 1, 0, 0), Qt.AlignLeft, extra)
painter.drawText(r.adjusted(2, 1, 0, 0), Qt.AlignLeft, " - {}".format(extra))
painter.restore()
def editorEvent(self, event, model, option, index):
if event.type() == QEvent.MouseMove and self._tooltipRowIndex != index.row():
self._tooltipRowIndex = index.row()
context = index.data(Qt.UserRole + 2)
extra = index.data(Qt.UserRole + 1)
QToolTip.showText(event.globalPos(),
"<p>#" + str(index.row()) + " - " + extra + "</p><p>" + context + "</p>")
return True
return False
def mouseLeave(self):
self._tooltipRowIndex = -1

108
manuskript/ui/searchMenu.py Normal file
View file

@ -0,0 +1,108 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
from PyQt5.QtWidgets import QMenu, QAction
from PyQt5.QtCore import QCoreApplication
from PyQt5 import QtCore
from manuskript.searchLabels import OutlineSearchLabels, CharacterSearchLabels, FlatDataSearchLabels, WorldSearchLabels, PlotSearchLabels
from manuskript.models.searchFilter import searchFilter
from manuskript.enums import Model
def filterKey(modelPreffix, column):
return modelPreffix + str(column)
class searchMenu(QMenu):
def __init__(self, parent=None):
QMenu.__init__(self, parent)
_translate = QCoreApplication.translate
# Model keys must match the ones used in search widget class
self.filters = {
Model.Outline: searchFilter(_translate("MainWindow", "Outline"), True, list(OutlineSearchLabels.keys())),
Model.Character: searchFilter(_translate("MainWindow", "Characters"), True, list(CharacterSearchLabels.keys())),
Model.FlatData: searchFilter(_translate("MainWindow", "FlatData"), True, list(FlatDataSearchLabels.keys())),
Model.World: searchFilter(_translate("MainWindow", "World"), True, list(WorldSearchLabels.keys())),
Model.Plot: searchFilter(_translate("MainWindow", "Plot"), True, list(PlotSearchLabels.keys()))
}
self.options = {
"CS": [self.tr("Case sensitive"), True],
"MatchWords": [self.tr("Match words"), False],
"Regex": [self.tr("Regex"), False]
}
self._generateOptions()
def _generateOptions(self):
a = QAction(self.tr("Search in:"), self)
a.setEnabled(False)
self.addAction(a)
for filterKey in self.filters:
a = QAction(self.tr(self.filters[filterKey].label()), self)
a.setCheckable(True)
a.setChecked(self.filters[filterKey].enabled())
a.setData(filterKey)
a.triggered.connect(self._updateFilters)
self.addAction(a)
self.addSeparator()
a = QAction(self.tr("Options:"), self)
a.setEnabled(False)
self.addAction(a)
for optionKey in self.options:
a = QAction(self.options[optionKey][0], self)
a.setCheckable(True)
a.setChecked(self.options[optionKey][1])
a.setData(optionKey)
a.triggered.connect(self._updateOptions)
self.addAction(a)
self.addSeparator()
def _updateFilters(self):
a = self.sender()
self.filters[a.data()].setEnabled(a.isChecked())
def _updateOptions(self):
a = self.sender()
self.options[a.data()][1] = a.isChecked()
def columns(self, modelName):
if self.filters[modelName].enabled():
return self.filters[modelName].modelColumns()
else:
return []
def caseSensitive(self):
return self.options["CS"][1]
def matchWords(self):
return self.options["MatchWords"][1]
def regex(self):
return self.options["Regex"][1]
def mouseReleaseEvent(self, event):
# Workaround for enabling / disabling actions without closing the menu.
# Source: https://stackoverflow.com/a/14967212
action = self.activeAction()
if action:
action.setEnabled(False)
QMenu.mouseReleaseEvent(self, event)
action.setEnabled(True)
action.trigger()
else:
QMenu.mouseReleaseEvent(self, event)
def keyPressEvent(self, event):
# Workaround for enabling / disabling actions without closing the menu.
# Source: https://stackoverflow.com/a/14967212
action = self.activeAction()
if action and event.key() == QtCore.Qt.Key_Return:
action.setEnabled(False)
QMenu.keyPressEvent(self, event)
action.setEnabled(True)
action.trigger()
else:
QMenu.keyPressEvent(self, event)

View file

@ -19,12 +19,12 @@ class Ui_search(object):
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setSpacing(0)
self.horizontalLayout.setObjectName("horizontalLayout")
self.text = QtWidgets.QLineEdit(search)
self.text.setInputMask("")
self.text.setFrame(False)
self.text.setClearButtonEnabled(True)
self.text.setObjectName("text")
self.horizontalLayout.addWidget(self.text)
self.searchTextInput = QtWidgets.QLineEdit(search)
self.searchTextInput.setInputMask("")
self.searchTextInput.setFrame(False)
self.searchTextInput.setClearButtonEnabled(True)
self.searchTextInput.setObjectName("searchTextInput")
self.horizontalLayout.addWidget(self.searchTextInput)
self.btnOptions = QtWidgets.QPushButton(search)
self.btnOptions.setText("")
icon = QtGui.QIcon.fromTheme("edit-find")
@ -45,5 +45,5 @@ class Ui_search(object):
def retranslateUi(self, search):
_translate = QtCore.QCoreApplication.translate
search.setWindowTitle(_translate("search", "Form"))
self.text.setPlaceholderText(_translate("search", "Search for..."))
self.searchTextInput.setPlaceholderText(_translate("search", "Search for..."))

View file

@ -35,7 +35,7 @@
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="text">
<widget class="QLineEdit" name="searchTextInput">
<property name="inputMask">
<string/>
</property>

View file

@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'manuskript/ui/settings_ui.ui'
# Form implementation generated from reading ui file 'settings_ui.ui'
#
# Created by: PyQt5 UI code generator 5.13.0
# Created by: PyQt5 UI code generator 5.15.0
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
@ -13,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Settings(object):
def setupUi(self, Settings):
Settings.setObjectName("Settings")
Settings.resize(658, 598)
Settings.resize(681, 598)
self.horizontalLayout_8 = QtWidgets.QHBoxLayout(Settings)
self.horizontalLayout_8.setObjectName("horizontalLayout_8")
self.lstMenu = QtWidgets.QListWidget(Settings)
@ -55,50 +56,9 @@ class Ui_Settings(object):
self.groupBox_2.setFont(font)
self.groupBox_2.setObjectName("groupBox_2")
self.formLayout_13 = QtWidgets.QFormLayout(self.groupBox_2)
self.formLayout_13.setFieldGrowthPolicy(QtWidgets.QFormLayout.FieldsStayAtSizeHint)
self.formLayout_13.setObjectName("formLayout_13")
self.label_56 = QtWidgets.QLabel(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.label_56.setFont(font)
self.label_56.setObjectName("label_56")
self.formLayout_13.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_56)
self.cmbStyle = QtWidgets.QComboBox(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.cmbStyle.setFont(font)
self.cmbStyle.setObjectName("cmbStyle")
self.formLayout_13.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.cmbStyle)
self.label_57 = QtWidgets.QLabel(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.label_57.setFont(font)
self.label_57.setObjectName("label_57")
self.formLayout_13.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.label_57)
self.cmbTranslation = QtWidgets.QComboBox(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.cmbTranslation.setFont(font)
self.cmbTranslation.setObjectName("cmbTranslation")
self.formLayout_13.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.cmbTranslation)
self.label_58 = QtWidgets.QLabel(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.label_58.setFont(font)
self.label_58.setObjectName("label_58")
self.formLayout_13.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.label_58)
self.spnGeneralFontSize = QtWidgets.QSpinBox(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.spnGeneralFontSize.setFont(font)
self.spnGeneralFontSize.setObjectName("spnGeneralFontSize")
self.formLayout_13.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.spnGeneralFontSize)
self.gridLayout_4 = QtWidgets.QGridLayout()
self.gridLayout_4.setObjectName("gridLayout_4")
self.label_2 = QtWidgets.QLabel(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
@ -106,7 +66,70 @@ class Ui_Settings(object):
self.label_2.setFont(font)
self.label_2.setWordWrap(True)
self.label_2.setObjectName("label_2")
self.formLayout_13.setWidget(2, QtWidgets.QFormLayout.SpanningRole, self.label_2)
self.gridLayout_4.addWidget(self.label_2, 0, 0, 1, 1)
self.horizontalLayout_12 = QtWidgets.QHBoxLayout()
self.horizontalLayout_12.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
self.horizontalLayout_12.setObjectName("horizontalLayout_12")
self.formLayout_14 = QtWidgets.QFormLayout()
self.formLayout_14.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow)
self.formLayout_14.setObjectName("formLayout_14")
self.label_56 = QtWidgets.QLabel(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.label_56.setFont(font)
self.label_56.setObjectName("label_56")
self.formLayout_14.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label_56)
self.cmbStyle = QtWidgets.QComboBox(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.cmbStyle.setFont(font)
self.cmbStyle.setObjectName("cmbStyle")
self.formLayout_14.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.cmbStyle)
self.label_57 = QtWidgets.QLabel(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.label_57.setFont(font)
self.label_57.setObjectName("label_57")
self.formLayout_14.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_57)
self.cmbTranslation = QtWidgets.QComboBox(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.cmbTranslation.setFont(font)
self.cmbTranslation.setObjectName("cmbTranslation")
self.formLayout_14.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.cmbTranslation)
self.label_58 = QtWidgets.QLabel(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.label_58.setFont(font)
self.label_58.setObjectName("label_58")
self.formLayout_14.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_58)
self.spnGeneralFontSize = QtWidgets.QSpinBox(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.spnGeneralFontSize.setFont(font)
self.spnGeneralFontSize.setObjectName("spnGeneralFontSize")
self.formLayout_14.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.spnGeneralFontSize)
self.horizontalLayout_12.addLayout(self.formLayout_14)
self.formLayout_15 = QtWidgets.QFormLayout()
self.formLayout_15.setObjectName("formLayout_15")
self.chkProgressChars = QtWidgets.QCheckBox(self.groupBox_2)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.chkProgressChars.setFont(font)
self.chkProgressChars.setObjectName("chkProgressChars")
self.formLayout_15.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.chkProgressChars)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.formLayout_15.setItem(0, QtWidgets.QFormLayout.LabelRole, spacerItem)
self.horizontalLayout_12.addLayout(self.formLayout_15)
self.gridLayout_4.addLayout(self.horizontalLayout_12, 1, 0, 1, 1)
self.formLayout_13.setLayout(0, QtWidgets.QFormLayout.SpanningRole, self.gridLayout_4)
self.verticalLayout_7.addWidget(self.groupBox_2)
self.groupBox_10 = QtWidgets.QGroupBox(self.stackedWidgetPage1)
font = QtGui.QFont()
@ -166,8 +189,8 @@ class Ui_Settings(object):
self.label.setFont(font)
self.label.setObjectName("label")
self.horizontalLayout_5.addWidget(self.label)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_5.addItem(spacerItem)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_5.addItem(spacerItem1)
self.verticalLayout_6.addLayout(self.horizontalLayout_5)
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
@ -202,8 +225,8 @@ class Ui_Settings(object):
self.label_14.setFont(font)
self.label_14.setObjectName("label_14")
self.horizontalLayout_7.addWidget(self.label_14)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_7.addItem(spacerItem1)
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_7.addItem(spacerItem2)
self.verticalLayout_6.addLayout(self.horizontalLayout_7)
self.chkSaveOnQuit = QtWidgets.QCheckBox(self.groupBox)
font = QtGui.QFont()
@ -223,8 +246,8 @@ class Ui_Settings(object):
self.chkSaveToZip.setObjectName("chkSaveToZip")
self.verticalLayout_6.addWidget(self.chkSaveToZip)
self.verticalLayout_7.addWidget(self.groupBox)
spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_7.addItem(spacerItem2)
spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_7.addItem(spacerItem3)
self.stack.addWidget(self.stackedWidgetPage1)
self.page_3 = QtWidgets.QWidget()
self.page_3.setObjectName("page_3")
@ -388,8 +411,8 @@ class Ui_Settings(object):
self.label_51.setObjectName("label_51")
self.gridLayout_2.addWidget(self.label_51, 6, 1, 1, 1)
self.verticalLayout.addWidget(self.chkRevisionRemove)
spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem3)
spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem4)
self.label_revisionDeprecation = QtWidgets.QLabel(self.page_3)
self.label_revisionDeprecation.setWordWrap(True)
self.label_revisionDeprecation.setOpenExternalLinks(True)
@ -524,6 +547,25 @@ class Ui_Settings(object):
self.sldTreeIconSize.setObjectName("sldTreeIconSize")
self.horizontalLayout_11.addWidget(self.sldTreeIconSize)
self.verticalLayout_17.addWidget(self.groupBox_16)
self.horizontalGroupBox = QtWidgets.QGroupBox(self.tab)
font = QtGui.QFont()
font.setBold(True)
font.setWeight(75)
self.horizontalGroupBox.setFont(font)
self.horizontalGroupBox.setObjectName("horizontalGroupBox")
self.horizontalLayout_13 = QtWidgets.QHBoxLayout(self.horizontalGroupBox)
self.horizontalLayout_13.setContentsMargins(9, 9, 9, 9)
self.horizontalLayout_13.setObjectName("horizontalLayout_13")
self.chkCountSpaces = QtWidgets.QCheckBox(self.horizontalGroupBox)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.chkCountSpaces.setFont(font)
self.chkCountSpaces.setObjectName("chkCountSpaces")
self.horizontalLayout_13.addWidget(self.chkCountSpaces)
spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_13.addItem(spacerItem5)
self.verticalLayout_17.addWidget(self.horizontalGroupBox)
self.horizontalLayout_9 = QtWidgets.QHBoxLayout()
self.horizontalLayout_9.setObjectName("horizontalLayout_9")
self.groupBox_8 = QtWidgets.QGroupBox(self.tab)
@ -548,6 +590,13 @@ class Ui_Settings(object):
self.rdoTreeWC.setFont(font)
self.rdoTreeWC.setObjectName("rdoTreeWC")
self.verticalLayout_15.addWidget(self.rdoTreeWC)
self.rdoTreeCC = QtWidgets.QRadioButton(self.groupBox_8)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.rdoTreeCC.setFont(font)
self.rdoTreeCC.setObjectName("rdoTreeCC")
self.verticalLayout_15.addWidget(self.rdoTreeCC)
self.rdoTreeProgress = QtWidgets.QRadioButton(self.groupBox_8)
font = QtGui.QFont()
font.setBold(False)
@ -586,6 +635,13 @@ class Ui_Settings(object):
self.rdoTreeTextWC.setFont(font)
self.rdoTreeTextWC.setObjectName("rdoTreeTextWC")
self.verticalLayout_16.addWidget(self.rdoTreeTextWC)
self.rdoTreeTextCC = QtWidgets.QRadioButton(self.groupBox_9)
font = QtGui.QFont()
font.setBold(False)
font.setWeight(50)
self.rdoTreeTextCC.setFont(font)
self.rdoTreeTextCC.setObjectName("rdoTreeTextCC")
self.verticalLayout_16.addWidget(self.rdoTreeTextCC)
self.rdoTreeTextProgress = QtWidgets.QRadioButton(self.groupBox_9)
font = QtGui.QFont()
font.setBold(False)
@ -607,12 +663,17 @@ class Ui_Settings(object):
self.rdoTreeTextNothing.setFont(font)
self.rdoTreeTextNothing.setObjectName("rdoTreeTextNothing")
self.verticalLayout_16.addWidget(self.rdoTreeTextNothing)
spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_16.addItem(spacerItem4)
spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_16.addItem(spacerItem6)
self.rdoTreeTextCC.raise_()
self.rdoTreeTextWC.raise_()
self.rdoTreeTextProgress.raise_()
self.rdoTreeTextSummary.raise_()
self.rdoTreeTextNothing.raise_()
self.horizontalLayout_9.addWidget(self.groupBox_9)
self.verticalLayout_17.addLayout(self.horizontalLayout_9)
spacerItem5 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_17.addItem(spacerItem5)
spacerItem7 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_17.addItem(spacerItem7)
icon = QtGui.QIcon.fromTheme("view-list-tree")
self.tabViews.addTab(self.tab, icon, "")
self.tab_2 = QtWidgets.QWidget()
@ -774,8 +835,8 @@ class Ui_Settings(object):
self.chkOutlineTitle.setObjectName("chkOutlineTitle")
self.gridLayout.addWidget(self.chkOutlineTitle, 3, 0, 1, 1)
self.verticalLayout_11.addWidget(self.groupBox_6)
spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_11.addItem(spacerItem6)
spacerItem8 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_11.addItem(spacerItem8)
icon = QtGui.QIcon.fromTheme("view-outline")
self.tabViews.addTab(self.tab_2, icon, "")
self.tab_3 = QtWidgets.QWidget()
@ -821,8 +882,8 @@ class Ui_Settings(object):
self.cmbCorkImage.setFont(font)
self.cmbCorkImage.setObjectName("cmbCorkImage")
self.verticalLayout_8.addWidget(self.cmbCorkImage)
spacerItem7 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_8.addItem(spacerItem7)
spacerItem9 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_8.addItem(spacerItem9)
self.gridLayout_3.addWidget(self.groupBox_7, 1, 1, 1, 1)
self.groupBox_11 = QtWidgets.QGroupBox(self.tab_3)
font = QtGui.QFont()
@ -1380,8 +1441,8 @@ class Ui_Settings(object):
self.btnLabelColor.setIconSize(QtCore.QSize(64, 64))
self.btnLabelColor.setObjectName("btnLabelColor")
self.verticalLayout_2.addWidget(self.btnLabelColor)
spacerItem8 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_2.addItem(spacerItem8)
spacerItem10 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_2.addItem(spacerItem10)
self.horizontalLayout_2.addLayout(self.verticalLayout_2)
self.verticalLayout_3.addLayout(self.horizontalLayout_2)
self.horizontalLayout = QtWidgets.QHBoxLayout()
@ -1398,8 +1459,8 @@ class Ui_Settings(object):
self.btnLabelRemove.setIcon(icon)
self.btnLabelRemove.setObjectName("btnLabelRemove")
self.horizontalLayout.addWidget(self.btnLabelRemove)
spacerItem9 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem9)
spacerItem11 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem11)
self.verticalLayout_3.addLayout(self.horizontalLayout)
self.stack.addWidget(self.stackedWidgetPage3)
self.stackedWidgetPage4 = QtWidgets.QWidget()
@ -1433,8 +1494,8 @@ class Ui_Settings(object):
self.btnStatusRemove.setIcon(icon)
self.btnStatusRemove.setObjectName("btnStatusRemove")
self.horizontalLayout_3.addWidget(self.btnStatusRemove)
spacerItem10 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_3.addItem(spacerItem10)
spacerItem12 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_3.addItem(spacerItem12)
self.verticalLayout_4.addLayout(self.horizontalLayout_3)
self.stack.addWidget(self.stackedWidgetPage4)
self.page = QtWidgets.QWidget()
@ -1482,8 +1543,8 @@ class Ui_Settings(object):
self.btnThemeRemove.setIcon(icon)
self.btnThemeRemove.setObjectName("btnThemeRemove")
self.horizontalLayout_6.addWidget(self.btnThemeRemove)
spacerItem11 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_6.addItem(spacerItem11)
spacerItem13 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_6.addItem(spacerItem13)
self.verticalLayout_12.addLayout(self.horizontalLayout_6)
self.themeStack.addWidget(self.stackedWidgetPage1_3)
self.stackedWidgetPage2_3 = QtWidgets.QWidget()
@ -1823,9 +1884,9 @@ class Ui_Settings(object):
self.horizontalLayout_8.addWidget(self.stack)
self.retranslateUi(Settings)
self.stack.setCurrentIndex(2)
self.tabViews.setCurrentIndex(3)
self.themeStack.setCurrentIndex(1)
self.stack.setCurrentIndex(0)
self.tabViews.setCurrentIndex(0)
self.themeStack.setCurrentIndex(0)
self.themeEditStack.setCurrentIndex(3)
self.lstMenu.currentRowChanged['int'].connect(self.stack.setCurrentIndex)
self.chkRevisionsKeep.toggled['bool'].connect(self.chkRevisionRemove.setEnabled)
@ -1851,10 +1912,12 @@ class Ui_Settings(object):
self.lstMenu.setSortingEnabled(__sortingEnabled)
self.lblTitleGeneral.setText(_translate("Settings", "General settings"))
self.groupBox_2.setTitle(_translate("Settings", "Application settings"))
self.label_2.setText(_translate("Settings", "Restarting Manuskript ensures all settings take effect."))
self.label_56.setText(_translate("Settings", "Style:"))
self.label_57.setText(_translate("Settings", "Language:"))
self.label_58.setText(_translate("Settings", "Font size:"))
self.label_2.setText(_translate("Settings", "Restarting Manuskript ensures all settings take effect."))
self.chkProgressChars.setText(_translate("Settings", "Show progress in chars next\n"
" to words"))
self.groupBox_10.setTitle(_translate("Settings", "Loading"))
self.chkAutoLoad.setText(_translate("Settings", "Automatically load last project on startup"))
self.groupBox.setTitle(_translate("Settings", "Saving"))
@ -1899,14 +1962,18 @@ class Ui_Settings(object):
self.cmbTreeBackground.setItemText(4, _translate("Settings", "Compile"))
self.groupBox_16.setTitle(_translate("Settings", "Icon Size"))
self.lblTreeIconSize.setText(_translate("Settings", "TextLabel"))
self.horizontalGroupBox.setTitle(_translate("Settings", "Char/Word Counter"))
self.chkCountSpaces.setText(_translate("Settings", "Count spaces as chars"))
self.groupBox_8.setTitle(_translate("Settings", "Folders"))
self.rdoTreeItemCount.setText(_translate("Settings", "Show ite&m count"))
self.rdoTreeWC.setText(_translate("Settings", "Show &word count"))
self.rdoTreeCC.setText(_translate("Settings", "Show char c&ount"))
self.rdoTreeProgress.setText(_translate("Settings", "S&how progress"))
self.rdoTreeSummary.setText(_translate("Settings", "Show summar&y"))
self.rdoTreeNothing.setText(_translate("Settings", "&Nothing"))
self.groupBox_9.setTitle(_translate("Settings", "Text"))
self.rdoTreeTextWC.setText(_translate("Settings", "&Show word count"))
self.rdoTreeTextCC.setText(_translate("Settings", "Sho&w char count"))
self.rdoTreeTextProgress.setText(_translate("Settings", "Show p&rogress"))
self.rdoTreeTextSummary.setText(_translate("Settings", "Show summary"))
self.rdoTreeTextNothing.setText(_translate("Settings", "Nothing"))

View file

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>658</width>
<width>681</width>
<height>598</height>
</rect>
</property>
@ -54,7 +54,7 @@
<item>
<widget class="QStackedWidget" name="stack">
<property name="currentIndex">
<number>2</number>
<number>0</number>
</property>
<widget class="QWidget" name="stackedWidgetPage1">
<layout class="QVBoxLayout" name="verticalLayout_7">
@ -98,93 +98,139 @@
<string>Application settings</string>
</property>
<layout class="QFormLayout" name="formLayout_13">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::FieldsStayAtSizeHint</enum>
</property>
<item row="3" column="0">
<widget class="QLabel" name="label_56">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Style:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="cmbStyle">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_57">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Language:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="cmbTranslation">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_58">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Font size:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QSpinBox" name="spnGeneralFontSize">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Restarting Manuskript ensures all settings take effect.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Restarting Manuskript ensures all settings take effect.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_12">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<layout class="QFormLayout" name="formLayout_14">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_56">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Style:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="cmbStyle">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_57">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Language:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="cmbTranslation">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_58">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Font size:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="spnGeneralFontSize">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QFormLayout" name="formLayout_15">
<item row="0" column="1">
<widget class="QCheckBox" name="chkProgressChars">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Show progress in chars next
to words</string>
</property>
</widget>
</item>
<item row="0" column="0">
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
@ -817,7 +863,7 @@
<item>
<widget class="QTabWidget" name="tabViews">
<property name="currentIndex">
<number>3</number>
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="icon">
@ -1055,6 +1101,59 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="horizontalGroupBox">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="title">
<string>Char/Word Counter</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_13">
<property name="leftMargin">
<number>9</number>
</property>
<property name="topMargin">
<number>9</number>
</property>
<property name="rightMargin">
<number>9</number>
</property>
<property name="bottomMargin">
<number>9</number>
</property>
<item>
<widget class="QCheckBox" name="chkCountSpaces">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Count spaces as chars</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
@ -1095,6 +1194,19 @@
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="rdoTreeCC">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Show char c&amp;ount</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="rdoTreeProgress">
<property name="font">
@ -1165,6 +1277,19 @@
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="rdoTreeTextCC">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Sho&amp;w char count</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="rdoTreeTextProgress">
<property name="font">
@ -1224,6 +1349,11 @@
</spacer>
</item>
</layout>
<zorder>rdoTreeTextCC</zorder>
<zorder>rdoTreeTextWC</zorder>
<zorder>rdoTreeTextProgress</zorder>
<zorder>rdoTreeTextSummary</zorder>
<zorder>rdoTreeTextNothing</zorder>
</widget>
</item>
</layout>
@ -2974,7 +3104,7 @@
<item>
<widget class="QStackedWidget" name="themeStack">
<property name="currentIndex">
<number>1</number>
<number>0</number>
</property>
<widget class="QWidget" name="stackedWidgetPage1_3">
<layout class="QVBoxLayout" name="verticalLayout_12">

View file

@ -106,13 +106,18 @@ class MDEditCompleter(MDEditView):
self.completer.popup(self.textUnderCursor(select=True))
def mouseMoveEvent(self, event):
"""
When mouse moves, we show tooltip when appropriate.
"""
self.beginTooltipMoveEvent()
MDEditView.mouseMoveEvent(self, event)
self.endTooltipMoveEvent()
onRef = [r for r in self.refRects if r.contains(event.pos())]
if not onRef:
qApp.restoreOverrideCursor()
QToolTip.hideText()
self.hideTooltip()
return
cursor = self.cursorForPosition(event.pos())
@ -120,7 +125,8 @@ class MDEditCompleter(MDEditView):
if ref:
if not qApp.overrideCursor():
qApp.setOverrideCursor(Qt.PointingHandCursor)
QToolTip.showText(self.mapToGlobal(event.pos()), Ref.tooltip(ref))
self.showTooltip(self.mapToGlobal(event.pos()), Ref.tooltip(ref))
def mouseReleaseEvent(self, event):
MDEditView.mouseReleaseEvent(self, event)

View file

@ -14,6 +14,8 @@ from manuskript.ui.highlighters.markdownEnums import MarkdownState as MS
from manuskript.ui.highlighters.markdownTokenizer import MarkdownTokenizer as MT
from manuskript import functions as F
import logging
LOGGER = logging.getLogger(__name__)
class MDEditView(textEditView):
@ -506,13 +508,15 @@ class MDEditView(textEditView):
"""
When mouse moves, we show tooltip when appropriate.
"""
self.beginTooltipMoveEvent()
textEditView.mouseMoveEvent(self, event)
self.endTooltipMoveEvent()
onRect = [r for r in self.clickRects if r.rect.contains(event.pos())]
if not onRect:
qApp.restoreOverrideCursor()
QToolTip.hideText()
self.hideTooltip()
return
ct = onRect[0]
@ -534,7 +538,7 @@ class MDEditView(textEditView):
if tooltip:
tooltip = self.tr("{} (CTRL+Click to open)").format(tooltip)
QToolTip.showText(self.mapToGlobal(event.pos()), tooltip)
self.showTooltip(self.mapToGlobal(event.pos()), tooltip)
def mouseReleaseEvent(self, event):
textEditView.mouseReleaseEvent(self, event)
@ -658,10 +662,10 @@ class ImageTooltip:
return
else:
# Somehow we lost track. Log what we can to hopefully figure it out.
print("Warning: unable to match fetched data for tooltip to original request.")
print("- Completed request:", url_key)
print("- Status upon finishing:", reply.error(), reply.errorString())
print("- Currently processing:", ImageTooltip.processing)
LOGGER.warning("Unable to match fetched data for tooltip to original request.")
LOGGER.warning("- Completed request: %s", url_key)
LOGGER.warning("- Status upon finishing: %s, %s", reply.error(), reply.errorString())
LOGGER.warning("- Currently processing: %s", ImageTooltip.processing)
return
# Update cache with retrieved data.

View file

@ -29,6 +29,8 @@ class characterTreeView(QTreeWidget):
self._rootItem = QTreeWidgetItem()
self.insertTopLevelItem(0, self._rootItem)
self.importanceMap = {self.tr("Main"):2, self.tr("Secondary"):1, self.tr("Minor"):0}
def setCharactersModel(self, model):
self._model = model
self._model.dataChanged.connect(self.updateMaybe)
@ -64,7 +66,7 @@ class characterTreeView(QTreeWidget):
for child in range(item.childCount()):
sub = item.child(child)
ID = sub.data(0, Qt.UserRole)
if ID is not None:
if ID != None:
# Update name
c = self._model.getCharacterByID(ID)
name = c.name()
@ -86,11 +88,9 @@ class characterTreeView(QTreeWidget):
self.clear()
characters = self._model.getCharactersByImportance()
h = [self.tr("Main"), self.tr("Secondary"), self.tr("Minor")]
for i in range(3):
for i, importanceLevel in enumerate(self.importanceMap):
# Create category item
cat = QTreeWidgetItem(self, [h[i]])
cat = QTreeWidgetItem(self, [importanceLevel])
cat.setBackground(0, QBrush(QColor(S.highlightLight)))
cat.setForeground(0, QBrush(QColor(S.highlightedTextDark)))
cat.setTextAlignment(0, Qt.AlignCenter)
@ -119,6 +119,24 @@ class characterTreeView(QTreeWidget):
self.expandAll()
self._updating = False
def addCharacter(self):
curr_item = self.currentItem()
curr_importance = 0
# check if an item is selected
if curr_item != None:
if curr_item.parent() == None:
# this is a top-level category, so find its importance
# get the current text, then look up the importance level
text = curr_item.text(0)
curr_importance = self.importanceMap[text]
else:
# get the importance from the currently-highlighted character
curr_character = self.currentCharacter()
curr_importance = curr_character.importance()
self._model.addCharacter(importance=curr_importance)
def removeCharacter(self):
"""
Removes selected character.
@ -130,22 +148,30 @@ class characterTreeView(QTreeWidget):
def choseCharacterColor(self):
ID = self.currentCharacterID()
c = self._model.getCharacterByID(ID)
if c:
color = iconColor(c.icon)
else:
color = Qt.white
self.colorDialog = QColorDialog(color, mainWindow())
color = self.colorDialog.getColor(color)
if color.isValid():
c.setColor(color)
mainWindow().updateCharacterColor(ID)
def changeCharacterPOVState(self, state):
ID = self.currentCharacterID()
c = self._model.getCharacterByID(ID)
c.setPOVEnabled(state == Qt.Checked)
mainWindow().updateCharacterPOVState(ID)
def addCharacterInfo(self):
self._model.addCharacterInfo(self.currentCharacterID())
def removeCharacterInfo(self):
self._model.removeCharacterInfo(self.currentCharacterID(),
)
self._model.removeCharacterInfo(self.currentCharacterID())
def currentCharacterID(self):
ID = None

View file

@ -43,11 +43,19 @@ class corkDelegate(QStyledItemDelegate):
return QStyledItemDelegate.editorEvent(self, event, model, option, index)
def createEditor(self, parent, option, index):
# When the user performs a global search and selects an Outline result (title or summary), the
# associated chapter is selected in cork view, triggering a call to this method with the results
# list widget set in self.sender(). In this case we store the searched column so we know which
# editor should be created.
searchedColumn = None
if self.sender() is not None and self.sender().objectName() == 'result' and self.sender().currentItem():
searchedColumn = self.sender().currentItem().data(Qt.UserRole).column()
self.updateRects(option, index)
bgColor = self.bgColors.get(index, "white")
if self.mainLineRect.contains(self.lastPos):
if searchedColumn == Outline.summarySentence or (self.lastPos is not None and self.mainLineRect.contains(self.lastPos)):
# One line summary
self.editing = Outline.summarySentence
edt = QLineEdit(parent)
@ -64,7 +72,7 @@ class corkDelegate(QStyledItemDelegate):
edt.setStyleSheet("background: {}; color: black;".format(bgColor))
return edt
elif self.titleRect.contains(self.lastPos):
elif searchedColumn == Outline.title or (self.lastPos is not None and self.titleRect.contains(self.lastPos)):
# Title
self.editing = Outline.title
edt = QLineEdit(parent)

View file

@ -27,6 +27,8 @@ class corkView(QListView, dndView, outlineBasics):
def updateBackground(self):
if settings.corkBackground["image"] != "":
img = findBackground(settings.corkBackground["image"])
if img == None:
img = ""
else:
# No background image
img = ""

View file

@ -13,7 +13,6 @@ class dndView(QAbstractItemView):
def dragMoveEvent(self, event):
# return QAbstractItemView.dragMoveEvent(self, event)
# print(a)
if event.keyboardModifiers() & Qt.ControlModifier:
event.setDropAction(Qt.CopyAction)
else:

View file

@ -31,7 +31,7 @@ class lineEditView(QLineEdit):
self._index = index
self._model = index.model()
# self.item = index.internalPointer()
if self._placeholderText is not None:
if self._placeholderText != None:
self.setPlaceholderText(self._placeholderText)
self.textEdited.connect(self.submit)
self.updateText()

View file

@ -313,7 +313,7 @@ class outlineLabelDelegate(QStyledItemDelegate):
idx = self.mdlLabels.indexFromItem(item)
opt = QStyleOptionViewItem(option)
self.initStyleOption(opt, idx)
s = qApp.style().sizeFromContents(QStyle.CT_ItemViewItem, opt, QSize())
s = qApp.style().sizeFromContents(QStyle.CT_ItemViewItem, opt, QSize(), None)
if s.width() > 150:
s.setWidth(150)
elif s.width() < 50:

View file

@ -5,7 +5,10 @@ from PyQt5.QtGui import QIntValidator
from manuskript.enums import Outline
from manuskript.ui.views.propertiesView_ui import Ui_propertiesView
from manuskript.models.characterPOVModel import characterPOVModel
import logging
LOGGER = logging.getLogger(__name__)
class propertiesView(QWidget, Ui_propertiesView):
def __init__(self, parent=None):
@ -14,7 +17,7 @@ class propertiesView(QWidget, Ui_propertiesView):
self.txtGoal.setColumn(Outline.setGoal)
def setModels(self, mdlOutline, mdlCharacter, mdlLabels, mdlStatus):
self.cmbPOV.setModels(mdlCharacter, mdlOutline)
self.cmbPOV.setModels(characterPOVModel(mdlCharacter), mdlOutline)
self.cmbLabel.setModels(mdlLabels, mdlOutline)
self.cmbStatus.setModels(mdlStatus, mdlOutline)
self.chkCompile.setModel(mdlOutline)
@ -38,7 +41,7 @@ class propertiesView(QWidget, Ui_propertiesView):
def selectionChanged(self, sourceView):
indexes = self.getIndexes(sourceView)
# print(indexes)
# LOGGER.debug("selectionChanged indexes: %s", indexes)
if len(indexes) == 0:
self.setEnabled(False)

View file

@ -1,11 +1,11 @@
#!/usr/bin/env python
# --!-- coding: utf8 --!--
import re
import re, textwrap
from PyQt5.Qt import QApplication
from PyQt5.QtCore import QTimer, QModelIndex, Qt, QEvent, pyqtSignal, QRegExp, QLocale, QPersistentModelIndex, QMutex
from PyQt5.QtGui import QTextBlockFormat, QTextCharFormat, QFont, QColor, QIcon, QMouseEvent, QTextCursor
from PyQt5.QtWidgets import QWidget, QTextEdit, qApp, QAction, QMenu
from PyQt5.QtWidgets import QWidget, QTextEdit, qApp, QAction, QMenu, QToolTip
from manuskript import settings
from manuskript.enums import Outline, World, Character, Plot
@ -14,6 +14,11 @@ from manuskript.models import outlineModel, outlineItem
from manuskript.ui.highlighters import BasicHighlighter
from manuskript.ui import style as S
from manuskript.functions import Spellchecker
from manuskript.models.characterModel import Character, CharacterInfo
import logging
LOGGER = logging.getLogger(__name__)
class textEditView(QTextEdit):
def __init__(self, parent=None, index=None, html=None, spellcheck=None,
@ -34,7 +39,7 @@ class textEditView(QTextEdit):
self._themeData = None
self._highlighterClass = BasicHighlighter
if spellcheck is None:
if spellcheck == None:
spellcheck = settings.spellcheck
self.spellcheck = spellcheck
@ -47,6 +52,8 @@ class textEditView(QTextEdit):
self.highlightWord = ""
self.highligtCS = False
self._dict = None
self._tooltip = { 'depth' : 0, 'active' : 0 }
# self.document().contentsChanged.connect(self.submit, F.AUC)
# Submit text changed only after 500ms without modifications
@ -54,13 +61,13 @@ class textEditView(QTextEdit):
self.updateTimer.setInterval(500)
self.updateTimer.setSingleShot(True)
self.updateTimer.timeout.connect(self.submit)
# self.updateTimer.timeout.connect(lambda: print("Timeout"))
# self.updateTimer.timeout.connect(lambda: LOGGER.debug("Timeout."))
self.updateTimer.stop()
self.document().contentsChanged.connect(self.updateTimer.start, F.AUC)
# self.document().contentsChanged.connect(lambda: print("Document changed"))
# self.document().contentsChanged.connect(lambda: LOGGER.debug("Document changed."))
# self.document().contentsChanged.connect(lambda: print(self.objectName(), "Contents changed"))
# self.document().contentsChanged.connect(lambda: LOGGER.debug("Contents changed: %s", self.objectName()))
self.setEnabled(False)
@ -163,9 +170,9 @@ class textEditView(QTextEdit):
def loadFontSettings(self):
if self._fromTheme or \
not self._index or \
type(self._index.model()) != outlineModel or \
self._column != Outline.text:
not self._index or \
type(self._index.model()) != outlineModel or \
self._column != Outline.text:
return
opt = settings.textEditor
@ -173,7 +180,7 @@ class textEditView(QTextEdit):
f.fromString(opt["font"])
background = (opt["background"] if not opt["backgroundTransparent"]
else "transparent")
foreground = opt["fontColor"] # if not opt["backgroundTransparent"]
foreground = opt["fontColor"] # if not opt["backgroundTransparent"]
# else S.text
# self.setFont(f)
self.setStyleSheet("""QTextEdit{{
@ -185,15 +192,16 @@ class textEditView(QTextEdit):
{maxWidth}
}}
""".format(
bg=background,
foreground=foreground,
ff=f.family(),
fs="{}pt".format(str(f.pointSize())),
mTB = opt["marginsTB"],
mLR = opt["marginsLR"],
maxWidth = "max-width: {}px;".format(opt["maxWidth"]) if opt["maxWidth"] else "",
)
)
bg=background,
foreground=foreground,
ff=f.family(),
fs="{}pt".format(str(f.pointSize())),
mTB=opt["marginsTB"],
mLR=opt["marginsLR"],
maxWidth="max-width: {}px;".format(
opt["maxWidth"]) if opt["maxWidth"] else "",
)
)
self._defaultFontSize = f.pointSize()
# We set the parent background to the editor's background in case
@ -205,11 +213,11 @@ class textEditView(QTextEdit):
QWidget#{name}{{
background: {bg};
}}""".format(
# We style by name, otherwise all inheriting widgets get the same
# colored background, for example context menu.
name=self.parent().objectName(),
bg=background,
))
# We style by name, otherwise all inheriting widgets get the same
# colored background, for example context menu.
name=self.parent().objectName(),
bg=background,
))
cf = QTextCharFormat()
# cf.setFont(f)
@ -243,7 +251,7 @@ class textEditView(QTextEdit):
if topLeft.parent() != self._index.parent():
return
# print("Model changed: ({}:{}), ({}:{}/{}), ({}:{}) for {} of {}".format(
# LOGGER.debug("Model changed: ({}:{}), ({}:{}/{}), ({}:{}) for {} of {}".format(
# topLeft.row(), topLeft.column(),
# self._index.row(), self._index.row(), self._column,
# bottomRight.row(), bottomRight.column(),
@ -273,11 +281,11 @@ class textEditView(QTextEdit):
def updateText(self):
self._updating.lock()
# print("Updating", self.objectName())
# LOGGER.debug("Updating %s", self.objectName())
if self._index:
self.disconnectDocument()
if self.toPlainText() != F.toString(self._index.data()):
# print(" Updating plaintext")
# LOGGER.debug(" Updating plaintext")
self.document().setPlainText(F.toString(self._index.data()))
self.reconnectDocument()
@ -314,18 +322,18 @@ class textEditView(QTextEdit):
text = self.toPlainText()
self._updating.unlock()
# print("Submitting", self.objectName())
# LOGGER.debug("Submitting %s", self.objectName())
if self._index and self._index.isValid():
# item = self._index.internalPointer()
if text != self._index.data():
# print(" Submitting plain text")
# LOGGER.debug(" Submitting plain text")
self._model.setData(QModelIndex(self._index), text)
elif self._indexes:
for i in self._indexes:
item = i.internalPointer()
if text != F.toString(item.data(self._column)):
print("Submitting many indexes")
LOGGER.debug("Submitting many indexes")
self._model.setData(i, text)
def keyPressEvent(self, event):
@ -393,6 +401,49 @@ class textEditView(QTextEdit):
Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
QTextEdit.mousePressEvent(self, event)
def beginTooltipMoveEvent(self):
self._tooltip['depth'] += 1
def endTooltipMoveEvent(self):
self._tooltip['depth'] -= 1
def showTooltip(self, pos, text):
QToolTip.showText(pos, text)
self._tooltip['active'] = self._tooltip['depth']
def hideTooltip(self):
if self._tooltip['active'] == self._tooltip['depth']:
QToolTip.hideText()
def mouseMoveEvent(self, event):
"""
When mouse moves, we show tooltip when appropriate.
"""
self.beginTooltipMoveEvent()
QTextEdit.mouseMoveEvent(self, event)
self.endTooltipMoveEvent()
match = None
# Check if the selected word has any suggestions for correction
if self.spellcheck and self._dict:
cursor = self.cursorForPosition(event.pos())
# Searches for correlating/overlapping matches
suggestions = self._dict.findSuggestions(self.toPlainText(), cursor.selectionStart(), cursor.selectionEnd())
if len(suggestions) > 0:
# I think it should focus on one type of error at a time.
match = suggestions[0]
if match:
# Wrap the message into a fitting width
msg_lines = textwrap.wrap(match.msg, 48)
self.showTooltip(event.globalPos(), "\n".join(msg_lines))
else:
self.hideTooltip()
def wheelEvent(self, event):
"""
We catch wheelEvent if key modifier is CTRL to change font size.
@ -427,58 +478,198 @@ class textEditView(QTextEdit):
QAction.__init__(self, *args)
self.triggered.connect(lambda x: self.correct.emit(
str(self.text())))
str(self.text())))
def contextMenuEvent(self, event):
# Based on http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/
popup_menu = self.createStandardContextMenu()
popup_menu.exec_(event.globalPos())
def newCharacter(self):
text = self.sender().data()
LOGGER.debug(f'New character: {text}')
# switch to character page
mw = F.mainWindow()
mw.tabMain.setCurrentIndex(mw.TabPersos)
# add character
c = mw.mdlCharacter.addCharacter(name=text)
# switch to character
item = mw.lstCharacters.getItemByID(c.ID())
mw.lstCharacters.setCurrentItem(item)
def newPlotItem(self):
text = self.sender().data()
LOGGER.debug(f'New plot item: {text}')
# switch to plot page
mw = F.mainWindow()
mw.tabMain.setCurrentIndex(mw.TabPlots)
# add character
p, ID = mw.mdlPlots.addPlot(text)
# switch to character
plotIndex = mw.mdlPlots.getIndexFromID(ID.text())
# segfaults for some reason
# mw.lstSubPlots.setCurrentIndex(plotIndex)
def newWorldItem(self):
text = self.sender().data()
LOGGER.debug(f'New world item: {text}')
mw = F.mainWindow()
mw.tabMain.setCurrentIndex(mw.TabWorld)
item = mw.mdlWorld.addItem(title=text)
mw.treeWorld.setCurrentIndex(
mw.mdlWorld.indexFromItem(item))
def appendContextMenuEntriesForWord(self, popup_menu, selectedWord):
# add "new <something>" buttons at end
if selectedWord != None:
# new character
charAction = QAction(self.tr("&New Character"), popup_menu)
charAction.setIcon(F.themeIcon("characters"))
charAction.triggered.connect(self.newCharacter)
charAction.setData(selectedWord)
popup_menu.insertAction(None, charAction)
# new plot item
plotAction = QAction(self.tr("&New Plot Item"), popup_menu)
plotAction.setIcon(F.themeIcon("plots"))
plotAction.triggered.connect(self.newPlotItem)
plotAction.setData(selectedWord)
popup_menu.insertAction(None, plotAction)
# new world item
worldAction = QAction(self.tr("&New World Item"), popup_menu)
worldAction.setIcon(F.themeIcon("world"))
worldAction.triggered.connect(self.newWorldItem)
worldAction.setData(selectedWord)
popup_menu.insertAction(None, worldAction)
return popup_menu
def createStandardContextMenu(self):
popup_menu = QTextEdit.createStandardContextMenu(self)
if not self.spellcheck:
return popup_menu
# Select the word under the cursor.
# But only if there is no selection (otherwise it's impossible to select more text to copy/cut)
cursor = self.textCursor()
if not cursor.hasSelection():
cursor.select(QTextCursor.WordUnderCursor)
self.setTextCursor(cursor)
selectedWord = cursor.selectedText() if cursor.hasSelection() else None
if not self.spellcheck:
return self.appendContextMenuEntriesForWord(popup_menu, selectedWord)
suggestions = []
# Check for any suggestions for corrections at the cursors position
if self._dict != None:
text = self.toPlainText()
suggestions = self._dict.findSuggestions(text, cursor.selectionStart(), cursor.selectionEnd())
# Select the word under the cursor if necessary.
# But only if there is no selection (otherwise it's impossible to select more text to copy/cut)
if not cursor.hasSelection() and len(suggestions) == 0:
old_position = cursor.position()
cursor.select(QTextCursor.WordUnderCursor)
self.setTextCursor(cursor)
if cursor.hasSelection():
selectedWord = cursor.selectedText()
# Check if the selected word is misspelled and offer spelling
# suggestions if it is.
suggestions = self._dict.findSuggestions(text, cursor.selectionStart(), cursor.selectionEnd())
if len(suggestions) == 0:
cursor.clearSelection()
cursor.setPosition(old_position, QTextCursor.MoveAnchor)
self.setTextCursor(cursor)
selectedWord = None
popup_menu = self.appendContextMenuEntriesForWord(popup_menu, selectedWord)
if len(suggestions) > 0 or selectedWord != None:
valid = len(suggestions) == 0
# Check if the selected word is misspelled and offer spelling
# suggestions if it is.
if self._dict and cursor.hasSelection():
text = str(cursor.selectedText())
valid = not self._dict.isMisspelled(text)
selectedWord = cursor.selectedText()
if not valid:
spell_menu = QMenu(self.tr('Spelling Suggestions'), self)
spell_menu.setIcon(F.themeIcon("spelling"))
for word in self._dict.getSuggestions(text):
action = self.SpellAction(word, spell_menu)
action.correct.connect(self.correctWord)
spell_menu.addAction(action)
# I think it should focus on one type of error at a time.
match = suggestions[0]
popup_menu.insertSeparator(popup_menu.actions()[0])
# Adds: add to dictionary
addAction = QAction(self.tr("&Add to dictionary"), popup_menu)
addAction.setIcon(QIcon.fromTheme("list-add"))
addAction.triggered.connect(self.addWordToDict)
addAction.setData(selectedWord)
popup_menu.insertAction(popup_menu.actions()[0], addAction)
# Only add the spelling suggests to the menu if there are
# suggestions.
if len(spell_menu.actions()) != 0:
# Adds: suggestions
popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)
# popup_menu.insertSeparator(popup_menu.actions()[0])
if match.locqualityissuetype == 'misspelling':
spell_menu = QMenu(self.tr('Spelling Suggestions'), self)
spell_menu.setIcon(F.themeIcon("spelling"))
if (match.end > match.start and selectedWord == None):
# Select the actual area of the match
cursor = self.textCursor()
cursor.setPosition(match.start, QTextCursor.MoveAnchor);
cursor.setPosition(match.end, QTextCursor.KeepAnchor);
self.setTextCursor(cursor)
selectedWord = cursor.selectedText()
for word in match.replacements:
action = self.SpellAction(word, spell_menu)
action.correct.connect(self.correctWord)
spell_menu.addAction(action)
# Adds: add to dictionary
addAction = QAction(self.tr("&Add to dictionary"), popup_menu)
addAction.setIcon(QIcon.fromTheme("list-add"))
addAction.triggered.connect(self.addWordToDict)
addAction.setData(selectedWord)
popup_menu.insertAction(popup_menu.actions()[0], addAction)
# Only add the spelling suggests to the menu if there are
# suggestions.
if len(match.replacements) > 0:
# Adds: suggestions
popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)
else:
correct_menu = None
correct_action = None
if (len(match.replacements) > 0 and match.end > match.start):
# Select the actual area of the match
cursor = self.textCursor()
cursor.setPosition(match.start, QTextCursor.MoveAnchor);
cursor.setPosition(match.end, QTextCursor.KeepAnchor);
self.setTextCursor(cursor)
if len(match.replacements) > 0:
correct_menu = QMenu(self.tr('&Correction Suggestions'), self)
correct_menu.setIcon(F.themeIcon("spelling"))
for word in match.replacements:
action = self.SpellAction(word, correct_menu)
action.correct.connect(self.correctWord)
correct_menu.addAction(action)
if correct_menu == None:
correct_action = QAction(self.tr('&Correction Suggestion'), popup_menu)
correct_action.setIcon(F.themeIcon("spelling"))
correct_action.setEnabled(False)
# Wrap the message into a fitting width
msg_lines = textwrap.wrap(match.msg, 48)
# Insert the lines of the message backwards
for i in range(0, len(msg_lines)):
popup_menu.insertSection(popup_menu.actions()[0], msg_lines[len(msg_lines) - (i + 1)])
if correct_menu != None:
popup_menu.insertMenu(popup_menu.actions()[0], correct_menu)
else:
popup_menu.insertAction(popup_menu.actions()[0], correct_action)
# If word was added to custom dict, give the possibility to remove it
elif valid and self._dict.isCustomWord(selectedWord):
elif self._dict.isCustomWord(selectedWord):
popup_menu.insertSeparator(popup_menu.actions()[0])
# Adds: remove from dictionary
rmAction = QAction(self.tr("&Remove from custom dictionary"), popup_menu)
rmAction = QAction(
self.tr("&Remove from custom dictionary"), popup_menu)
rmAction.setIcon(QIcon.fromTheme("list-remove"))
rmAction.triggered.connect(self.rmWordFromDict)
rmAction.setData(selectedWord)

Some files were not shown because too many files have changed in this diff Show more