diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 0000000..7286cb9 --- /dev/null +++ b/.github/workflows/linux.yml @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index c34333a..75595ae 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.travis.yml b/.travis.yml index a39e423..995f028 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/i18n/manuskript_de.qm b/i18n/manuskript_de.qm index 4900812..fb3a6c2 100644 Binary files a/i18n/manuskript_de.qm and b/i18n/manuskript_de.qm differ diff --git a/i18n/manuskript_fa.qm b/i18n/manuskript_fa.qm index be651ee..1c1ecdf 100644 Binary files a/i18n/manuskript_fa.qm and b/i18n/manuskript_fa.qm differ diff --git a/i18n/manuskript_fa.ts b/i18n/manuskript_fa.ts index 009d04f..407bc98 100644 --- a/i18n/manuskript_fa.ts +++ b/i18n/manuskript_fa.ts @@ -38,7 +38,7 @@ - + Plain text @@ -53,82 +53,82 @@ - + Error - + Standalone document (not just a fragment) - + Include a table of contents. - + Number of sections level to include in TOC: - + Typographically correct output - + Normalize the document (cleaner) - + Specify the base level for headers: - + Use reference-style links instead of inline links - + Use ATX-style headers - + Self-contained HTML files, with no dependencies - + Use <q> tags for quotes in HTML - + LaTeX engine used to produce the PDF. - + Paper size: - + Font size: - + Class: - + Line spacing: @@ -159,13 +159,13 @@ - + Simplest export to plain text. Allows you to use your own markup not understood by Manuskript, for example <a href='www.fountain.io'>Fountain</a>. - + <p>A universal document converter. Can be used to convert Markdown to a wide range of other formats.</p> <p>Website: <a href="http://www.pandoc.org">http://pandoc.org/</a></p> @@ -198,24 +198,24 @@ - + Disable YAML metadata block. Use that if you get YAML related error. - + Convert to ePUB3 - + Could not process regular expression: {} - + Choose output file… @@ -324,24 +324,24 @@ Use that if you get YAML related error. Import - + Markdown import - + <b>Info:</b> A very simple parser that will go through a markdown document and create items for each titles.<br/>&nbsp; - + Folder import - + <p><b>Info:</b> Imports a whole directory structure. Folders are added as folders, and plaintext documents within (you chose which ones by extension) @@ -350,47 +350,47 @@ Use that if you get YAML related error. - + Include only those extensions: - + Comma separated values - + Sort items by name - + Import folder then files - + OPML Import - + File open failed. - + This does not appear to be a valid OPML file. - + Pandoc import - + <b>Info:</b> Manuskript can import from <b>markdown</b> or <b>OPML</b>. Pandoc will convert your document to either (see option below), and @@ -400,17 +400,17 @@ Use that if you get YAML related error. - + Import using: - + Wrap lines: - + <p>Should pandoc create cosmetic / non-semantic line-breaks?</p><p> <b>auto</b>: wraps at 72 characters.<br> @@ -420,27 +420,27 @@ Use that if you get YAML related error. - + Mind Map Import - + This does not appear to be a valid Mind Map file. - + Mind Map import - + Import tip as: - + Untitled @@ -456,7 +456,7 @@ Use that if you get YAML related error. MainWindow - + General @@ -496,7 +496,7 @@ Use that if you get YAML related error. - + Name @@ -506,7 +506,7 @@ Use that if you get YAML related error. - + Summary @@ -516,7 +516,7 @@ Use that if you get YAML related error. - + Summary: @@ -526,17 +526,17 @@ Use that if you get YAML related error. - + One paragraph - + One page - + Full @@ -566,7 +566,7 @@ Use that if you get YAML related error. - + Next @@ -586,312 +586,312 @@ Use that if you get YAML related error. - + Filter - + Basic info - + Importance - + Motivation - + Goal - + Conflict - + Epiphany - + <html><head/><body><p align="right">One sentence<br/>summary</p></body></html> - + <html><head/><body><p align="right">One paragraph<br/>summary</p></body></html> - + Notes - + Detailed info - + Plots - + Plot - + Character(s) - + Description - + Result - + Resolution steps - + World - + Populates with empty data - + More - + Source of passion - + Source of conflict - + Outline - + Editor - + Debug - + FlatData - + Persos - + Labels - + &File - + &Recent - + &Help - + &Tools - + &Edit - + &View - + &Mode - + &Cheat sheet - + Sea&rch - + &Navigation - + &Open - + Ctrl+O - + &Save - + Ctrl+S - + Sa&ve as... - + Ctrl+Shift+S - + &Quit - + Ctrl+Q - + &Show help texts - + Ctrl+Shift+B - + &Spellcheck - + F9 - + &Labels... - + &Status... - + Tree - + &Simple - + &Fiction - + Index cards - + S&ettings - + F8 - + &Close project - + Co&mpile - + F6 - + &Frequency Analyzer @@ -901,562 +901,665 @@ Use that if you get YAML related error. - + &About - + About Manuskript - + Manuskript - + Project {} saved. - + WARNING: Project {} not saved. - + Project {} loaded. - - Project {} loaded with some errors: - - - - - * {} wasn't found in project file. - - - - + Project {} loaded with some errors. - + (~{} pages) - + Words: {}{} - + Book summary - + Project tree - + Metadata - + Story line - + Enter information about your book, and yourself. - + The basic situation, in the form of a 'What if...?' question. Ex: 'What if the most dangerous evil wizard wasn't able to kill a baby?' (Harry Potter) - + Take time to think about a one sentence (~50 words) summary of your book. Then expand it to a paragraph, then to a page, then to a full summary. - + Create your characters. - + Develop plots. - + Build worlds. Create hierarchy of broad categories down to specific details. - + Create the outline of your masterpiece. - + Write. - + Debug info. Sometimes useful. - + Dictionary - + Nothing - + POV - + Label - + Progress - + Compile - + Icon color - + Text color - + Background color - + Icon - + Text - + Background - + Border - + Corner - + Add plot step - + &Import… - + F7 - + &Copy - + Ctrl+C - + C&ut - + Ctrl+X - + &Paste - + Ctrl+V - + &Split… - + Ctrl+Shift+K - + Sp&lit at cursor - + Ctrl+K - + Ctrl+M - + Ctrl+D - + Del - + &Move Up - + Ctrl+Shift+Up - + M&ove Down - + Ctrl+Shift+Down - + Dupl&icate - + &Delete - + &Rename - + F2 - + Organi&ze - + M&erge - + &Format - + &Header - + &Level 1 (setext) - + Ctrl+Alt+1 - + Level &2 - + Ctrl+Alt+2 - + Level &1 (atx) - + Ctrl+1 - + L&evel 2 - + Ctrl+2 - + Level &3 - + Ctrl+3 - + Level &4 - + Ctrl+4 - + Level &5 - + Ctrl+5 - + Level &6 - + Ctrl+6 - + &Bold - + Ctrl+B - + &Italic - + Ctrl+I - + &Strike - + &Verbatim - + Su&perscript - + Ctrl++ - + Subsc&ript - + Ctrl+- - + Co&mment block - + Ctrl+Shift+C - + Clear &formats - + Ctrl+0 - + &Comment line(s) - + &Ordered list - + &Unordered list - + B&lockquote - + Remove selected plot step(s) - + The file {} does not exist. Has it been moved or deleted? - + Install {}{} to use spellcheck - + {} has no installed dictionaries - + {}{} is not installed - + Save project? - + Save changes to project "{}" before closing? - + Your changes will be lost if you don't save them. - + PyQt / Qt versions 5.11 and 5.12 are known to cause a crash which might result in a loss of data. - + PyQt {} and Qt {} are in use. - + Proceed with import at your own risk + + + Allow POV + + + + + Search + + + + + Ctrl+F + + + + + F3 + + + + + Shift+F3 + + + + + Situation + + + + + Status + + + + + &Technical Support + + + + + How to obtain technical support for Manuskript. + + + + + F1 + + + + + &Locate log file... + + + + + Locate log file + + + + + Locate the diagnostic log file used for this session. + + + + + Shift+F1 + + + + + Sorry! + + + + + This session is not being logged. + + + + + A log file is a Work in Progress! + + + + + The log file "{}" will continue to be written to until Manuskript is closed. + + + + + It will now be displayed in your file manager, but is of limited use until you close Manuskript. + + + + + Error! + + + + + An error was encountered while trying to show the log file below in your file manager. + + + + + Search + + + No results found + + Settings @@ -1471,7 +1574,7 @@ Use that if you get YAML related error. - + Revisions @@ -1481,17 +1584,17 @@ Use that if you get YAML related error. - + Labels - + Status - + Fullscreen @@ -1506,658 +1609,709 @@ Use that if you get YAML related error. - + Loading - + Automatically load last project on startup - + Saving - + Automatically save every - + minutes. - + If no changes during - + seconds. - + Save on project close - + <html><head/><body><p>If you check this option, your project will be saved as one single file. Easier to copy or backup, but does not allow collaborative editing, or versioning.<br/>If this is unchecked, your project will be saved as a folder containing many small files.</p></body></html> - + Save to one single file - + Revisions are a way to keep track of modifications. For each text item, it stores any changes you make to the main text, allowing you to see and restoring previous versions. - + Keep revisions - + S&mart remove - + Keep: - + Smart remove allows you to keep only a certain number of revisions. It is strongly recommended to use it, lest you file will becomes full of thousands of insignificant changes. - + revisions per day for the last month - + revisions per minute for the last 10 minutes - + revisions per hour for the last day - + revisions per 10 minutes for the last hour - + revisions per week till the end of time - + Views settings - + Tree - + Colors - + Icon color: - + Nothing - + POV - + Label - + Progress - + Compile - + Text color: - + Background color: - + Folders - + Show ite&m count - + Show summary - + &Nothing - + Text - + Outline - + Visible columns - + Goal - + Word count - + Percentage - + Title - + Index cards - + Item colors - + Border color: - + Corner color: - + Background - + Color: - + Ctrl+S - + Image: - + Text editor - + Font - + Family: - + Size: - + Misspelled: - + Background: - + Paragraphs - + Line spacing: - + Single - + 1.5 lines - + Double - + Proportional - + % - + Tab width: - + px - + Indent 1st line - + Spacing: - + New - + Edit - + Delete - + Theme name: - + Apply - + Cancel - + Window Background - + Text Background - + Text Options - + Paragraph Options - + Type: - + No Image - + Tiled - + Centered - + Stretched - + Scaled - + Zoomed - + Opacity: - + Position: - + Left - + Center - + Right - + Width: - + Corner radius: - + Margins: - + Padding: - + Font: - + Style - + Cursor - + Use block insertion of - + Alignment: - + Justify - + Alignment - + Icon Size - + TextLabel - + Disable blinking - + Text area - + Max width - + Left/Right margins: - + Top/Bottom margins: - + S&how progress - + Show summar&y - + Show p&rogress - + Old st&yle - + Transparent - + Restore defaults - + Style: - + Language: - + Font size: - + Restarting Manuskript ensures all settings take effect. - + Show &word count - + &Show word count - + &New style - + Typewriter mode - + Focus mode - + None - + Sentence - + Line - + Paragraph - + <p><b>The Revisions feature has been at the source of many reported issues. In this version of Manuskript it has been turned off by default for new projects in order to provide the best experience.</b></p><p>Why aren't these issues fixed already? <a href="https://www.theologeek.ch/manuskript/contribute/">We need your help to make Manuskript better!</a></p> + + + Show progress in chars next + to words + + + + + Char/Word Counter + + + + + Count spaces as chars + + + + + Show char c&ount + + + + + Sho&w char count + + SpellAction - + Spelling Suggestions - + &Add to dictionary - + &Remove from custom dictionary + + + &New Character + + + + + &New Plot Item + + + + + &New World Item + + + + + &Correction Suggestions + + + + + &Correction Suggestion + + about @@ -2188,37 +2342,37 @@ Use that if you get YAML related error. abstractModel - + Title - + POV - + Label - + Status - + Compile - + Word count - + Goal @@ -2259,17 +2413,12 @@ Use that if you get YAML related error. characterModel - - New character - - - - + Name - + Value @@ -2277,17 +2426,17 @@ Use that if you get YAML related error. characterTreeView - + Main - + Secondary - + Minor @@ -2403,12 +2552,12 @@ Use that if you get YAML related error. corkDelegate - + One line summary - + Full summary @@ -2719,72 +2868,72 @@ Use that if you get YAML related error. fullScreenEditor - + Theme: - + {} words / {} - + {} words - + Spellcheck - + Navigation - + New Text - + Title - + Title: Show Full Path - + Theme selector - + Word count - + Progress - + Progress: Auto Show/Hide - + Clock - + Clock: Show Seconds @@ -2863,14 +3012,6 @@ Use that if you get YAML related error. - - lastAccessedDirectoryInfo - - - Last accessed directory "{}" loaded. - - - lineEditView @@ -2985,18 +3126,33 @@ Use that if you get YAML related error. - + Root - - {} words / {} + + {} words - - {} words + + ({} chars) {} words / {} + + + + + {} words / {} + + + + + {} chars + + + + + {} chars @@ -3049,7 +3205,7 @@ Use that if you get YAML related error. myPanel - + Auto-hide @@ -3203,12 +3359,12 @@ Use that if you get YAML related error. outlineItem - + {} words / {} ({}) - + {} words @@ -3216,17 +3372,17 @@ Use that if you get YAML related error. pandocSettings - + General - + Table of Content - + Custom settings for {} @@ -3430,37 +3586,32 @@ Use that if you get YAML related error. plotModel - - New plot - - - - + Name - + Meta - + New step - + Main - + Secondary - + Minor @@ -3468,22 +3619,22 @@ Use that if you get YAML related error. plotTreeView - + Main - + Secondary - + Minor - + **Plot:** {} @@ -3547,52 +3698,52 @@ Use that if you get YAML related error. references - + Not a reference: {}. - + Unknown reference: {}. - + Path: - + Stats: - + POV: - + Status: - + Label: - + Short summary: - + Long summary: - + Notes: @@ -3612,72 +3763,72 @@ Use that if you get YAML related error. - + Go to {}. - + Description - + Result - + Characters - + Resolution steps - + Passion - + Conflict - + <b>Unknown reference:</b> {}. - + Folder: <b>{}</b> - + Text: <b>{}</b> - + Character: <b>{}</b> - + Plot: <b>{}</b> - + World: <b>{name}</b>{path} - + Referenced in: @@ -3720,12 +3871,12 @@ Use that if you get YAML related error. - + Restore - + Delete @@ -3785,12 +3936,12 @@ Use that if you get YAML related error. - + Line {}: - + Clear all @@ -3807,111 +3958,56 @@ Use that if you get YAML related error. Search for... - - - Search in: - - - - - All - - - - - Title - - - - - Text - - - - - Summary - - - - - Notes - - - - - POV - - - - - Status - - - - - Label - - - - - Options: - - - - - Case sensitive - - settingsWindow - + New status - + New label - + newtheme - + New theme - + (read-only) - + Open Image - + Image files (*.jpg; *.jpeg; *.png) - + Error - + Unable to load selected file - + Unable to add selected image: {} @@ -3998,22 +4094,22 @@ Use that if you get YAML related error. tabSplitter - + Open selected items in that view. - + Split horizontally - + Close split - + Split vertically @@ -4021,7 +4117,7 @@ Use that if you get YAML related error. textEditView - + Various @@ -4120,27 +4216,27 @@ Use that if you get YAML related error. - + Novel - + Novella - + Short Story - + Research paper - + Demo projects @@ -4175,147 +4271,147 @@ Use that if you get YAML related error. - + Open project - + Manuskript project (*.msk);;All files (*) - + Save project as... - + Manuskript project (*.msk) - + Manuskript - + Create New Project - + Warning - + Overwrite existing project {} ? - + Empty fiction - + Chapter - + Scene - + Trilogy - + Book - + Section - + Empty non-fiction - + words each. - + of - + Text - + Something - + <b>Total:</b> {} words (~ {} pages) - + Fiction - + Non-fiction - + Idea - + Note - + Research - + TODO - + First draft - + Second draft - + Final @@ -4323,212 +4419,212 @@ Use that if you get YAML related error. worldModel - + New item - + Fantasy world building - + Physical - + Climate - + Topography - + Astronomy - + Wild life - + Flora - + History - + Races - + Diseases - + Cultural - + Customs - + Food - + Languages - + Education - + Dresses - + Science - + Calendar - + Bodily language - + Ethics - + Religion - + Government - + Politics - + Gender roles - + Music and arts - + Architecture - + Military - + Technology - + Courtship - + Demography - + Transportation - + Medicine - + Magic system - + Rules - + Organization - + Magical objects - + Magical places - + Magical races - + Important places - + Important objects - + Natural resources diff --git a/i18n/manuskript_hu.ts b/i18n/manuskript_hu.ts index 22658e6..58eeae3 100644 --- a/i18n/manuskript_hu.ts +++ b/i18n/manuskript_hu.ts @@ -38,7 +38,7 @@ Előnézet kiemelővel. - + Plain text Egyszerű szöveg @@ -53,77 +53,77 @@ Szükséges a latex telepítése. - + Error Hiba - + Standalone document (not just a fragment) Egyedülálló dokumentum (nem csak töredék) - + Include a table of contents. Tartalomjegyzék belefoglalása. - + Number of sections level to include in TOC: Szekció szintek száma, melyek bekerülnek a Tartalomjegyzékbe: - + Typographically correct output Tipográfiailag helyes kimenet - + Normalize the document (cleaner) Dokumentum normalizálása (tisztító) - + Specify the base level for headers: Adja meg az alapszintet a tartalomjegyzékhez: - + Use reference-style links instead of inline links Referencia-stílus linkek helyett inline linkek használata - + Use ATX-style headers ATX stílusú fejlécek használata - + Use <q> tags for quotes in HTML Használja a <q> címkéket a HTML idézetekhez - + LaTeX engine used to produce the PDF. A LaTeX motort PDF generálására használjuk. - + Paper size: Papírméret: - + Font size: Betűméret: - + Class: Osztály: - + Line spacing: Sortávolság: @@ -155,14 +155,14 @@ Feltételezi, hogy a szövegek a markdown-ban vannak formázva. - + Simplest export to plain text. Allows you to use your own markup not understood by Manuskript, for example <a href='www.fountain.io'>Fountain</a>. A legegyszerűbb lehetőség sima szöveg exportálására. Lehetővé teszi saját jelölőnyelv használatát, melyet a manuskript nem tud feldolgozni, mint például a <a href='www.fountain.io'>Fountain</a>. - + <p>A universal document converter. Can be used to convert Markdown to a wide range of other formats.</p> <p>Website: <a href="http://www.pandoc.org">http://pandoc.org/</a></p> @@ -203,30 +203,30 @@ egy egész sor más formátumra</p> böngészhetők vagy vezérelhetők. - + Disable YAML metadata block. Use that if you get YAML related error. YAML metaadt blokk letiltása. Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. - + Convert to ePUB3 Konvertálás ePUB3 formátumra - + Self-contained HTML files, with no dependencies - + Could not process regular expression: {} - + Choose output file… @@ -335,12 +335,12 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Import - + Markdown import Markdown importálása - + <b>Info:</b> A very simple parser that will go through a markdown document and create items for each titles.<br/>&nbsp; @@ -349,12 +349,12 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. elemeket hoz létre minden egyes címhez.<br/>&nbsp; - + Folder import Mappa importálása - + <p><b>Info:</b> Imports a whole directory structure. Folders are added as folders, and plaintext documents within (you chose which ones by extension) @@ -367,47 +367,47 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. <p>Csak szövegfájlokat támogat (sem képeket, sem bináris állományokat vagy egyebet nem támogat).</p> - + Include only those extensions: Csak a következő kiterjesztésűek belefoglalása: - + Comma separated values Veszővel elválasztott értékek - + Sort items by name Elemek rendezése név szerint - + Import folder then files Mappa, majd fájlok importálása - + OPML Import OPML importálás - + File open failed. Fájlmegnyitás sikertelen. - + This does not appear to be a valid OPML file. Ez egy érvénytelen OPML fájlnak tűnik. - + Pandoc import Pandoc importálása - + <b>Info:</b> Manuskript can import from <b>markdown</b> or <b>OPML</b>. Pandoc will convert your document to either (see option below), and @@ -422,17 +422,17 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. <br/>&nbsp; - + Import using: Importálás ezzel: - + Wrap lines: Sorok tördelése: - + <p>Should pandoc create cosmetic / non-semantic line-breaks?</p><p> <b>auto</b>: wraps at 72 characters.<br> @@ -446,27 +446,27 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. <b>megőrzés</b>:megpróbálja megőrizni az eredeti sortöréseket.</p> - + Mind Map Import Elmetérkép importálása - + This does not appear to be a valid Mind Map file. Ez egy érvénytelen Elmetérkép fájlnak tűnik. - + Mind Map import Elmetérkép importálása - + Import tip as: Importálás mint: - + Untitled Név nélküli @@ -482,7 +482,7 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. MainWindow - + General Általános @@ -522,7 +522,7 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Szerző - + Name Név @@ -532,7 +532,7 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Email cím - + Summary Összefoglaló @@ -542,7 +542,7 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Szituáció: - + Summary: Összefoglaló: @@ -552,17 +552,17 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Egy mondat - + One paragraph Egy bekezdés - + One page Egy oldal - + Full Teljes @@ -592,7 +592,7 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Teljes összefoglaló - + Next Következő @@ -612,312 +612,312 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Nevek - + Filter Szűrő - + Basic info Alapinformáció - + Importance Fontosság - + Motivation Motiváció - + Goal Cél - + Conflict Konfliktus - + Epiphany Fordulópont - + <html><head/><body><p align="right">One sentence<br/>summary</p></body></html> <html><head/><body><p align="right">Egy mondat<br/>összefoglaló</p></body></html> - + <html><head/><body><p align="right">One paragraph<br/>summary</p></body></html> <html><head/><body><p align="right">Egy bekezdés<br/>összefoglaló</p></body></html> - + Notes Jegyzetek - + Detailed info Részletes információ - + Plots Cselekmények - + Plot Cselekmény - + Character(s) Szereplő(k) - + Description Leírás - + Result Eredmény - + Resolution steps Megoldás lépései - + World Világ - + Populates with empty data Feltöltés üres adatokkal - + More Több - + Source of passion Szenvedély forrása - + Source of conflict Konfliktus forrása - + Outline Áttekintés - + Editor Szerkesztő - + Debug Hibakeresés - + FlatData SimaAdat - + Persos Személyek - + Labels Címkék - + &File &Fájl - + &Recent &Előző - + &Help &Súgó - + &Tools &Eszközök - + &Edit &Szerkesztés - + &View &Nézet - + &Mode &Mód - + &Cheat sheet &Puska - + Sea&rch &Keresés - + &Navigation &Navigáció - + &Open Megn&yitás - + Ctrl+O Ctrl+O - + &Save &Mentés - + Ctrl+S Ctrl+S - + Sa&ve as... Mentés m&ásként... - + Ctrl+Shift+S Ctrl+Shift+S - + &Quit &Kilépés - + Ctrl+Q Ctrl+Q - + &Show help texts Sú&gó szövegek mutatása - + Ctrl+Shift+B Ctrl+Shift+B - + &Spellcheck &Helyesírás-ellenőrzés - + F9 F9 - + &Labels... &Címkék... - + &Status... &Státusz... - + Tree Fa - + &Simple &Egyszerű - + &Fiction &Fikció - + Index cards Tárgymutató kártyák - + S&ettings &Beállítások - + F8 F8 - + &Close project &Projekt bezárása - + Co&mpile &Összeállítás - + F6 F6 - + &Frequency Analyzer &Gyakoriság Elemző @@ -927,564 +927,667 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Könyv információk - + &About &Névjegy - + About Manuskript A Manuskript -ről - + Manuskript Manuskript - + Project {} saved. {} projekt mentve. - + WARNING: Project {} not saved. FIGYELEM: {} projekt nem került mentésre. - + Project {} loaded. {} projekt betöltve. - - Project {} loaded with some errors: - {} projekt betöltve, hibákkal: - - - - * {} wasn't found in project file. - * {} nem található a projekt fájlban. - - - + Project {} loaded with some errors. {} projekt betöltve, hibákkal. - + (~{} pages) (~{} oldal) - + Words: {}{} Szó: {}{} - + Book summary Könyv összefoglalása - + Project tree Projektfa - + Metadata Metaadat - + Story line Történetív - + Enter information about your book, and yourself. Adjon meg információt a könyvéről és önmagáról. - + The basic situation, in the form of a 'What if...?' question. Ex: 'What if the most dangerous evil wizard wasn't able to kill a baby?' (Harry Potter) Az alapszituáció 'Mi lenne ha...?' kérdésként feltéve. Pl.. 'Mi lenne ha a legveszélyesebb ' gonosz varázsló ne lenne képes megölni egy csecsemőt?' (Harry Potter) - + Take time to think about a one sentence (~50 words) summary of your book. Then expand it to a paragraph, then to a page, then to a full summary. Szánjon rá időt, hogy elgondolkodjon a könyve egymondatos (-50 szavas) összefoglalóján. Aztán bővítse ki egy bekezdéssé, majd egy oldallá, majd egy teljes összefoglalóvá. - + Create your characters. Alkossa meg a szereplőit. - + Develop plots. Cselekmények kidolgozása. - + Build worlds. Create hierarchy of broad categories down to specific details. Építsen világokat. Készítse el az átfogó kategóriák (és az specifikus részleteinek) hierarchiáját. - + Create the outline of your masterpiece. Készítse el a mesterműve áttekintését. - + Write. Írjon. - + Debug info. Sometimes useful. Hibakeresési információ. Valami hasznos. - + Dictionary Szótár - + Nothing Semmi - + POV Szempont - + Label Címke - + Progress Előrehaladás - + Compile Összeállítás - + Icon color Ikonszín - + Text color Szövegszín - + Background color Háttérszín - + Icon Ikon - + Text Szöveg - + Background Háttér - + Border Szegély - + Corner Sarok - + Add plot step Cselekmény lépés hozzáadása (CTRL+Enter) - + &Import… &Importálás… - + F7 F7 - + &Copy &Másolás - + Ctrl+C Ctrl+C - + C&ut &Kivágás - + Ctrl+X Ctrl+X - + &Paste &Beillesztés - + Ctrl+V Ctrl+V - + &Split… &Felosztás… - + Ctrl+Shift+K Ctrl+Shift+K - + Sp&lit at cursor Fe&losztás kurzornál - + Ctrl+K Ctrl+K - + Ctrl+M Ctrl+M - + Ctrl+D Ctrl+D - + Del Törlés - + &Move Up &Mozgatás Fel - + Ctrl+Shift+Up Ctrl+Shift+Up - + M&ove Down M&ozgatás Le - + Ctrl+Shift+Down Ctrl+Shift+Down - + Dupl&icate &Duplikálás - + &Delete &Törlés - + &Rename &Átnevezés - + F2 F2 - + Organi&ze &Rendszerezés - + M&erge Össze&fésülés - + Remove selected plot step(s) - + &Format - + &Header - + &Level 1 (setext) - + Ctrl+Alt+1 - + Level &2 - + Ctrl+Alt+2 - + Level &1 (atx) - + Ctrl+1 - + L&evel 2 - + Ctrl+2 - + Level &3 - + Ctrl+3 - + Level &4 - + Ctrl+4 - + Level &5 - + Ctrl+5 - + Level &6 - + Ctrl+6 - + &Bold - + Ctrl+B - + &Italic - + Ctrl+I - + &Strike - + &Verbatim - + Su&perscript - + Ctrl++ - + Subsc&ript - + Ctrl+- - + Co&mment block - + Ctrl+Shift+C - + Clear &formats - + Ctrl+0 - + &Comment line(s) - + &Ordered list - + &Unordered list - + B&lockquote - + The file {} does not exist. Has it been moved or deleted? - + Install {}{} to use spellcheck - + {} has no installed dictionaries - + {}{} is not installed - + Save project? - + Save changes to project "{}" before closing? - + Your changes will be lost if you don't save them. - + PyQt / Qt versions 5.11 and 5.12 are known to cause a crash which might result in a loss of data. - + PyQt {} and Qt {} are in use. - + Proceed with import at your own risk + + + Allow POV + + + + + Search + + + + + Ctrl+F + + + + + F3 + F3 + + + + Shift+F3 + + + + + Situation + + + + + Status + Státusz + + + + &Technical Support + + + + + How to obtain technical support for Manuskript. + + + + + F1 + F1 + + + + &Locate log file... + + + + + Locate log file + + + + + Locate the diagnostic log file used for this session. + + + + + Shift+F1 + + + + + Sorry! + + + + + This session is not being logged. + + + + + A log file is a Work in Progress! + + + + + The log file "{}" will continue to be written to until Manuskript is closed. + + + + + It will now be displayed in your file manager, but is of limited use until you close Manuskript. + + + + + Error! + + + + + An error was encountered while trying to show the log file below in your file manager. + + + + + Search + + + No results found + + Settings @@ -1499,7 +1602,7 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Általános - + Revisions Felülvizsgálatok @@ -1509,17 +1612,17 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Nézetek - + Labels Címkék - + Status Státusz - + Fullscreen Teljes képernyő @@ -1534,658 +1637,709 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Alkalmazás stílusa - + Loading Betöltés - + Automatically load last project on startup Automatikusan töltse be a legutóbbi projektet induláskor - + Saving Mentés - + Automatically save every Automatikusan mentsen minden - + minutes. percben. - + If no changes during Ha nem történik változás - + seconds. másodpercig. - + Save on project close Mentés kilépéskor - + <html><head/><body><p>If you check this option, your project will be saved as one single file. Easier to copy or backup, but does not allow collaborative editing, or versioning.<br/>If this is unchecked, your project will be saved as a folder containing many small files.</p></body></html> <html><head/><body><p>Ha ezt a lehetőséget bejelöli, a projektje egyetlen fájlként kerül mentésre. Így egyszerűbb másolni vagy biztonsági mentést készíteni róla, de elesik a csapatmunka és a verziókezelés lehetőségétől.<br/>Amennyiben nincs bejelölve, a projektje mappaként kerül mentésre, amely sok kis fáljt tartalmaz</p></body></html> - + Save to one single file Mentés egyetlen fájlba - + Revisions are a way to keep track of modifications. For each text item, it stores any changes you make to the main text, allowing you to see and restoring previous versions. A felülvizsgálatok használata egy módszer a módosítások követésére. Minden szöveges elem számára tartalmazza annak minden módosulatát, lehetővé téve, hogy megtekintse és visszaállítsa az előző verziókat. - + Keep revisions Felülvizsgálatok megtartása - + S&mart remove &Intelligens eltávolítás - + Keep: Megtartás: - + Smart remove allows you to keep only a certain number of revisions. It is strongly recommended to use it, lest you file will becomes full of thousands of insignificant changes. Az intelligens eltávolítás lehetővé teszi, hogy csak bizonyos számú felülvizsgálatot tartson meg. Használata erősen javasolt, ellenkező esetben a fájlja tele lesz jelentéktelen módosítások ezreivel. - + revisions per day for the last month felülvizsgálat naponta, a múlt hónapban - + revisions per minute for the last 10 minutes felülvizsgálat percenként, az elmúlt 10 percben - + revisions per hour for the last day felülvizsgálat óránként a legutóbbi napra - + revisions per 10 minutes for the last hour felülvizsgálat 10 percenként, az utóbbi egy órában - + revisions per week till the end of time felülvizsgálat hetente, valaha - + Views settings Beállítások megtekintése - + Tree Fa - + Colors Színek - + Icon color: Ikon színe: - + Nothing Semmi - + POV Szempont - + Label Címke - + Progress Előrehaladás - + Compile Összeállítás - + Text color: Szövegszín: - + Background color: Háttérszín: - + Folders Mappák - + Show ite&m count Ele&mszám mutatása - + Show summary Összefoglaló mutatása - + &Nothing &Semmi - + Text Szöveg - + Outline Körvonal - + Visible columns Látható oszlopok - + Goal Cél - + Word count Szószám - + Percentage Százalék - + Title Cím - + Index cards Kartotéklapok - + Item colors Elemszínek - + Border color: Szegélyszín: - + Corner color: Sarokszín: - + Background Háttér - + Color: Szín: - + Ctrl+S Ctrl+S - + Image: Kép: - + Text editor Szövegszerkesztő - + Font Betűtípus - + Family: Család: - + Size: Méret: - + Misspelled: Elírt: - + Background: Háttér: - + Paragraphs Bekezdések - + Line spacing: Vonaltávolság: - + Single Egyes - + 1.5 lines 1.5 sor - + Double Dupla - + Proportional Arányos - + % % - + Tab width: Tabulátorszélesség: - + px :px - + Indent 1st line Első sor behúzása - + Spacing: Szóközölés: - + New Új - + Edit Szerkesztés - + Delete Törlés - + Theme name: Témanév: - + Apply Elfogadás - + Cancel Mégsem - + Window Background Ablakháttér - + Text Background Szövegháttér - + Text Options Szövegbeállítások - + Paragraph Options Bekezdés Beállítások - + Type: Típus: - + No Image Nincs Kép - + Tiled Csempék - + Centered Középrezárt - + Stretched Kinyújtott - + Scaled Skálázott - + Zoomed Nagyított - + Opacity: Telítettség: - + Position: Pozíció: - + Left Bal - + Center Közép - + Right Jobb - + Width: Szélesség: - + Corner radius: Sarok rádiusza: - + Margins: Margók: - + Padding: Párnázottság: - + Font: Betű: - + Style Stílus - + Cursor Kurzor - + Use block insertion of Blokkbeillesztés használata - + Alignment: Elrendezés: - + Justify Kiegyenlítés - + Alignment Elrendezés - + Icon Size Ikonméret - + TextLabel SzövegCímke - + Disable blinking Villogás letiltása - + Text area Szövegterület - + Max width Max távolság - + Left/Right margins: Bal/Jobb margók: - + Top/Bottom margins: Felső/Alsó margók: - + S&how progress Előrehaladás &mutatása - + Show summar&y &Összegzés mutatása - + Show p&rogress &Előrehaladás mutatása - + Old st&yle Régi &stílus - + Transparent Átlátszó - + Restore defaults Alapértelmezés visszaállítása - + Style: - + Language: - + Font size: Betűméret: - + Restarting Manuskript ensures all settings take effect. - + Show &word count - + &Show word count - + &New style - + Typewriter mode - + Focus mode - + None Egyik sem - + Sentence - + Line - + Paragraph - + <p><b>The Revisions feature has been at the source of many reported issues. In this version of Manuskript it has been turned off by default for new projects in order to provide the best experience.</b></p><p>Why aren't these issues fixed already? <a href="https://www.theologeek.ch/manuskript/contribute/">We need your help to make Manuskript better!</a></p> + + + Show progress in chars next + to words + + + + + Char/Word Counter + + + + + Count spaces as chars + + + + + Show char c&ount + + + + + Sho&w char count + + SpellAction - + Spelling Suggestions Helyesírási javaslatok - + &Add to dictionary &Hozzáadás a szótárhoz - + &Remove from custom dictionary &Eltávolítás az egyéni szótárból + + + &New Character + + + + + &New Plot Item + + + + + &New World Item + + + + + &Correction Suggestions + + + + + &Correction Suggestion + + about @@ -2216,37 +2370,37 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. abstractModel - + Title Cím - + POV Szempont - + Label Cím - + Status Státusz - + Compile Összeállítás - + Word count Szószám - + Goal Cél @@ -2287,17 +2441,12 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. characterModel - - New character - Új szereplő - - - + Name Név - + Value Érték @@ -2305,17 +2454,17 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. characterTreeView - + Main - + Secondary Másodlagos - + Minor Kisebb jelentőségű @@ -2431,12 +2580,12 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. corkDelegate - + One line summary Egysoros összefoglaló - + Full summary Teljes összefoglaló @@ -2747,72 +2896,72 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. fullScreenEditor - + Theme: Téma: - + {} words / {} {} szó / {} - + {} words {} szó - + Spellcheck - + Navigation - + New Text - + Title Cím - + Title: Show Full Path - + Theme selector - + Word count Szószám - + Progress Előrehaladás - + Progress: Auto Show/Hide - + Clock - + Clock: Show Seconds @@ -2891,14 +3040,6 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Beállítások - - lastAccessedDirectoryInfo - - - Last accessed directory "{}" loaded. - - - lineEditView @@ -3013,20 +3154,35 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Alt+Up - + Root Gyökér - - {} words / {} - {} szó / {} - - - + {} words {} szó. + + + ({} chars) {} words / {} + + + + + {} words / {} + + + + + {} chars + + + + + {} chars + + markdownSettings @@ -3077,7 +3233,7 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. myPanel - + Auto-hide Auto-rejtés @@ -3231,12 +3387,12 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. outlineItem - + {} words / {} ({}) {} szó / {} ({}) - + {} words {} szó @@ -3244,17 +3400,17 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. pandocSettings - + General Általános - + Table of Content Tartalomjegyzék - + Custom settings for {} Egyedi beállítások erre {} @@ -3458,37 +3614,32 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. plotModel - - New plot - Új cselekmény - - - + Name Név - + Meta Meta - + New step Új lépés - + Main - + Secondary Másodlagos - + Minor Kisebb jelentőségű @@ -3496,22 +3647,22 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. plotTreeView - + Main - + Secondary Másodlagos - + Minor Kisebb jelentőségű - + **Plot:** {} **Cselekmény:** {} @@ -3575,52 +3726,52 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. references - + Not a reference: {}. Ez nem hivatkozás: {}. - + Unknown reference: {}. Ismeretlen hivatkozás: {}. - + Path: Elérési út: - + Stats: Statisztika: - + POV: Szempont: - + Status: Státusz: - + Label: Címke: - + Short summary: Rövid összefoglaló: - + Long summary: Hosszú összefoglaló: - + Notes: Jegyzetek: @@ -3640,72 +3791,72 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. A következő szempontja: - + Go to {}. Menj ide {}. - + Description Leírás - + Result Eredmény - + Characters Szereplők - + Resolution steps Megoldási lépések - + Passion Szenvedély - + Conflict Konfliktus - + <b>Unknown reference:</b> {}. <b>Ismeretlen hivatkozás:</b> {}. - + Folder: <b>{}</b> Mappa: <b>{}</b> - + Text: <b>{}</b> Szöveg: <b>{}</b> - + Character: <b>{}</b> Szereplő: <b>{}</b> - + Plot: <b>{}</b> Cselekmény: <b>{}</b> - + World: <b>{name}</b>{path} Világ: <b>{name}</b>{path} - + Referenced in: Hivatkozva itt: @@ -3748,12 +3899,12 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Lehetőségek - + Restore Visszaállítás - + Delete Törlés @@ -3813,12 +3964,12 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. {} másodperccel ezelőtt - + Line {}: Sor {}: - + Clear all Minden törlése @@ -3835,111 +3986,56 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Search for... Keresés... - - - Search in: - Keresés ebben: - - - - All - Mind - - - - Title - Cím - - - - Text - Szöveg - - - - Summary - Összefoglaló - - - - Notes - Jegyzetek - - - - POV - Szempont - - - - Status - Státusz - - - - Label - Címke - - - - Options: - Lehetőségek: - - - - Case sensitive - Kis-nagybetű érzékeny - settingsWindow - + New status Új státusz - + New label Új címke - + newtheme újtéma - + New theme Új téma - + (read-only) (csak-olvasható) - + Open Image - + Image files (*.jpg; *.jpeg; *.png) - + Error Hiba - + Unable to load selected file - + Unable to add selected image: {} @@ -4040,22 +4136,22 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. tabSplitter - + Open selected items in that view. Kijelölt elemek megnyitása abban a nézetben. - + Split horizontally Vízszintes felosztás - + Close split Felosztás bezárása - + Split vertically Függőleges felosztás @@ -4063,7 +4159,7 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. textEditView - + Various Különféle @@ -4162,27 +4258,27 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Üres - + Novel Regény - + Novella Kisregény - + Short Story Elbeszélés - + Research paper Kutatási jegyzet - + Demo projects Bemutató projektet @@ -4217,147 +4313,147 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. Létrehozás - + Open project Projekt megnyitása - + Manuskript project (*.msk);;All files (*) Manuskript projekt (*.msk);;Minden fájl (*) - + Save project as... Projekt mentése másként... - + Manuskript project (*.msk) Manuskript projekt (*.msk) - + Manuskript Manuskript - + Create New Project Új Projekt Létrehozása - + Warning Figyelmeztetés - + Overwrite existing project {} ? Felülírja a létező projektet {} ? - + Empty fiction Üres regény - + Chapter Fejezet - + Scene Jelenet - + Trilogy Trilógia - + Book Könyv - + Section Szakasz - + Empty non-fiction Üres nem-kitalált - + words each. szó mindegyik. - + of a - + Text Szöveg - + Something Valami - + <b>Total:</b> {} words (~ {} pages) <b>Összesen:</b> {} szó (~ {} oldal) - + Fiction Kitalált - + Non-fiction Valós alapú - + Idea Ötlet - + Note Jegyzet - + Research Kutatás - + TODO TODO - + First draft Első vázlat - + Second draft Második vázlat - + Final Végső @@ -4365,212 +4461,212 @@ Akkor használja ezt, ha YAML-hoz kapcsolódó gondjai vannak. worldModel - + New item Új elem - + Fantasy world building Fantáziavilág építése - + Physical Fizikai - + Climate Éghajlat - + Topography Domborzat - + Astronomy Csillagászat - + Wild life Vadvilág - + Flora Növényvilág - + History Történelem - + Races Fajok - + Diseases Betegségek - + Cultural Kulturális - + Customs Szokások - + Food Étel - + Languages Nyelvek - + Education Oktatás - + Dresses Öltözékek - + Science Tudomány - + Calendar Naptár - + Bodily language Testbeszéd - + Ethics Erkölcsök - + Religion Vallás - + Government Kormány - + Politics Politikák - + Gender roles Nemi szerepek - + Music and arts Zene és művészetek - + Architecture Építészet - + Military Katonaság - + Technology Technológia - + Courtship Udvarlás - + Demography Demográfia - + Transportation Közlekedés - + Medicine Orvosság - + Magic system Mágiarendszer - + Rules Szabályok - + Organization Szervezet - + Magical objects Varázstárgyak - + Magical places Mágikus helyek - + Magical races Mágikus fajok - + Important places Fontos helyek - + Important objects Fontos tárgyak - + Natural resources diff --git a/i18n/manuskript_nl.qm b/i18n/manuskript_nl.qm index af172be..2643d98 100644 Binary files a/i18n/manuskript_nl.qm and b/i18n/manuskript_nl.qm differ diff --git a/i18n/manuskript_ro.ts b/i18n/manuskript_ro.ts index 1d0662c..29a1997 100644 --- a/i18n/manuskript_ro.ts +++ b/i18n/manuskript_ro.ts @@ -38,7 +38,7 @@ - + Plain text @@ -53,82 +53,82 @@ - + Error - + Standalone document (not just a fragment) - + Include a table of contents. - + Number of sections level to include in TOC: - + Typographically correct output - + Normalize the document (cleaner) - + Specify the base level for headers: - + Use reference-style links instead of inline links - + Use ATX-style headers - + Self-contained HTML files, with no dependencies - + Use <q> tags for quotes in HTML - + LaTeX engine used to produce the PDF. - + Paper size: - + Font size: - + Class: - + Line spacing: @@ -159,13 +159,13 @@ - + Simplest export to plain text. Allows you to use your own markup not understood by Manuskript, for example <a href='www.fountain.io'>Fountain</a>. - + <p>A universal document converter. Can be used to convert Markdown to a wide range of other formats.</p> <p>Website: <a href="http://www.pandoc.org">http://pandoc.org/</a></p> @@ -198,24 +198,24 @@ - + Disable YAML metadata block. Use that if you get YAML related error. - + Convert to ePUB3 - + Could not process regular expression: {} - + Choose output file… @@ -324,24 +324,24 @@ Use that if you get YAML related error. Import - + Markdown import - + <b>Info:</b> A very simple parser that will go through a markdown document and create items for each titles.<br/>&nbsp; - + Folder import - + <p><b>Info:</b> Imports a whole directory structure. Folders are added as folders, and plaintext documents within (you chose which ones by extension) @@ -350,47 +350,47 @@ Use that if you get YAML related error. - + Include only those extensions: - + Comma separated values - + Sort items by name - + Import folder then files - + OPML Import - + File open failed. - + This does not appear to be a valid OPML file. - + Pandoc import - + <b>Info:</b> Manuskript can import from <b>markdown</b> or <b>OPML</b>. Pandoc will convert your document to either (see option below), and @@ -400,17 +400,17 @@ Use that if you get YAML related error. - + Import using: - + Wrap lines: - + <p>Should pandoc create cosmetic / non-semantic line-breaks?</p><p> <b>auto</b>: wraps at 72 characters.<br> @@ -420,27 +420,27 @@ Use that if you get YAML related error. - + Mind Map Import - + This does not appear to be a valid Mind Map file. - + Mind Map import - + Import tip as: - + Untitled @@ -456,7 +456,7 @@ Use that if you get YAML related error. MainWindow - + General @@ -496,7 +496,7 @@ Use that if you get YAML related error. - + Name @@ -506,7 +506,7 @@ Use that if you get YAML related error. - + Summary @@ -516,7 +516,7 @@ Use that if you get YAML related error. - + Summary: @@ -526,17 +526,17 @@ Use that if you get YAML related error. - + One paragraph - + One page - + Full @@ -566,7 +566,7 @@ Use that if you get YAML related error. - + Next @@ -586,312 +586,312 @@ Use that if you get YAML related error. - + Filter - + Basic info - + Importance - + Motivation - + Goal - + Conflict - + Epiphany - + <html><head/><body><p align="right">One sentence<br/>summary</p></body></html> - + <html><head/><body><p align="right">One paragraph<br/>summary</p></body></html> - + Notes - + Detailed info - + Plots - + Plot - + Character(s) - + Description - + Result - + Resolution steps - + World - + Populates with empty data - + More - + Source of passion - + Source of conflict - + Outline - + Editor - + Debug - + FlatData - + Persos - + Labels - + &File - + &Recent - + &Help - + &Tools - + &Edit - + &View - + &Mode - + &Cheat sheet - + Sea&rch - + &Navigation - + &Open - + Ctrl+O - + &Save - + Ctrl+S - + Sa&ve as... - + Ctrl+Shift+S - + &Quit - + Ctrl+Q - + &Show help texts - + Ctrl+Shift+B - + &Spellcheck - + F9 - + &Labels... - + &Status... - + Tree - + &Simple - + &Fiction - + Index cards - + S&ettings - + F8 - + &Close project - + Co&mpile - + F6 - + &Frequency Analyzer @@ -901,562 +901,665 @@ Use that if you get YAML related error. - + &About - + About Manuskript - + Manuskript - + Project {} saved. - + WARNING: Project {} not saved. - + Project {} loaded. - - Project {} loaded with some errors: - - - - - * {} wasn't found in project file. - - - - + Project {} loaded with some errors. - + (~{} pages) - + Words: {}{} - + Book summary - + Project tree - + Metadata - + Story line - + Enter information about your book, and yourself. - + The basic situation, in the form of a 'What if...?' question. Ex: 'What if the most dangerous evil wizard wasn't able to kill a baby?' (Harry Potter) - + Take time to think about a one sentence (~50 words) summary of your book. Then expand it to a paragraph, then to a page, then to a full summary. - + Create your characters. - + Develop plots. - + Build worlds. Create hierarchy of broad categories down to specific details. - + Create the outline of your masterpiece. - + Write. - + Debug info. Sometimes useful. - + Dictionary - + Nothing - + POV - + Label - + Progress - + Compile - + Icon color - + Text color - + Background color - + Icon - + Text - + Background - + Border - + Corner - + Add plot step - + &Import… - + F7 - + &Copy - + Ctrl+C - + C&ut - + Ctrl+X - + &Paste - + Ctrl+V - + &Split… - + Ctrl+Shift+K - + Sp&lit at cursor - + Ctrl+K - + Ctrl+M - + Ctrl+D - + Del - + &Move Up - + Ctrl+Shift+Up - + M&ove Down - + Ctrl+Shift+Down - + Dupl&icate - + &Delete - + &Rename - + F2 - + Organi&ze - + M&erge - + &Format - + &Header - + &Level 1 (setext) - + Ctrl+Alt+1 - + Level &2 - + Ctrl+Alt+2 - + Level &1 (atx) - + Ctrl+1 - + L&evel 2 - + Ctrl+2 - + Level &3 - + Ctrl+3 - + Level &4 - + Ctrl+4 - + Level &5 - + Ctrl+5 - + Level &6 - + Ctrl+6 - + &Bold - + Ctrl+B - + &Italic - + Ctrl+I - + &Strike - + &Verbatim - + Su&perscript - + Ctrl++ - + Subsc&ript - + Ctrl+- - + Co&mment block - + Ctrl+Shift+C - + Clear &formats - + Ctrl+0 - + &Comment line(s) - + &Ordered list - + &Unordered list - + B&lockquote - + Remove selected plot step(s) - + The file {} does not exist. Has it been moved or deleted? - + Install {}{} to use spellcheck - + {} has no installed dictionaries - + {}{} is not installed - + Save project? - + Save changes to project "{}" before closing? - + Your changes will be lost if you don't save them. - + PyQt / Qt versions 5.11 and 5.12 are known to cause a crash which might result in a loss of data. - + PyQt {} and Qt {} are in use. - + Proceed with import at your own risk + + + Allow POV + + + + + Search + + + + + Ctrl+F + + + + + F3 + + + + + Shift+F3 + + + + + Situation + + + + + Status + + + + + &Technical Support + + + + + How to obtain technical support for Manuskript. + + + + + F1 + + + + + &Locate log file... + + + + + Locate log file + + + + + Locate the diagnostic log file used for this session. + + + + + Shift+F1 + + + + + Sorry! + + + + + This session is not being logged. + + + + + A log file is a Work in Progress! + + + + + The log file "{}" will continue to be written to until Manuskript is closed. + + + + + It will now be displayed in your file manager, but is of limited use until you close Manuskript. + + + + + Error! + + + + + An error was encountered while trying to show the log file below in your file manager. + + + + + Search + + + No results found + + Settings @@ -1471,7 +1574,7 @@ Use that if you get YAML related error. - + Revisions @@ -1481,17 +1584,17 @@ Use that if you get YAML related error. - + Labels - + Status - + Fullscreen @@ -1506,658 +1609,709 @@ Use that if you get YAML related error. - + Loading - + Automatically load last project on startup - + Saving - + Automatically save every - + minutes. - + If no changes during - + seconds. - + Save on project close - + <html><head/><body><p>If you check this option, your project will be saved as one single file. Easier to copy or backup, but does not allow collaborative editing, or versioning.<br/>If this is unchecked, your project will be saved as a folder containing many small files.</p></body></html> - + Save to one single file - + Revisions are a way to keep track of modifications. For each text item, it stores any changes you make to the main text, allowing you to see and restoring previous versions. - + Keep revisions - + S&mart remove - + Keep: - + Smart remove allows you to keep only a certain number of revisions. It is strongly recommended to use it, lest you file will becomes full of thousands of insignificant changes. - + revisions per day for the last month - + revisions per minute for the last 10 minutes - + revisions per hour for the last day - + revisions per 10 minutes for the last hour - + revisions per week till the end of time - + Views settings - + Tree - + Colors - + Icon color: - + Nothing - + POV - + Label - + Progress - + Compile - + Text color: - + Background color: - + Folders - + Show ite&m count - + Show summary - + &Nothing - + Text - + Outline - + Visible columns - + Goal - + Word count - + Percentage - + Title - + Index cards - + Item colors - + Border color: - + Corner color: - + Background - + Color: - + Ctrl+S - + Image: - + Text editor - + Font - + Family: - + Size: - + Misspelled: - + Background: - + Paragraphs - + Line spacing: - + Single - + 1.5 lines - + Double - + Proportional - + % - + Tab width: - + px - + Indent 1st line - + Spacing: - + New - + Edit - + Delete - + Theme name: - + Apply - + Cancel - + Window Background - + Text Background - + Text Options - + Paragraph Options - + Type: - + No Image - + Tiled - + Centered - + Stretched - + Scaled - + Zoomed - + Opacity: - + Position: - + Left - + Center - + Right - + Width: - + Corner radius: - + Margins: - + Padding: - + Font: - + Style - + Cursor - + Use block insertion of - + Alignment: - + Justify - + Alignment - + Icon Size - + TextLabel - + Disable blinking - + Text area - + Max width - + Left/Right margins: - + Top/Bottom margins: - + S&how progress - + Show summar&y - + Show p&rogress - + Old st&yle - + Transparent - + Restore defaults - + Style: - + Language: - + Font size: - + Restarting Manuskript ensures all settings take effect. - + Show &word count - + &Show word count - + &New style - + Typewriter mode - + Focus mode - + None - + Sentence - + Line - + Paragraph - + <p><b>The Revisions feature has been at the source of many reported issues. In this version of Manuskript it has been turned off by default for new projects in order to provide the best experience.</b></p><p>Why aren't these issues fixed already? <a href="https://www.theologeek.ch/manuskript/contribute/">We need your help to make Manuskript better!</a></p> + + + Show progress in chars next + to words + + + + + Char/Word Counter + + + + + Count spaces as chars + + + + + Show char c&ount + + + + + Sho&w char count + + SpellAction - + Spelling Suggestions - + &Add to dictionary - + &Remove from custom dictionary + + + &New Character + + + + + &New Plot Item + + + + + &New World Item + + + + + &Correction Suggestions + + + + + &Correction Suggestion + + about @@ -2188,37 +2342,37 @@ Use that if you get YAML related error. abstractModel - + Title - + POV - + Label - + Status - + Compile - + Word count - + Goal @@ -2259,17 +2413,12 @@ Use that if you get YAML related error. characterModel - - New character - - - - + Name - + Value @@ -2277,17 +2426,17 @@ Use that if you get YAML related error. characterTreeView - + Main - + Secondary - + Minor @@ -2403,12 +2552,12 @@ Use that if you get YAML related error. corkDelegate - + One line summary - + Full summary @@ -2719,72 +2868,72 @@ Use that if you get YAML related error. fullScreenEditor - + Theme: - + {} words / {} - + {} words - + Spellcheck - + Navigation - + New Text - + Title - + Title: Show Full Path - + Theme selector - + Word count - + Progress - + Progress: Auto Show/Hide - + Clock - + Clock: Show Seconds @@ -2863,14 +3012,6 @@ Use that if you get YAML related error. - - lastAccessedDirectoryInfo - - - Last accessed directory "{}" loaded. - - - lineEditView @@ -2985,18 +3126,33 @@ Use that if you get YAML related error. - + Root - - {} words / {} + + {} words - - {} words + + ({} chars) {} words / {} + + + + + {} words / {} + + + + + {} chars + + + + + {} chars @@ -3049,7 +3205,7 @@ Use that if you get YAML related error. myPanel - + Auto-hide @@ -3203,12 +3359,12 @@ Use that if you get YAML related error. outlineItem - + {} words / {} ({}) - + {} words @@ -3216,17 +3372,17 @@ Use that if you get YAML related error. pandocSettings - + General - + Table of Content - + Custom settings for {} @@ -3430,37 +3586,32 @@ Use that if you get YAML related error. plotModel - - New plot - - - - + Name - + Meta - + New step - + Main - + Secondary - + Minor @@ -3468,22 +3619,22 @@ Use that if you get YAML related error. plotTreeView - + Main - + Secondary - + Minor - + **Plot:** {} @@ -3547,52 +3698,52 @@ Use that if you get YAML related error. references - + Not a reference: {}. - + Unknown reference: {}. - + Path: - + Stats: - + POV: - + Status: - + Label: - + Short summary: - + Long summary: - + Notes: @@ -3612,72 +3763,72 @@ Use that if you get YAML related error. - + Go to {}. - + Description - + Result - + Characters - + Resolution steps - + Passion - + Conflict - + <b>Unknown reference:</b> {}. - + Folder: <b>{}</b> - + Text: <b>{}</b> - + Character: <b>{}</b> - + Plot: <b>{}</b> - + World: <b>{name}</b>{path} - + Referenced in: @@ -3720,12 +3871,12 @@ Use that if you get YAML related error. - + Restore - + Delete @@ -3785,12 +3936,12 @@ Use that if you get YAML related error. - + Line {}: - + Clear all @@ -3807,111 +3958,56 @@ Use that if you get YAML related error. Search for... - - - Search in: - - - - - All - - - - - Title - - - - - Text - - - - - Summary - - - - - Notes - - - - - POV - - - - - Status - - - - - Label - - - - - Options: - - - - - Case sensitive - - settingsWindow - + New status - + New label - + newtheme - + New theme - + (read-only) - + Open Image - + Image files (*.jpg; *.jpeg; *.png) - + Error - + Unable to load selected file - + Unable to add selected image: {} @@ -3998,22 +4094,22 @@ Use that if you get YAML related error. tabSplitter - + Open selected items in that view. - + Split horizontally - + Close split - + Split vertically @@ -4021,7 +4117,7 @@ Use that if you get YAML related error. textEditView - + Various @@ -4120,27 +4216,27 @@ Use that if you get YAML related error. - + Novel - + Novella - + Short Story - + Research paper - + Demo projects @@ -4175,147 +4271,147 @@ Use that if you get YAML related error. - + Open project - + Manuskript project (*.msk);;All files (*) - + Save project as... - + Manuskript project (*.msk) - + Manuskript - + Create New Project - + Warning - + Overwrite existing project {} ? - + Empty fiction - + Chapter - + Scene - + Trilogy - + Book - + Section - + Empty non-fiction - + words each. - + of - + Text - + Something - + <b>Total:</b> {} words (~ {} pages) - + Fiction - + Non-fiction - + Idea - + Note - + Research - + TODO - + First draft - + Second draft - + Final @@ -4323,212 +4419,212 @@ Use that if you get YAML related error. worldModel - + New item - + Fantasy world building - + Physical - + Climate - + Topography - + Astronomy - + Wild life - + Flora - + History - + Races - + Diseases - + Cultural - + Customs - + Food - + Languages - + Education - + Dresses - + Science - + Calendar - + Bodily language - + Ethics - + Religion - + Government - + Politics - + Gender roles - + Music and arts - + Architecture - + Military - + Technology - + Courtship - + Demography - + Transportation - + Medicine - + Magic system - + Rules - + Organization - + Magical objects - + Magical places - + Magical races - + Important places - + Important objects - + Natural resources diff --git a/i18n/manuskript_tr.ts b/i18n/manuskript_tr.ts index 86737fd..7dfe4f4 100644 --- a/i18n/manuskript_tr.ts +++ b/i18n/manuskript_tr.ts @@ -38,7 +38,7 @@ - + Plain text Düz metin @@ -53,82 +53,82 @@ LaTex yüklü olmalıdır. - + Error Hata - + Standalone document (not just a fragment) - + Include a table of contents. İçindekiler kısmı ekleyin. - + Number of sections level to include in TOC: - + Typographically correct output Doğru yazılmış sonuç - + Normalize the document (cleaner) - + Specify the base level for headers: - + Use reference-style links instead of inline links - + Use ATX-style headers ATX-stili başlık kullan - + Self-contained HTML files, with no dependencies - + Use <q> tags for quotes in HTML - + LaTeX engine used to produce the PDF. - + Paper size: Kağıt boyutu: - + Font size: Yazı tipi boyutu: - + Class: Sınıf: - + Line spacing: Satır aralığı: @@ -159,13 +159,13 @@ - + Simplest export to plain text. Allows you to use your own markup not understood by Manuskript, for example <a href='www.fountain.io'>Fountain</a>. - + <p>A universal document converter. Can be used to convert Markdown to a wide range of other formats.</p> <p>Website: <a href="http://www.pandoc.org">http://pandoc.org/</a></p> @@ -198,24 +198,24 @@ - + Disable YAML metadata block. Use that if you get YAML related error. - + Convert to ePUB3 ePUB3 dosyasına çevir - + Could not process regular expression: {} Düzenli ifadeler işlenirken hata oluştu : {} - + Choose output file… @@ -324,24 +324,24 @@ Use that if you get YAML related error. Import - + Markdown import - + <b>Info:</b> A very simple parser that will go through a markdown document and create items for each titles.<br/>&nbsp; - + Folder import - + <p><b>Info:</b> Imports a whole directory structure. Folders are added as folders, and plaintext documents within (you chose which ones by extension) @@ -350,47 +350,47 @@ Use that if you get YAML related error. - + Include only those extensions: - + Comma separated values Virgülle ayrılmış değerler - + Sort items by name - + Import folder then files - + OPML Import - + File open failed. Dosya açılamadı. - + This does not appear to be a valid OPML file. Bu bir OPML dosyası değil. - + Pandoc import - + <b>Info:</b> Manuskript can import from <b>markdown</b> or <b>OPML</b>. Pandoc will convert your document to either (see option below), and @@ -400,17 +400,17 @@ Use that if you get YAML related error. - + Import using: Bunu kullanarak içeri aktar: - + Wrap lines: - + <p>Should pandoc create cosmetic / non-semantic line-breaks?</p><p> <b>auto</b>: wraps at 72 characters.<br> @@ -420,27 +420,27 @@ Use that if you get YAML related error. - + Mind Map Import - + This does not appear to be a valid Mind Map file. - + Mind Map import - + Import tip as: - + Untitled Başlıksız @@ -456,7 +456,7 @@ Use that if you get YAML related error. MainWindow - + General Genel @@ -496,7 +496,7 @@ Use that if you get YAML related error. Yazar - + Name İsim @@ -506,7 +506,7 @@ Use that if you get YAML related error. E-posta - + Summary Özet @@ -516,7 +516,7 @@ Use that if you get YAML related error. Durum: - + Summary: Özet: @@ -526,17 +526,17 @@ Use that if you get YAML related error. Bir cümle - + One paragraph Bir paragraf - + One page Bir sayfa - + Full @@ -566,7 +566,7 @@ Use that if you get YAML related error. Tam özet - + Next İleri @@ -586,312 +586,312 @@ Use that if you get YAML related error. İsimler - + Filter Filtrele - + Basic info Temel bilgiler - + Importance Önem - + Motivation Motivasyon - + Goal Amaç - + Conflict Çatışma - + Epiphany Tezahür - + <html><head/><body><p align="right">One sentence<br/>summary</p></body></html> <html><head/><body><p align="right">Bir cümlelik <br/>özet</p></body></html> - + <html><head/><body><p align="right">One paragraph<br/>summary</p></body></html> <html><head/><body><p align="right">Bir paragraflık<br/>özet</p></body></html> - + Notes Notlar - + Detailed info Detaylı bilgi - + Plots - + Plot - + Character(s) Karakter(ler) - + Description Tanım - + Result Sonuç - + Resolution steps Çözülme adımları - + World Evren - + Populates with empty data - + More Daha fazla - + Source of passion Tutku kaynağı - + Source of conflict Çatışma kaynağı - + Outline Taslak - + Editor Editör - + Debug Hata ayıklama - + FlatData - + Persos - + Labels Etiketler - + &File &Dosya - + &Recent &Yeni - + &Help &Yardım - + &Tools &Araçlar - + &Edit &Düzenle - + &View - + &Mode - + &Cheat sheet - + Sea&rch - + &Navigation - + &Open &Aç - + Ctrl+O Ctrl+O - + &Save &Kaydet - + Ctrl+S Ctrl+S - + Sa&ve as... Farklı Kaydet... - + Ctrl+Shift+S Ctrl+Shift+S - + &Quit &Kapat - + Ctrl+Q Ctrl+Q - + &Show help texts &Yardım dosyalarını göster - + Ctrl+Shift+B Ctrl+Shift+B - + &Spellcheck &Yazım Kontrolü - + F9 F9 - + &Labels... &Etiketler... - + &Status... &Durum... - + Tree - + &Simple - + &Fiction - + Index cards - + S&ettings - + F8 F8 - + &Close project - + Co&mpile - + F6 F6 - + &Frequency Analyzer @@ -901,562 +901,665 @@ Use that if you get YAML related error. - + &About - + About Manuskript - + Manuskript - + Project {} saved. - + WARNING: Project {} not saved. - + Project {} loaded. - - Project {} loaded with some errors: - - - - - * {} wasn't found in project file. - - - - + Project {} loaded with some errors. - + (~{} pages) - + Words: {}{} - + Book summary - + Project tree - + Metadata - + Story line - + Enter information about your book, and yourself. - + The basic situation, in the form of a 'What if...?' question. Ex: 'What if the most dangerous evil wizard wasn't able to kill a baby?' (Harry Potter) - + Take time to think about a one sentence (~50 words) summary of your book. Then expand it to a paragraph, then to a page, then to a full summary. - + Create your characters. - + Develop plots. - + Build worlds. Create hierarchy of broad categories down to specific details. - + Create the outline of your masterpiece. - + Write. - + Debug info. Sometimes useful. - + Dictionary - + Nothing - + POV - + Label - + Progress - + Compile - + Icon color - + Text color - + Background color - + Icon - + Text - + Background - + Border - + Corner - + Add plot step - + &Import… - + F7 F7 - + &Copy - + Ctrl+C - + C&ut - + Ctrl+X - + &Paste - + Ctrl+V - + &Split… - + Ctrl+Shift+K - + Sp&lit at cursor - + Ctrl+K - + Ctrl+M - + Ctrl+D - + Del - + &Move Up - + Ctrl+Shift+Up - + M&ove Down - + Ctrl+Shift+Down - + Dupl&icate - + &Delete - + &Rename - + F2 F2 - + Organi&ze - + M&erge - + &Format - + &Header - + &Level 1 (setext) - + Ctrl+Alt+1 - + Level &2 - + Ctrl+Alt+2 - + Level &1 (atx) - + Ctrl+1 - + L&evel 2 - + Ctrl+2 - + Level &3 - + Ctrl+3 - + Level &4 - + Ctrl+4 - + Level &5 - + Ctrl+5 - + Level &6 - + Ctrl+6 - + &Bold - + Ctrl+B - + &Italic - + Ctrl+I - + &Strike - + &Verbatim - + Su&perscript - + Ctrl++ - + Subsc&ript - + Ctrl+- - + Co&mment block - + Ctrl+Shift+C - + Clear &formats - + Ctrl+0 - + &Comment line(s) - + &Ordered list - + &Unordered list - + B&lockquote - + Remove selected plot step(s) - + The file {} does not exist. Has it been moved or deleted? - + Install {}{} to use spellcheck - + {} has no installed dictionaries - + {}{} is not installed - + Save project? - + Save changes to project "{}" before closing? - + Your changes will be lost if you don't save them. - + PyQt / Qt versions 5.11 and 5.12 are known to cause a crash which might result in a loss of data. - + PyQt {} and Qt {} are in use. - + Proceed with import at your own risk + + + Allow POV + + + + + Search + + + + + Ctrl+F + + + + + F3 + F3 + + + + Shift+F3 + + + + + Situation + + + + + Status + Durum + + + + &Technical Support + + + + + How to obtain technical support for Manuskript. + + + + + F1 + F1 + + + + &Locate log file... + + + + + Locate log file + + + + + Locate the diagnostic log file used for this session. + + + + + Shift+F1 + + + + + Sorry! + + + + + This session is not being logged. + + + + + A log file is a Work in Progress! + + + + + The log file "{}" will continue to be written to until Manuskript is closed. + + + + + It will now be displayed in your file manager, but is of limited use until you close Manuskript. + + + + + Error! + + + + + An error was encountered while trying to show the log file below in your file manager. + + + + + Search + + + No results found + + Settings @@ -1471,7 +1574,7 @@ Use that if you get YAML related error. Genel - + Revisions @@ -1481,17 +1584,17 @@ Use that if you get YAML related error. - + Labels Etiketler - + Status Durum - + Fullscreen @@ -1506,658 +1609,709 @@ Use that if you get YAML related error. - + Loading - + Automatically load last project on startup - + Saving - + Automatically save every - + minutes. - + If no changes during - + seconds. - + Save on project close - + <html><head/><body><p>If you check this option, your project will be saved as one single file. Easier to copy or backup, but does not allow collaborative editing, or versioning.<br/>If this is unchecked, your project will be saved as a folder containing many small files.</p></body></html> - + Save to one single file - + Revisions are a way to keep track of modifications. For each text item, it stores any changes you make to the main text, allowing you to see and restoring previous versions. - + Keep revisions - + S&mart remove - + Keep: - + Smart remove allows you to keep only a certain number of revisions. It is strongly recommended to use it, lest you file will becomes full of thousands of insignificant changes. - + revisions per day for the last month - + revisions per minute for the last 10 minutes - + revisions per hour for the last day - + revisions per 10 minutes for the last hour - + revisions per week till the end of time - + Views settings - + Tree - + Colors - + Icon color: - + Nothing - + POV - + Label - + Progress - + Compile - + Text color: - + Background color: - + Folders - + Show ite&m count - + Show summary - + &Nothing - + Text - + Outline Taslak - + Visible columns - + Goal Amaç - + Word count - + Percentage - + Title Başlık - + Index cards - + Item colors - + Border color: - + Corner color: - + Background - + Color: - + Ctrl+S Ctrl+S - + Image: - + Text editor - + Font - + Family: - + Size: - + Misspelled: - + Background: - + Paragraphs - + Line spacing: Satır aralığı: - + Single - + 1.5 lines - + Double - + Proportional - + % - + Tab width: - + px - + Indent 1st line - + Spacing: - + New - + Edit - + Delete - + Theme name: - + Apply - + Cancel - + Window Background - + Text Background - + Text Options - + Paragraph Options - + Type: - + No Image - + Tiled - + Centered - + Stretched - + Scaled - + Zoomed - + Opacity: - + Position: - + Left - + Center - + Right - + Width: - + Corner radius: - + Margins: - + Padding: - + Font: - + Style - + Cursor - + Use block insertion of - + Alignment: - + Justify - + Alignment - + Icon Size - + TextLabel - + Disable blinking - + Text area - + Max width - + Left/Right margins: - + Top/Bottom margins: - + S&how progress - + Show summar&y - + Show p&rogress - + Old st&yle - + Transparent - + Restore defaults - + Style: - + Language: - + Font size: Yazı tipi boyutu: - + Restarting Manuskript ensures all settings take effect. - + Show &word count - + &Show word count - + &New style - + Typewriter mode - + Focus mode - + None - + Sentence - + Line - + Paragraph - + <p><b>The Revisions feature has been at the source of many reported issues. In this version of Manuskript it has been turned off by default for new projects in order to provide the best experience.</b></p><p>Why aren't these issues fixed already? <a href="https://www.theologeek.ch/manuskript/contribute/">We need your help to make Manuskript better!</a></p> + + + Show progress in chars next + to words + + + + + Char/Word Counter + + + + + Count spaces as chars + + + + + Show char c&ount + + + + + Sho&w char count + + SpellAction - + Spelling Suggestions - + &Add to dictionary - + &Remove from custom dictionary + + + &New Character + + + + + &New Plot Item + + + + + &New World Item + + + + + &Correction Suggestions + + + + + &Correction Suggestion + + about @@ -2188,37 +2342,37 @@ Use that if you get YAML related error. abstractModel - + Title Başlık - + POV - + Label - + Status Durum - + Compile - + Word count - + Goal Amaç @@ -2259,17 +2413,12 @@ Use that if you get YAML related error. characterModel - - New character - - - - + Name İsim - + Value @@ -2277,17 +2426,17 @@ Use that if you get YAML related error. characterTreeView - + Main - + Secondary - + Minor @@ -2403,12 +2552,12 @@ Use that if you get YAML related error. corkDelegate - + One line summary - + Full summary Tam özet @@ -2719,72 +2868,72 @@ Use that if you get YAML related error. fullScreenEditor - + Theme: - + {} words / {} - + {} words - + Spellcheck - + Navigation - + New Text - + Title Başlık - + Title: Show Full Path - + Theme selector - + Word count - + Progress - + Progress: Auto Show/Hide - + Clock - + Clock: Show Seconds @@ -2863,14 +3012,6 @@ Use that if you get YAML related error. Ayarlar - - lastAccessedDirectoryInfo - - - Last accessed directory "{}" loaded. - - - lineEditView @@ -2985,18 +3126,33 @@ Use that if you get YAML related error. - + Root - - {} words / {} + + {} words - - {} words + + ({} chars) {} words / {} + + + + + {} words / {} + + + + + {} chars + + + + + {} chars @@ -3049,7 +3205,7 @@ Use that if you get YAML related error. myPanel - + Auto-hide @@ -3203,12 +3359,12 @@ Use that if you get YAML related error. outlineItem - + {} words / {} ({}) - + {} words @@ -3216,17 +3372,17 @@ Use that if you get YAML related error. pandocSettings - + General Genel - + Table of Content - + Custom settings for {} @@ -3430,37 +3586,32 @@ Use that if you get YAML related error. plotModel - - New plot - - - - + Name İsim - + Meta - + New step - + Main - + Secondary - + Minor @@ -3468,22 +3619,22 @@ Use that if you get YAML related error. plotTreeView - + Main - + Secondary - + Minor - + **Plot:** {} @@ -3547,52 +3698,52 @@ Use that if you get YAML related error. references - + Not a reference: {}. - + Unknown reference: {}. - + Path: - + Stats: - + POV: - + Status: Durum: - + Label: - + Short summary: - + Long summary: - + Notes: @@ -3612,72 +3763,72 @@ Use that if you get YAML related error. - + Go to {}. - + Description Tanım - + Result Sonuç - + Characters Karakterler - + Resolution steps Çözülme adımları - + Passion - + Conflict Çatışma - + <b>Unknown reference:</b> {}. - + Folder: <b>{}</b> - + Text: <b>{}</b> - + Character: <b>{}</b> - + Plot: <b>{}</b> - + World: <b>{name}</b>{path} - + Referenced in: @@ -3720,12 +3871,12 @@ Use that if you get YAML related error. - + Restore - + Delete @@ -3785,12 +3936,12 @@ Use that if you get YAML related error. - + Line {}: - + Clear all @@ -3807,111 +3958,56 @@ Use that if you get YAML related error. Search for... - - - Search in: - - - - - All - - - - - Title - Başlık - - - - Text - - - - - Summary - Özet - - - - Notes - Notlar - - - - POV - - - - - Status - Durum - - - - Label - - - - - Options: - - - - - Case sensitive - - settingsWindow - + New status - + New label - + newtheme - + New theme - + (read-only) - + Open Image - + Image files (*.jpg; *.jpeg; *.png) - + Error Hata - + Unable to load selected file - + Unable to add selected image: {} @@ -3998,22 +4094,22 @@ Use that if you get YAML related error. tabSplitter - + Open selected items in that view. - + Split horizontally - + Close split - + Split vertically @@ -4021,7 +4117,7 @@ Use that if you get YAML related error. textEditView - + Various @@ -4120,27 +4216,27 @@ Use that if you get YAML related error. - + Novel - + Novella - + Short Story - + Research paper - + Demo projects @@ -4175,147 +4271,147 @@ Use that if you get YAML related error. - + Open project - + Manuskript project (*.msk);;All files (*) - + Save project as... - + Manuskript project (*.msk) - + Manuskript - + Create New Project - + Warning - + Overwrite existing project {} ? - + Empty fiction - + Chapter - + Scene - + Trilogy - + Book - + Section - + Empty non-fiction - + words each. - + of - + Text - + Something - + <b>Total:</b> {} words (~ {} pages) - + Fiction - + Non-fiction - + Idea - + Note - + Research - + TODO - + First draft - + Second draft - + Final @@ -4323,212 +4419,212 @@ Use that if you get YAML related error. worldModel - + New item - + Fantasy world building - + Physical - + Climate - + Topography - + Astronomy - + Wild life - + Flora - + History - + Races - + Diseases - + Cultural - + Customs - + Food - + Languages - + Education - + Dresses - + Science - + Calendar - + Bodily language - + Ethics - + Religion - + Government - + Politics - + Gender roles - + Music and arts - + Architecture - + Military - + Technology - + Courtship - + Demography - + Transportation - + Medicine - + Magic system - + Rules - + Organization - + Magical objects - + Magical places - + Magical races - + Important places - + Important objects - + Natural resources diff --git a/i18n/manuskript_uk.ts b/i18n/manuskript_uk.ts index 70c852c..b077eb7 100644 --- a/i18n/manuskript_uk.ts +++ b/i18n/manuskript_uk.ts @@ -38,7 +38,7 @@ Попередній перегляд із маркером. - + Plain text Звичайний текст @@ -53,77 +53,77 @@ LaTeX має бути встановлено. - + Error Помилка - + Standalone document (not just a fragment) Окремий документ (не лише фрагмент) - + Include a table of contents. Включити зміст. - + Number of sections level to include in TOC: Рівень підрозділів, що їх буде додано до змісту: - + Typographically correct output Виправити результат для друку - + Normalize the document (cleaner) Чистіший документ (виправити помилки) - + Specify the base level for headers: Укажіть початковий рівень заголовків: - + Use reference-style links instead of inline links Використувати виноски замість вбудованих посилань - + Use ATX-style headers Використувати atx-заголовки (# ґратки) - + Use <q> tags for quotes in HTML Використувати тег <p> для цитувань у HTML - + LaTeX engine used to produce the PDF. Рушій LaTeX для створення файлу PDF. - + Paper size: Формат аркуша: - + Font size: Розмір шрифту: - + Class: Клас: - + Line spacing: Міжрядковий інтервал: @@ -155,14 +155,14 @@ Припускає, що текст форматовано на markdown. - + Simplest export to plain text. Allows you to use your own markup not understood by Manuskript, for example <a href='www.fountain.io'>Fountain</a>. Найпростіший експорт у звичайний текст. Дозволяє використувати власні мови розмітки, що їх не підтримує manuskript, наприклад <a href='www.fountain.io'>Fountain</a>. - + <p>A universal document converter. Can be used to convert Markdown to a wide range of other formats.</p> <p>Website: <a href="http://www.pandoc.org">http://pandoc.org/</a></p> @@ -203,30 +203,30 @@ за допомогою структуризатора. - + Disable YAML metadata block. Use that if you get YAML related error. Вимкнути блок метаданих YAML. Оберіть у разі помилки з YAML. - + Convert to ePUB3 Конвертувати у ePUB3 - + Self-contained HTML files, with no dependencies - + Could not process regular expression: {} - + Choose output file… @@ -335,12 +335,12 @@ Use that if you get YAML related error. Import - + Markdown import Імпортувати файл markdown - + <b>Info:</b> A very simple parser that will go through a markdown document and create items for each titles.<br/>&nbsp; @@ -349,12 +349,12 @@ Use that if you get YAML related error. створить елементи на кожний заголовок.<br/>&nbsp; - + Folder import Імпорт теки - + <p><b>Info:</b> Imports a whole directory structure. Folders are added as folders, and plaintext documents within (you chose which ones by extension) @@ -367,47 +367,47 @@ Use that if you get YAML related error. <p>Підтримувано лише текстові файли, а не зображення, двійкові тощо.</p> - + Include only those extensions: Включити лише наступні розширення: - + Comma separated values Значення, розділені комою - + Sort items by name Сортувати елементи за назвою - + Import folder then files Імпортувати теку і файли - + OPML Import Імпорт OPML - + File open failed. Не вдалося відкрити файл. - + This does not appear to be a valid OPML file. Здається, це не припустимий файл OPML. - + Pandoc import Імпорт pandoc - + <b>Info:</b> Manuskript can import from <b>markdown</b> or <b>OPML</b>. Pandoc will convert your document to either (see option below), and @@ -422,17 +422,17 @@ Use that if you get YAML related error. <br/>&nbsp; - + Import using: Імпорт за допомогою: - + Wrap lines: Переносити рядки: - + <p>Should pandoc create cosmetic / non-semantic line-breaks?</p><p> <b>auto</b>: wraps at 72 characters.<br> @@ -442,27 +442,27 @@ Use that if you get YAML related error. - + Mind Map Import Імпорт мапи думок - + This does not appear to be a valid Mind Map file. Це не припустимий формат файлу мапи думок. - + Mind Map import Імпорт мапи думок - + Import tip as: Імпортувати поради як: - + Untitled Без назви @@ -478,7 +478,7 @@ Use that if you get YAML related error. MainWindow - + General Загальне @@ -518,7 +518,7 @@ Use that if you get YAML related error. Автор - + Name Ім'я @@ -528,7 +528,7 @@ Use that if you get YAML related error. Електронна пошта - + Summary Стислий переказ @@ -538,7 +538,7 @@ Use that if you get YAML related error. Ситуація: - + Summary: Стислий переказ: @@ -548,17 +548,17 @@ Use that if you get YAML related error. Одне речення - + One paragraph Один абзац - + One page Одна сторінка - + Full Цілком @@ -588,7 +588,7 @@ Use that if you get YAML related error. Загальний опис - + Next Далі @@ -608,312 +608,312 @@ Use that if you get YAML related error. Імена - + Filter Фільтрувати - + Basic info Загальна інформація - + Importance Значність - + Motivation Мотивація - + Goal Мета - + Conflict Конфлікт - + Epiphany Прояснення - + <html><head/><body><p align="right">One sentence<br/>summary</p></body></html> <html><head/><body><p align="right">Переказ одним<br/>реченням</p></body></html> - + <html><head/><body><p align="right">One paragraph<br/>summary</p></body></html> <html><head/><body><p align="right">Переказ одним<br/>абзацом</p></body></html> - + Notes Нотатки - + Detailed info Докладна інформація - + Plots Сюжети - + Plot Сюжет - + Character(s) Персонаж(-і) - + Description Опис - + Result Результат - + Resolution steps Кроки розвитку сюжету - + World Світ - + Populates with empty data - + More Більше - + Source of passion - + Source of conflict Джерело конфлікту - + Outline Обрис - + Editor Редактор - + Debug Налагодити - + FlatData - + Persos - + Labels Позначки - + &File &Файл - + &Recent &Нещодавні - + &Help &Допомога - + &Tools &Знаряддя - + &Edit &Редагувати - + &View &Переглянути - + &Mode &Режим - + &Cheat sheet &Шпаргалка - + Sea&rch Шука&ти - + &Navigation &Перехід - + &Open &Відкрити - + Ctrl+O Ctrl+O - + &Save &Зберегти - + Ctrl+S Ctrl+S - + Sa&ve as... Збере&гти як... - + Ctrl+Shift+S Ctrl+Shift+S - + &Quit &Вихід - + Ctrl+Q Ctrl+Q - + &Show help texts &Показувати тексти довідки - + Ctrl+Shift+B Ctrl+Shift+B - + &Spellcheck &Перевірка правопису - + F9 F9 - + &Labels... &Позначки... - + &Status... &Стан... - + Tree Дерево - + &Simple &Простий - + &Fiction &Художня література - + Index cards Каталог - + S&ettings Н&алаштування - + F8 F8 - + &Close project &Закрити проект - + Co&mpile Екс&портувати - + F6 F6 - + &Frequency Analyzer &Частотний аналізатор @@ -923,562 +923,665 @@ Use that if you get YAML related error. Інформація про книжку - + &About &Про програму - + About Manuskript Про Манускрипт - + Manuskript Манускрипт - + Project {} saved. Проект {} збережено. - + WARNING: Project {} not saved. УВАГА: Проект {} не збережено. - + Project {} loaded. Проект {} завантажено. - - Project {} loaded with some errors: - Проект {} завантажено з кількома помилками: - - - - * {} wasn't found in project file. - * {} не знайдено у файлі проекту. - - - + Project {} loaded with some errors. Проект {} завантажено з кількома помилками. - + (~{} pages) (~{} сторінок) - + Words: {}{} Слів: {}{} - + Book summary Стислий переказ книжки - + Project tree Дерево проекту - + Metadata Метадані - + Story line - + Enter information about your book, and yourself. - + The basic situation, in the form of a 'What if...?' question. Ex: 'What if the most dangerous evil wizard wasn't able to kill a baby?' (Harry Potter) - + Take time to think about a one sentence (~50 words) summary of your book. Then expand it to a paragraph, then to a page, then to a full summary. - + Create your characters. - + Develop plots. - + Build worlds. Create hierarchy of broad categories down to specific details. - + Create the outline of your masterpiece. - + Write. - + Debug info. Sometimes useful. - + Dictionary - + Nothing - + POV З погляду - + Label Позначка - + Progress Стан - + Compile Експорт - + Icon color Колір позначки - + Text color Колір тексту - + Background color Колір тла - + Icon Значок - + Text Текст - + Background Тло - + Border Рамка - + Corner Кут - + Add plot step - + &Import… - + F7 F7 - + &Copy - + Ctrl+C - + C&ut - + Ctrl+X - + &Paste - + Ctrl+V - + &Split… - + Ctrl+Shift+K - + Sp&lit at cursor - + Ctrl+K - + Ctrl+M - + Ctrl+D - + Del Вилучити - + &Move Up - + Ctrl+Shift+Up - + M&ove Down - + Ctrl+Shift+Down - + Dupl&icate - + &Delete &Вилучити - + &Rename &Перейменувати - + F2 F2 - + Organi&ze - + M&erge - + Remove selected plot step(s) - + &Format - + &Header - + &Level 1 (setext) - + Ctrl+Alt+1 - + Level &2 - + Ctrl+Alt+2 - + Level &1 (atx) - + Ctrl+1 - + L&evel 2 - + Ctrl+2 - + Level &3 - + Ctrl+3 - + Level &4 - + Ctrl+4 - + Level &5 - + Ctrl+5 - + Level &6 - + Ctrl+6 - + &Bold - + Ctrl+B - + &Italic - + Ctrl+I - + &Strike - + &Verbatim - + Su&perscript - + Ctrl++ - + Subsc&ript - + Ctrl+- - + Co&mment block - + Ctrl+Shift+C - + Clear &formats - + Ctrl+0 - + &Comment line(s) - + &Ordered list - + &Unordered list - + B&lockquote - + The file {} does not exist. Has it been moved or deleted? - + Install {}{} to use spellcheck - + {} has no installed dictionaries - + {}{} is not installed - + Save project? - + Save changes to project "{}" before closing? - + Your changes will be lost if you don't save them. - + PyQt / Qt versions 5.11 and 5.12 are known to cause a crash which might result in a loss of data. - + PyQt {} and Qt {} are in use. - + Proceed with import at your own risk + + + Allow POV + + + + + Search + + + + + Ctrl+F + + + + + F3 + F3 + + + + Shift+F3 + + + + + Situation + + + + + Status + Стан + + + + &Technical Support + + + + + How to obtain technical support for Manuskript. + + + + + F1 + F1 + + + + &Locate log file... + + + + + Locate log file + + + + + Locate the diagnostic log file used for this session. + + + + + Shift+F1 + + + + + Sorry! + + + + + This session is not being logged. + + + + + A log file is a Work in Progress! + + + + + The log file "{}" will continue to be written to until Manuskript is closed. + + + + + It will now be displayed in your file manager, but is of limited use until you close Manuskript. + + + + + Error! + + + + + An error was encountered while trying to show the log file below in your file manager. + + + + + Search + + + No results found + + Settings @@ -1493,7 +1596,7 @@ Use that if you get YAML related error. Загальне - + Revisions @@ -1503,17 +1606,17 @@ Use that if you get YAML related error. - + Labels Позначки - + Status Стан - + Fullscreen @@ -1528,658 +1631,709 @@ Use that if you get YAML related error. - + Loading - + Automatically load last project on startup - + Saving - + Automatically save every - + minutes. - + If no changes during - + seconds. - + Save on project close - + <html><head/><body><p>If you check this option, your project will be saved as one single file. Easier to copy or backup, but does not allow collaborative editing, or versioning.<br/>If this is unchecked, your project will be saved as a folder containing many small files.</p></body></html> - + Save to one single file - + Revisions are a way to keep track of modifications. For each text item, it stores any changes you make to the main text, allowing you to see and restoring previous versions. - + Keep revisions - + S&mart remove - + Keep: - + Smart remove allows you to keep only a certain number of revisions. It is strongly recommended to use it, lest you file will becomes full of thousands of insignificant changes. - + revisions per day for the last month - + revisions per minute for the last 10 minutes - + revisions per hour for the last day - + revisions per 10 minutes for the last hour - + revisions per week till the end of time - + Views settings - + Tree Дерево - + Colors - + Icon color: - + Nothing - + POV З погляду - + Label Позначка - + Progress Стан - + Compile Експорт - + Text color: - + Background color: - + Folders - + Show ite&m count - + Show summary - + &Nothing - + Text Текст - + Outline Обрис - + Visible columns - + Goal Мета - + Word count - + Percentage - + Title Назва - + Index cards Каталог - + Item colors - + Border color: - + Corner color: - + Background Тло - + Color: - + Ctrl+S Ctrl+S - + Image: - + Text editor - + Font - + Family: - + Size: - + Misspelled: - + Background: - + Paragraphs - + Line spacing: Міжрядковий інтервал: - + Single - + 1.5 lines - + Double - + Proportional - + % - + Tab width: - + px - + Indent 1st line - + Spacing: - + New - + Edit - + Delete - + Theme name: - + Apply - + Cancel - + Window Background - + Text Background - + Text Options - + Paragraph Options - + Type: - + No Image - + Tiled - + Centered - + Stretched - + Scaled - + Zoomed - + Opacity: - + Position: - + Left - + Center - + Right - + Width: - + Corner radius: - + Margins: - + Padding: - + Font: - + Style - + Cursor - + Use block insertion of - + Alignment: - + Justify - + Alignment - + Icon Size - + TextLabel - + Disable blinking - + Text area - + Max width - + Left/Right margins: - + Top/Bottom margins: - + S&how progress - + Show summar&y - + Show p&rogress - + Old st&yle - + Transparent - + Restore defaults - + Style: - + Language: - + Font size: Розмір шрифту: - + Restarting Manuskript ensures all settings take effect. - + Show &word count - + &Show word count - + &New style - + Typewriter mode - + Focus mode - + None - + Sentence - + Line - + Paragraph - + <p><b>The Revisions feature has been at the source of many reported issues. In this version of Manuskript it has been turned off by default for new projects in order to provide the best experience.</b></p><p>Why aren't these issues fixed already? <a href="https://www.theologeek.ch/manuskript/contribute/">We need your help to make Manuskript better!</a></p> + + + Show progress in chars next + to words + + + + + Char/Word Counter + + + + + Count spaces as chars + + + + + Show char c&ount + + + + + Sho&w char count + + SpellAction - + Spelling Suggestions - + &Add to dictionary - + &Remove from custom dictionary + + + &New Character + + + + + &New Plot Item + + + + + &New World Item + + + + + &Correction Suggestions + + + + + &Correction Suggestion + + about @@ -2210,37 +2364,37 @@ Use that if you get YAML related error. abstractModel - + Title Назва - + POV З погляду - + Label Позначка - + Status Стан - + Compile Експорт - + Word count - + Goal Мета @@ -2281,17 +2435,12 @@ Use that if you get YAML related error. characterModel - - New character - - - - + Name Ім'я - + Value @@ -2299,17 +2448,17 @@ Use that if you get YAML related error. characterTreeView - + Main - + Secondary - + Minor @@ -2425,12 +2574,12 @@ Use that if you get YAML related error. corkDelegate - + One line summary - + Full summary Загальний опис @@ -2741,72 +2890,72 @@ Use that if you get YAML related error. fullScreenEditor - + Theme: - + {} words / {} - + {} words - + Spellcheck - + Navigation - + New Text - + Title Назва - + Title: Show Full Path - + Theme selector - + Word count - + Progress Стан - + Progress: Auto Show/Hide - + Clock - + Clock: Show Seconds @@ -2885,14 +3034,6 @@ Use that if you get YAML related error. Налаштування - - lastAccessedDirectoryInfo - - - Last accessed directory "{}" loaded. - - - lineEditView @@ -3007,19 +3148,34 @@ Use that if you get YAML related error. - + Root - - {} words / {} + + {} words + {} слів + + + + ({} chars) {} words / {} - - {} words - {} слів + + {} words / {} + + + + + {} chars + + + + + {} chars + @@ -3071,7 +3227,7 @@ Use that if you get YAML related error. myPanel - + Auto-hide @@ -3225,12 +3381,12 @@ Use that if you get YAML related error. outlineItem - + {} words / {} ({}) - + {} words @@ -3238,17 +3394,17 @@ Use that if you get YAML related error. pandocSettings - + General Загальне - + Table of Content - + Custom settings for {} @@ -3452,37 +3608,32 @@ Use that if you get YAML related error. plotModel - - New plot - - - - + Name Ім'я - + Meta - + New step - + Main - + Secondary - + Minor @@ -3490,22 +3641,22 @@ Use that if you get YAML related error. plotTreeView - + Main - + Secondary - + Minor - + **Plot:** {} @@ -3569,52 +3720,52 @@ Use that if you get YAML related error. references - + Not a reference: {}. - + Unknown reference: {}. - + Path: Шлях: - + Stats: - + POV: - + Status: Стан: - + Label: - + Short summary: - + Long summary: - + Notes: @@ -3634,72 +3785,72 @@ Use that if you get YAML related error. - + Go to {}. - + Description Опис - + Result Результат - + Characters Персонажі - + Resolution steps Кроки розвитку сюжету - + Passion - + Conflict Конфлікт - + <b>Unknown reference:</b> {}. - + Folder: <b>{}</b> - + Text: <b>{}</b> - + Character: <b>{}</b> - + Plot: <b>{}</b> - + World: <b>{name}</b>{path} - + Referenced in: @@ -3742,12 +3893,12 @@ Use that if you get YAML related error. - + Restore - + Delete @@ -3807,12 +3958,12 @@ Use that if you get YAML related error. - + Line {}: - + Clear all @@ -3829,111 +3980,56 @@ Use that if you get YAML related error. Search for... - - - Search in: - - - - - All - - - - - Title - Назва - - - - Text - Текст - - - - Summary - Стислий переказ - - - - Notes - Нотатки - - - - POV - З погляду - - - - Status - Стан - - - - Label - Позначка - - - - Options: - - - - - Case sensitive - - settingsWindow - + New status - + New label - + newtheme - + New theme - + (read-only) - + Open Image - + Image files (*.jpg; *.jpeg; *.png) - + Error Помилка - + Unable to load selected file - + Unable to add selected image: {} @@ -4020,22 +4116,22 @@ Use that if you get YAML related error. tabSplitter - + Open selected items in that view. - + Split horizontally - + Close split - + Split vertically @@ -4043,7 +4139,7 @@ Use that if you get YAML related error. textEditView - + Various @@ -4142,27 +4238,27 @@ Use that if you get YAML related error. - + Novel - + Novella - + Short Story - + Research paper - + Demo projects @@ -4197,147 +4293,147 @@ Use that if you get YAML related error. - + Open project - + Manuskript project (*.msk);;All files (*) - + Save project as... - + Manuskript project (*.msk) - + Manuskript Манускрипт - + Create New Project - + Warning - + Overwrite existing project {} ? - + Empty fiction - + Chapter - + Scene - + Trilogy - + Book - + Section - + Empty non-fiction - + words each. - + of - + Text Текст - + Something - + <b>Total:</b> {} words (~ {} pages) - + Fiction - + Non-fiction - + Idea - + Note - + Research - + TODO - + First draft - + Second draft - + Final @@ -4345,212 +4441,212 @@ Use that if you get YAML related error. worldModel - + New item - + Fantasy world building - + Physical - + Climate - + Topography - + Astronomy - + Wild life - + Flora - + History - + Races - + Diseases - + Cultural - + Customs - + Food - + Languages - + Education - + Dresses - + Science - + Calendar - + Bodily language - + Ethics - + Religion - + Government - + Politics - + Gender roles - + Music and arts - + Architecture - + Military - + Technology - + Courtship - + Demography - + Transportation - + Medicine - + Magic system - + Rules - + Organization - + Magical objects - + Magical places - + Magical races - + Important places - + Important objects - + Natural resources diff --git a/i18n/manuskript_zh_CN.qm b/i18n/manuskript_zh_CN.qm index 82d4d28..34c2a09 100644 Binary files a/i18n/manuskript_zh_CN.qm and b/i18n/manuskript_zh_CN.qm differ diff --git a/i18n/manuskript_zh_HANT.ts b/i18n/manuskript_zh_HANT.ts index 8c64001..06b95b0 100644 --- a/i18n/manuskript_zh_HANT.ts +++ b/i18n/manuskript_zh_HANT.ts @@ -38,7 +38,7 @@ 以熒光筆模式預覽 - + Plain text 純文本 @@ -53,82 +53,82 @@ 需要安裝 LaTeX。 - + Error 錯誤 - + Standalone document (not just a fragment) 獨立文件(不只是分段) - + Include a table of contents. 包含一個目錄。 - + Number of sections level to include in TOC: - + Typographically correct output 排版正確的輸出 - + Normalize the document (cleaner) 規範化檔案(整理) - + Specify the base level for headers: 選擇默認的Header級別 - + Use reference-style links instead of inline links 使用引用樣式連結而不是內聯連結 - + Use ATX-style headers 使用 ATX 樣式 Headers - + Self-contained HTML files, with no dependencies 獨立的 HTML 檔案,無依賴項 - + Use <q> tags for quotes in HTML 在 HTML 中為引號使用<q>標記 - + LaTeX engine used to produce the PDF. PDF使用 LaTeX 引擎生成。 - + Paper size: 紙張大小: - + Font size: 字體大小: - + Class: 類別: - + Line spacing: @@ -159,13 +159,13 @@ - + Simplest export to plain text. Allows you to use your own markup not understood by Manuskript, for example <a href='www.fountain.io'>Fountain</a>. - + <p>A universal document converter. Can be used to convert Markdown to a wide range of other formats.</p> <p>Website: <a href="http://www.pandoc.org">http://pandoc.org/</a></p> @@ -198,24 +198,24 @@ - + Disable YAML metadata block. Use that if you get YAML related error. - + Convert to ePUB3 - + Could not process regular expression: {} - + Choose output file… @@ -324,24 +324,24 @@ Use that if you get YAML related error. Import - + Markdown import - + <b>Info:</b> A very simple parser that will go through a markdown document and create items for each titles.<br/>&nbsp; - + Folder import - + <p><b>Info:</b> Imports a whole directory structure. Folders are added as folders, and plaintext documents within (you chose which ones by extension) @@ -350,47 +350,47 @@ Use that if you get YAML related error. - + Include only those extensions: - + Comma separated values - + Sort items by name - + Import folder then files - + OPML Import - + File open failed. - + This does not appear to be a valid OPML file. - + Pandoc import - + <b>Info:</b> Manuskript can import from <b>markdown</b> or <b>OPML</b>. Pandoc will convert your document to either (see option below), and @@ -400,17 +400,17 @@ Use that if you get YAML related error. - + Import using: - + Wrap lines: - + <p>Should pandoc create cosmetic / non-semantic line-breaks?</p><p> <b>auto</b>: wraps at 72 characters.<br> @@ -420,27 +420,27 @@ Use that if you get YAML related error. - + Mind Map Import - + This does not appear to be a valid Mind Map file. - + Mind Map import - + Import tip as: - + Untitled @@ -456,7 +456,7 @@ Use that if you get YAML related error. MainWindow - + General @@ -496,7 +496,7 @@ Use that if you get YAML related error. - + Name @@ -506,7 +506,7 @@ Use that if you get YAML related error. - + Summary @@ -516,7 +516,7 @@ Use that if you get YAML related error. - + Summary: @@ -526,17 +526,17 @@ Use that if you get YAML related error. - + One paragraph - + One page - + Full @@ -566,7 +566,7 @@ Use that if you get YAML related error. - + Next @@ -586,312 +586,312 @@ Use that if you get YAML related error. - + Filter - + Basic info - + Importance - + Motivation - + Goal - + Conflict - + Epiphany - + <html><head/><body><p align="right">One sentence<br/>summary</p></body></html> - + <html><head/><body><p align="right">One paragraph<br/>summary</p></body></html> - + Notes - + Detailed info - + Plots - + Plot - + Character(s) - + Description - + Result - + Resolution steps - + World - + Populates with empty data - + More - + Source of passion - + Source of conflict - + Outline - + Editor - + Debug - + FlatData - + Persos - + Labels - + &File - + &Recent - + &Help - + &Tools - + &Edit - + &View - + &Mode - + &Cheat sheet - + Sea&rch - + &Navigation - + &Open - + Ctrl+O - + &Save - + Ctrl+S - + Sa&ve as... - + Ctrl+Shift+S - + &Quit - + Ctrl+Q - + &Show help texts - + Ctrl+Shift+B - + &Spellcheck - + F9 - + &Labels... - + &Status... - + Tree - + &Simple - + &Fiction - + Index cards - + S&ettings - + F8 - + &Close project - + Co&mpile - + F6 - + &Frequency Analyzer @@ -901,562 +901,665 @@ Use that if you get YAML related error. - + &About - + About Manuskript - + Manuskript - + Project {} saved. - + WARNING: Project {} not saved. - + Project {} loaded. - - Project {} loaded with some errors: - - - - - * {} wasn't found in project file. - - - - + Project {} loaded with some errors. - + (~{} pages) - + Words: {}{} - + Book summary - + Project tree - + Metadata - + Story line - + Enter information about your book, and yourself. - + The basic situation, in the form of a 'What if...?' question. Ex: 'What if the most dangerous evil wizard wasn't able to kill a baby?' (Harry Potter) - + Take time to think about a one sentence (~50 words) summary of your book. Then expand it to a paragraph, then to a page, then to a full summary. - + Create your characters. - + Develop plots. - + Build worlds. Create hierarchy of broad categories down to specific details. - + Create the outline of your masterpiece. - + Write. - + Debug info. Sometimes useful. - + Dictionary - + Nothing - + POV - + Label - + Progress - + Compile - + Icon color - + Text color - + Background color - + Icon - + Text - + Background - + Border - + Corner - + Add plot step - + &Import… - + F7 - + &Copy - + Ctrl+C - + C&ut - + Ctrl+X - + &Paste - + Ctrl+V - + &Split… - + Ctrl+Shift+K - + Sp&lit at cursor - + Ctrl+K - + Ctrl+M - + Ctrl+D - + Del - + &Move Up - + Ctrl+Shift+Up - + M&ove Down - + Ctrl+Shift+Down - + Dupl&icate - + &Delete - + &Rename - + F2 - + Organi&ze - + M&erge - + &Format - + &Header - + &Level 1 (setext) - + Ctrl+Alt+1 - + Level &2 - + Ctrl+Alt+2 - + Level &1 (atx) - + Ctrl+1 - + L&evel 2 - + Ctrl+2 - + Level &3 - + Ctrl+3 - + Level &4 - + Ctrl+4 - + Level &5 - + Ctrl+5 - + Level &6 - + Ctrl+6 - + &Bold - + Ctrl+B - + &Italic - + Ctrl+I - + &Strike - + &Verbatim - + Su&perscript - + Ctrl++ - + Subsc&ript - + Ctrl+- - + Co&mment block - + Ctrl+Shift+C - + Clear &formats - + Ctrl+0 - + &Comment line(s) - + &Ordered list - + &Unordered list - + B&lockquote - + Remove selected plot step(s) - + The file {} does not exist. Has it been moved or deleted? - + Install {}{} to use spellcheck - + {} has no installed dictionaries - + {}{} is not installed - + Save project? - + Save changes to project "{}" before closing? - + Your changes will be lost if you don't save them. - + PyQt / Qt versions 5.11 and 5.12 are known to cause a crash which might result in a loss of data. - + PyQt {} and Qt {} are in use. - + Proceed with import at your own risk + + + Allow POV + + + + + Search + + + + + Ctrl+F + + + + + F3 + + + + + Shift+F3 + + + + + Situation + + + + + Status + + + + + &Technical Support + + + + + How to obtain technical support for Manuskript. + + + + + F1 + + + + + &Locate log file... + + + + + Locate log file + + + + + Locate the diagnostic log file used for this session. + + + + + Shift+F1 + + + + + Sorry! + + + + + This session is not being logged. + + + + + A log file is a Work in Progress! + + + + + The log file "{}" will continue to be written to until Manuskript is closed. + + + + + It will now be displayed in your file manager, but is of limited use until you close Manuskript. + + + + + Error! + + + + + An error was encountered while trying to show the log file below in your file manager. + + + + + Search + + + No results found + + Settings @@ -1471,7 +1574,7 @@ Use that if you get YAML related error. - + Revisions @@ -1481,17 +1584,17 @@ Use that if you get YAML related error. - + Labels - + Status - + Fullscreen @@ -1506,658 +1609,709 @@ Use that if you get YAML related error. - + Loading - + Automatically load last project on startup - + Saving - + Automatically save every - + minutes. - + If no changes during - + seconds. - + Save on project close - + <html><head/><body><p>If you check this option, your project will be saved as one single file. Easier to copy or backup, but does not allow collaborative editing, or versioning.<br/>If this is unchecked, your project will be saved as a folder containing many small files.</p></body></html> - + Save to one single file - + Revisions are a way to keep track of modifications. For each text item, it stores any changes you make to the main text, allowing you to see and restoring previous versions. - + Keep revisions - + S&mart remove - + Keep: - + Smart remove allows you to keep only a certain number of revisions. It is strongly recommended to use it, lest you file will becomes full of thousands of insignificant changes. - + revisions per day for the last month - + revisions per minute for the last 10 minutes - + revisions per hour for the last day - + revisions per 10 minutes for the last hour - + revisions per week till the end of time - + Views settings - + Tree - + Colors - + Icon color: - + Nothing - + POV - + Label - + Progress - + Compile - + Text color: - + Background color: - + Folders - + Show ite&m count - + Show summary - + &Nothing - + Text - + Outline - + Visible columns - + Goal - + Word count - + Percentage - + Title - + Index cards - + Item colors - + Border color: - + Corner color: - + Background - + Color: - + Ctrl+S - + Image: - + Text editor - + Font - + Family: - + Size: - + Misspelled: - + Background: - + Paragraphs - + Line spacing: - + Single - + 1.5 lines - + Double - + Proportional - + % - + Tab width: - + px - + Indent 1st line - + Spacing: - + New - + Edit - + Delete - + Theme name: - + Apply - + Cancel - + Window Background - + Text Background - + Text Options - + Paragraph Options - + Type: - + No Image - + Tiled - + Centered - + Stretched - + Scaled - + Zoomed - + Opacity: - + Position: - + Left - + Center - + Right - + Width: - + Corner radius: - + Margins: - + Padding: - + Font: - + Style - + Cursor - + Use block insertion of - + Alignment: - + Justify - + Alignment - + Icon Size - + TextLabel - + Disable blinking - + Text area - + Max width - + Left/Right margins: - + Top/Bottom margins: - + S&how progress - + Show summar&y - + Show p&rogress - + Old st&yle - + Transparent - + Restore defaults - + Style: - + Language: - + Font size: 字體大小: - + Restarting Manuskript ensures all settings take effect. - + Show &word count - + &Show word count - + &New style - + Typewriter mode - + Focus mode - + None - + Sentence - + Line - + Paragraph - + <p><b>The Revisions feature has been at the source of many reported issues. In this version of Manuskript it has been turned off by default for new projects in order to provide the best experience.</b></p><p>Why aren't these issues fixed already? <a href="https://www.theologeek.ch/manuskript/contribute/">We need your help to make Manuskript better!</a></p> + + + Show progress in chars next + to words + + + + + Char/Word Counter + + + + + Count spaces as chars + + + + + Show char c&ount + + + + + Sho&w char count + + SpellAction - + Spelling Suggestions - + &Add to dictionary - + &Remove from custom dictionary + + + &New Character + + + + + &New Plot Item + + + + + &New World Item + + + + + &Correction Suggestions + + + + + &Correction Suggestion + + about @@ -2188,37 +2342,37 @@ Use that if you get YAML related error. abstractModel - + Title - + POV - + Label - + Status - + Compile - + Word count - + Goal @@ -2259,17 +2413,12 @@ Use that if you get YAML related error. characterModel - - New character - - - - + Name - + Value @@ -2277,17 +2426,17 @@ Use that if you get YAML related error. characterTreeView - + Main - + Secondary - + Minor @@ -2403,12 +2552,12 @@ Use that if you get YAML related error. corkDelegate - + One line summary - + Full summary @@ -2719,72 +2868,72 @@ Use that if you get YAML related error. fullScreenEditor - + Theme: - + {} words / {} - + {} words - + Spellcheck - + Navigation - + New Text - + Title - + Title: Show Full Path - + Theme selector - + Word count - + Progress - + Progress: Auto Show/Hide - + Clock - + Clock: Show Seconds @@ -2863,14 +3012,6 @@ Use that if you get YAML related error. - - lastAccessedDirectoryInfo - - - Last accessed directory "{}" loaded. - - - lineEditView @@ -2985,18 +3126,33 @@ Use that if you get YAML related error. - + Root - - {} words / {} + + {} words - - {} words + + ({} chars) {} words / {} + + + + + {} words / {} + + + + + {} chars + + + + + {} chars @@ -3049,7 +3205,7 @@ Use that if you get YAML related error. myPanel - + Auto-hide @@ -3203,12 +3359,12 @@ Use that if you get YAML related error. outlineItem - + {} words / {} ({}) - + {} words @@ -3216,17 +3372,17 @@ Use that if you get YAML related error. pandocSettings - + General - + Table of Content - + Custom settings for {} @@ -3430,37 +3586,32 @@ Use that if you get YAML related error. plotModel - - New plot - - - - + Name - + Meta - + New step - + Main - + Secondary - + Minor @@ -3468,22 +3619,22 @@ Use that if you get YAML related error. plotTreeView - + Main - + Secondary - + Minor - + **Plot:** {} @@ -3547,52 +3698,52 @@ Use that if you get YAML related error. references - + Not a reference: {}. - + Unknown reference: {}. - + Path: - + Stats: - + POV: - + Status: - + Label: - + Short summary: - + Long summary: - + Notes: @@ -3612,72 +3763,72 @@ Use that if you get YAML related error. - + Go to {}. - + Description - + Result - + Characters - + Resolution steps - + Passion - + Conflict - + <b>Unknown reference:</b> {}. - + Folder: <b>{}</b> - + Text: <b>{}</b> - + Character: <b>{}</b> - + Plot: <b>{}</b> - + World: <b>{name}</b>{path} - + Referenced in: @@ -3720,12 +3871,12 @@ Use that if you get YAML related error. - + Restore - + Delete @@ -3785,12 +3936,12 @@ Use that if you get YAML related error. - + Line {}: - + Clear all @@ -3807,111 +3958,56 @@ Use that if you get YAML related error. Search for... - - - Search in: - - - - - All - - - - - Title - - - - - Text - - - - - Summary - - - - - Notes - - - - - POV - - - - - Status - - - - - Label - - - - - Options: - - - - - Case sensitive - - settingsWindow - + New status - + New label - + newtheme - + New theme - + (read-only) - + Open Image - + Image files (*.jpg; *.jpeg; *.png) - + Error 錯誤 - + Unable to load selected file - + Unable to add selected image: {} @@ -3998,22 +4094,22 @@ Use that if you get YAML related error. tabSplitter - + Open selected items in that view. - + Split horizontally - + Close split - + Split vertically @@ -4021,7 +4117,7 @@ Use that if you get YAML related error. textEditView - + Various @@ -4120,27 +4216,27 @@ Use that if you get YAML related error. - + Novel - + Novella - + Short Story - + Research paper - + Demo projects @@ -4175,147 +4271,147 @@ Use that if you get YAML related error. - + Open project - + Manuskript project (*.msk);;All files (*) - + Save project as... - + Manuskript project (*.msk) - + Manuskript - + Create New Project - + Warning - + Overwrite existing project {} ? - + Empty fiction - + Chapter - + Scene - + Trilogy - + Book - + Section - + Empty non-fiction - + words each. - + of - + Text - + Something - + <b>Total:</b> {} words (~ {} pages) - + Fiction - + Non-fiction - + Idea - + Note - + Research - + TODO - + First draft - + Second draft - + Final @@ -4323,212 +4419,212 @@ Use that if you get YAML related error. worldModel - + New item - + Fantasy world building - + Physical - + Climate - + Topography - + Astronomy - + Wild life - + Flora - + History - + Races - + Diseases - + Cultural - + Customs - + Food - + Languages - + Education - + Dresses - + Science - + Calendar - + Bodily language - + Ethics - + Religion - + Government - + Politics - + Gender roles - + Music and arts - + Architecture - + Military - + Technology - + Courtship - + Demography - + Transportation - + Medicine - + Magic system - + Rules - + Organization - + Magical objects - + Magical places - + Magical races - + Important places - + Important objects - + Natural resources diff --git a/main.pyproject b/main.pyproject new file mode 100644 index 0000000..2cad84e --- /dev/null +++ b/main.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["manuskript/ui/editors/completer.py","manuskript/ui/views/outlineBasics.py","manuskript/ui/exporters/exporter.py","manuskript/tests/models/conftest.py","manuskript/ui/views/textEditView.py","manuskript/ui/revisions.py","manuskript/ui/editors/__init__.py","manuskript/tests/ui/importers/test_importers.py","manuskript/ui/views/outlineDelegates.py","manuskript/ui/exporters/manuskript/plainTextSettings_ui.ui","manuskript/enums.py","manuskript/ui/editors/textFormat_ui.py","manuskript/ui/highlighters/__init__.py","manuskript/converters/pandocConverter.py","manuskript/models/outlineModel.py","manuskript/models/characterPOVModel.py","manuskript/ui/editors/textFormat.py","manuskript/ui/editors/completer_ui.ui","manuskript/ui/settings_ui.py","manuskript/ui/views/storylineView_ui.ui","manuskript/ui/cheatSheet_ui.py","manuskript/ui/search.py","manuskript/settings.py","manuskript/importer/opmlImporter.py","manuskript/ui/views/treeView.py","manuskript/ui/exporters/exporter_ui.ui","manuskript/ui/editors/mainEditor_ui.py","manuskript/converters/markdownConverter.py","manuskript/models/persosProxyModel.py","manuskript/ui/editors/locker_ui.py","manuskript/ui/views/MDEditView.py","manuskript/ui/welcome_ui.py","manuskript/ui/views/propertiesView_ui.py","manuskript/ui/collapsibleGroupBox.py","manuskript/ui/editors/blockUserData.py","manuskript/models/references.py","manuskript/converters/__init__.py","manuskript/ui/views/corkDelegate.py","manuskript/ui/tools/__init__.py","manuskript/ui/style.py","manuskript/ui/tools/frequency_ui.py","manuskript/ui/views/cmbOutlineLabelChoser.py","manuskript/exporter/pandoc/HTML.py","manuskript/ui/views/propertiesView.py","manuskript/exporter/pandoc/outputFormats.py","manuskript/ui/views/sldImportance_ui.py","manuskript/ui/highlighters/MMDHighlighter.py","manuskript/ui/editors/textFormat_ui.ui","manuskript/tests/test_functions.py","manuskript/ui/mainWindow.ui","manuskript/ui/about_ui.py","manuskript/tests/test_settingsWindow.py","manuskript/tests/ui/exporters/__init__.py","manuskript/exporter/manuskript/__init__.py","manuskript/ui/editors/mainEditor.py","manuskript/ui/exporters/manuskript/__init__.py","manuskript/exporter/pandoc/abstractOutput.py","manuskript/tests/ui/test_welcome.py","manuskript/ui/cheatSheet_ui.ui","manuskript/importer/__init__.py","manuskript/ui/exporters/exportersManager.py","manuskript/exporter/pandoc/PDF.py","manuskript/ui/views/metadataView.py","manuskript/importer/markdownImporter.py","manuskript/ui/editors/tabSplitter_ui.py","manuskript/ui/views/cmbOutlineStatusChoser.py","manuskript/ui/editors/completer_ui.py","manuskript/tests/models/test_outlineItem.py","manuskript/ui/search_ui.py","manuskript/ui/views/cmbOutlineCharacterChoser.py","manuskript/load_save/version_1.py","manuskript/ui/search_ui.ui","manuskript/ui/views/plotTreeView.py","manuskript/ui/editors/mainEditor_ui.ui","manuskript/ui/views/PDFViewer.py","manuskript/settingsWindow.py","manuskript/loadSave.py","manuskript/ui/views/lineEditView.py","manuskript/ui/exporters/exportersManager_ui.ui","manuskript/exporter/manuskript/markdown.py","manuskript/importer/pandocImporters.py","manuskript/mainWindow.py","manuskript/ui/importers/generalSettings_ui.ui","manuskript/functions/__init__.py","manuskript/models/abstractItem.py","manuskript/ui/editors/locker.py","manuskript/ui/editors/tabSplitter_ui.ui","manuskript/ui/exporters/__init__.py","manuskript/ui/exporters/exportersManager_ui.py","manuskript/models/__init__.py","manuskript/ui/exporters/manuskript/plainTextSettings.py","manuskript/ui/editors/MDFunctions.py","manuskript/ui/views/sldImportance.py","manuskript/ui/views/basicItemView_ui.ui","manuskript/tests/models/test_references.py","manuskript/exporter/manuskript/HTML.py","manuskript/ui/views/metadataView_ui.ui","manuskript/exporter/__init__.py","manuskript/ui/views/outlineView.py","manuskript/main.py","manuskript/ui/revisions_ui.ui","manuskript/tests/ui/exporters/test_exporters.py","manuskript/ui/views/storylineView_ui.py","manuskript/tests/ui/__init__.py","manuskript/ui/about_ui.ui","manuskript/models/outlineItem.py","manuskript/__init__.py","manuskript/ui/statusLabel.py","manuskript/models/plotModel.py","manuskript/tests/__init__.py","manuskript/ui/views/dndView.py","manuskript/ui/tools/splitDialog.py","manuskript/ui/exporters/exporter_ui.py","manuskript/ui/settings_ui.ui","manuskript/ui/about.py","manuskript/ui/welcome.py","manuskript/ui/importers/importer.py","manuskript/ui/exporters/manuskript/plainTextSettings_ui.py","manuskript/importer/abstractImporter.py","manuskript/ui/views/__init__.py","manuskript/ui/highlighters/markdownEnums.py","manuskript/ui/importers/importer_ui.py","manuskript/ui/tools/frequency_ui.ui","manuskript/ui/helpLabel.py","manuskript/load_save/version_0.py","manuskript/ui/editors/editorWidget_ui.py","manuskript/ui/importers/__init__.py","manuskript/ui/editors/editorWidget.py","manuskript/ui/importers/generalSettings_ui.py","manuskript/functions/spellchecker.py","manuskript/ui/collapsibleGroupBox2.py","manuskript/ui/__init__.py","manuskript/ui/welcome_ui.ui","manuskript/ui/views/propertiesView_ui.ui","manuskript/tests/conftest.py","manuskript/ui/mainWindow.py","manuskript/ui/collapsibleDockWidgets.py","manuskript/ui/editors/tabSplitter.py","manuskript/importer/mindMapImporter.py","manuskript/ui/highlighters/markdownTokenizer.py","manuskript/ui/tools/frequencyAnalyzer.py","manuskript/converters/abstractConverter.py","manuskript/ui/editors/fullScreenEditor.py","manuskript/ui/editors/themes.py","manuskript/ui/views/webView.py","manuskript/load_save/__init__.py","manuskript/ui/views/corkView.py","manuskript/tests/ui/importers/__init__.py","manuskript/ui/editors/editorWidget_ui.ui","manuskript/ui/highlighters/markdownHighlighter.py","manuskript/version.py","manuskript/importer/folderImporter.py","manuskript/ui/views/sldImportance_ui.ui","manuskript/models/plotsProxyModel.py","manuskript/ui/views/storylineView.py","manuskript/models/characterModel.py","manuskript/ui/views/metadataView_ui.py","manuskript/ui/cheatSheet.py","manuskript/ui/views/plotDelegate.py","manuskript/exporter/manuskript/plainText.py","manuskript/ui/editors/locker_ui.ui","manuskript/ui/revisions_ui.py","manuskript/exporter/basic.py","manuskript/ui/highlighters/basicHighlighter.py","manuskript/ui/importers/generalSettings.py","manuskript/ui/views/MDEditCompleter.py","manuskript/exporter/pandoc/abstractPlainText.py","manuskript/ui/views/treeDelegates.py","manuskript/ui/views/chkOutlineCompile.py","manuskript/ui/views/basicItemView_ui.py","manuskript/models/worldModel.py","manuskript/models/abstractModel.py","manuskript/exporter/pandoc/plainText.py","manuskript/ui/views/characterTreeView.py","manuskript/tests/models/__init__.py","manuskript/exporter/pandoc/__init__.py","manuskript/ui/importers/importer_ui.ui","manuskript/ui/views/basicItemView.py","bin/manuskript"] +} diff --git a/manuskript/converters/markdownConverter.py b/manuskript/converters/markdownConverter.py index f0e577b..6d4f2a4 100644 --- a/manuskript/converters/markdownConverter.py +++ b/manuskript/converters/markdownConverter.py @@ -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) diff --git a/manuskript/converters/pandocConverter.py b/manuskript/converters/pandocConverter.py index ea91e9a..566c580 100644 --- a/manuskript/converters/pandocConverter.py +++ b/manuskript/converters/pandocConverter.py @@ -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 diff --git a/manuskript/enums.py b/manuskript/enums.py index 4e94d0b..3b3658d 100644 --- a/manuskript/enums.py +++ b/manuskript/enums.py @@ -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 \ No newline at end of file diff --git a/manuskript/exporter/basic.py b/manuskript/exporter/basic.py index 4fa4ad2..7f02f9c 100644 --- a/manuskript/exporter/basic.py +++ b/manuskript/exporter/basic.py @@ -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() # diff --git a/manuskript/exporter/manuskript/HTML.py b/manuskript/exporter/manuskript/HTML.py index 9f89087..89c0dcf 100644 --- a/manuskript/exporter/manuskript/HTML.py +++ b/manuskript/exporter/manuskript/HTML.py @@ -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) diff --git a/manuskript/exporter/manuskript/plainText.py b/manuskript/exporter/manuskript/plainText.py index 250f6bf..187e863 100644 --- a/manuskript/exporter/manuskript/plainText.py +++ b/manuskript/exporter/manuskript/plainText.py @@ -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: diff --git a/manuskript/exporter/pandoc/PDF.py b/manuskript/exporter/pandoc/PDF.py index 5adf485..7e67c42 100644 --- a/manuskript/exporter/pandoc/PDF.py +++ b/manuskript/exporter/pandoc/PDF.py @@ -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() diff --git a/manuskript/exporter/pandoc/__init__.py b/manuskript/exporter/pandoc/__init__.py index a920fcb..a9637e9 100644 --- a/manuskript/exporter/pandoc/__init__.py +++ b/manuskript/exporter/pandoc/__init__.py @@ -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 diff --git a/manuskript/exporter/pandoc/abstractPlainText.py b/manuskript/exporter/pandoc/abstractPlainText.py index 5cfea99..8c5a07b 100644 --- a/manuskript/exporter/pandoc/abstractPlainText.py +++ b/manuskript/exporter/pandoc/abstractPlainText.py @@ -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.")), diff --git a/manuskript/functions/__init__.py b/manuskript/functions/__init__.py index 8d49192..4bce00b 100644 --- a/manuskript/functions/__init__.py +++ b/manuskript/functions/__init__.py @@ -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 += '' + text[startPos:endPos].replace('\n', '') + '' + + 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 diff --git a/manuskript/functions/spellchecker.py b/manuskript/functions/spellchecker.py index da41f0d..2e884d0 100644 --- a/manuskript/functions/spellchecker.py +++ b/manuskript/functions/spellchecker.py @@ -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) diff --git a/manuskript/importer/mindMapImporter.py b/manuskript/importer/mindMapImporter.py index 31155c9..7999bdf 100644 --- a/manuskript/importer/mindMapImporter.py +++ b/manuskript/importer/mindMapImporter.py @@ -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)) diff --git a/manuskript/importer/opmlImporter.py b/manuskript/importer/opmlImporter.py index e434cd5..79d871e 100644 --- a/manuskript/importer/opmlImporter.py +++ b/manuskript/importer/opmlImporter.py @@ -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 diff --git a/manuskript/importer/pandocImporters.py b/manuskript/importer/pandocImporters.py index 6e281cc..9a0e99d 100644 --- a/manuskript/importer/pandocImporters.py +++ b/manuskript/importer/pandocImporters.py @@ -38,7 +38,7 @@ class pandocImporter(abstractImporter): r = pandocExporter().run(args) - if r is None: + if r == None: return None if formatTo == "opml": diff --git a/manuskript/loadSave.py b/manuskript/loadSave.py index e0d95b2..7657376 100644 --- a/manuskript/loadSave.py +++ b/manuskript/loadSave.py @@ -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) diff --git a/manuskript/load_save/version_0.py b/manuskript/load_save/version_0.py index e89aa22..a64f851 100644 --- a/manuskript/load_save/version_0.py +++ b/manuskript/load_save/version_0.py @@ -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) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index 6c4f91d..617dcd6 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -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) diff --git a/manuskript/logging.py b/manuskript/logging.py new file mode 100644 index 0000000..cced038 --- /dev/null +++ b/manuskript/logging.py @@ -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 ""), msg_log_context.line) + ) + qtcl.log(logging.DEBUG, + ' ! {0}'.format((msg_log_context.function or "")) + ) + 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. diff --git a/manuskript/main.py b/manuskript/main.py index 63bc9cf..c4d5bb6 100644 --- a/manuskript/main.py +++ b/manuskript/main.py @@ -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() diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index 5afb0fa..4778490 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -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!"), + "

" + + self.tr("This session is not being logged.") + + "

", + 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!"), + "

" + + self.tr("The log file \"{}\" will continue to be written to until Manuskript is closed.").format(os.path.basename(logfile)) + + "

" + + "

" + + self.tr("It will now be displayed in your file manager, but is of limited use until you close Manuskript.") + + "

", + 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!"), + "

" + + self.tr("An error was encountered while trying to show the log file below in your file manager.") + + "

" + + "

" + + logfile + + "

", + 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"), diff --git a/manuskript/models/abstractItem.py b/manuskript/models/abstractItem.py index f3fa049..fb38691 100644 --- a/manuskript/models/abstractItem.py +++ b/manuskript/models/abstractItem.py @@ -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 diff --git a/manuskript/models/abstractModel.py b/manuskript/models/abstractModel.py index a536084..2ec3bfc 100644 --- a/manuskript/models/abstractModel.py +++ b/manuskript/models/abstractModel.py @@ -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) diff --git a/manuskript/models/characterModel.py b/manuskript/models/characterModel.py index a42f8f0..ae0eab9 100644 --- a/manuskript/models/characterModel.py +++ b/manuskript/models/characterModel.py @@ -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 diff --git a/manuskript/models/characterPOVModel.py b/manuskript/models/characterPOVModel.py new file mode 100644 index 0000000..fbbe419 --- /dev/null +++ b/manuskript/models/characterPOVModel.py @@ -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)) diff --git a/manuskript/models/flatDataModelWrapper.py b/manuskript/models/flatDataModelWrapper.py new file mode 100644 index 0000000..57ac262 --- /dev/null +++ b/manuskript/models/flatDataModelWrapper.py @@ -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] \ No newline at end of file diff --git a/manuskript/models/outlineItem.py b/manuskript/models/outlineItem.py index 56b6464..97160aa 100644 --- a/manuskript/models/outlineItem.py +++ b/manuskript/models/outlineItem.py @@ -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 diff --git a/manuskript/models/outlineModel.py b/manuskript/models/outlineModel.py index 63494e2..162ce80 100644 --- a/manuskript/models/outlineModel.py +++ b/manuskript/models/outlineModel.py @@ -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 diff --git a/manuskript/models/plotModel.py b/manuskript/models/plotModel.py index fe01bd0..113b75e 100644 --- a/manuskript/models/plotModel.py +++ b/manuskript/models/plotModel.py @@ -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 \ No newline at end of file diff --git a/manuskript/models/references.py b/manuskript/models/references.py index 17696c8..9baa8db 100644 --- a/manuskript/models/references.py +++ b/manuskript/models/references.py @@ -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 diff --git a/manuskript/models/searchFilter.py b/manuskript/models/searchFilter.py new file mode 100644 index 0000000..ae2096d --- /dev/null +++ b/manuskript/models/searchFilter.py @@ -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 diff --git a/manuskript/models/searchResultModel.py b/manuskript/models/searchResultModel.py new file mode 100644 index 0000000..07ad038 --- /dev/null +++ b/manuskript/models/searchResultModel.py @@ -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 diff --git a/manuskript/models/searchableItem.py b/manuskript/models/searchableItem.py new file mode 100644 index 0000000..25ca23b --- /dev/null +++ b/manuskript/models/searchableItem.py @@ -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) diff --git a/manuskript/models/searchableModel.py b/manuskript/models/searchableModel.py new file mode 100644 index 0000000..c7246b9 --- /dev/null +++ b/manuskript/models/searchableModel.py @@ -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 diff --git a/manuskript/models/worldModel.py b/manuskript/models/worldModel.py index 61254cd..475736a 100644 --- a/manuskript/models/worldModel.py +++ b/manuskript/models/worldModel.py @@ -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)) + diff --git a/manuskript/searchLabels.py b/manuskript/searchLabels.py new file mode 100644 index 0000000..587e468 --- /dev/null +++ b/manuskript/searchLabels.py @@ -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" +} diff --git a/manuskript/settings.py b/manuskript/settings.py index 96a658e..56eb0ec 100644 --- a/manuskript/settings.py +++ b/manuskript/settings.py @@ -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"] diff --git a/manuskript/settingsWindow.py b/manuskript/settingsWindow.py index 4ae34bf..4802410 100644 --- a/manuskript/settingsWindow.py +++ b/manuskript/settingsWindow.py @@ -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) diff --git a/manuskript/tests/__init__.py b/manuskript/tests/__init__.py index 19c56d6..54a0fe1 100644 --- a/manuskript/tests/__init__.py +++ b/manuskript/tests/__init__.py @@ -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: diff --git a/manuskript/tests/conftest.py b/manuskript/tests/conftest.py index 09629e4..ff212ba 100644 --- a/manuskript/tests/conftest.py +++ b/manuskript/tests/conftest.py @@ -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 diff --git a/manuskript/tests/models/test_outlineItem.py b/manuskript/tests/models/test_outlineItem.py index cc681c0..e217309 100644 --- a/manuskript/tests/models/test_outlineItem.py +++ b/manuskript/tests/models/test_outlineItem.py @@ -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 diff --git a/manuskript/tests/models/test_references.py b/manuskript/tests/models/test_references.py index c49e5a5..b9fb2d0 100644 --- a/manuskript/tests/models/test_references.py +++ b/manuskript/tests/models/test_references.py @@ -39,7 +39,7 @@ def test_references(MWSampleProject): assert "\n" in Ref.infos(Ref.plotReference(plotID)) assert "Not a ref" in Ref.infos("") 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("") == -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("") == -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("") == -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("") == -1 @@ -84,9 +84,9 @@ def test_references(MWSampleProject): # Titles for ref in refs: - assert Ref.title(ref) is not None - assert Ref.title("") is None - assert Ref.title(Ref.plotReference("999")) is None + assert Ref.title(ref) != None + assert Ref.title("") == 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("") for ref in refs: - assert Ref.tooltip(ref) is not None + assert Ref.tooltip(ref) != None # Links - assert Ref.refToLink("") is None + assert Ref.refToLink("") == 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 "") is None + assert Ref.open("") == None assert Ref.open(Ref.plotReference("999")) == False assert Ref.open(Ref.characterReference("999")) == False assert Ref.open(Ref.textReference("999")) == False diff --git a/manuskript/tests/models/test_searchFilter.py b/manuskript/tests/models/test_searchFilter.py new file mode 100644 index 0000000..8c484d0 --- /dev/null +++ b/manuskript/tests/models/test_searchFilter.py @@ -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 diff --git a/manuskript/tests/models/test_searchResultModel.py b/manuskript/tests/models/test_searchResultModel.py new file mode 100644 index 0000000..62d71ca --- /dev/null +++ b/manuskript/tests/models/test_searchResultModel.py @@ -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 Lucas") + 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 Lucas" + diff --git a/manuskript/tests/test_functions.py b/manuskript/tests/test_functions.py index fbea5ab..fc8dffc 100644 --- a/manuskript/tests/test_functions.py +++ b/manuskript/tests/test_functions.py @@ -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, "text")] + + +def test_search_singleLine_start(): + assert F.search(re.compile("text"), "text is this") == [(0, 4, "text is this")] + + +def test_search_singleLine_end(): + assert F.search(re.compile("text"), "This is text") == [(8, 12, "This is text")] + + +def test_search_multipleLines_fullMatch(): + assert F.search(re.compile("text"), "This is\ntext\nOK") == [(8, 12, "[...] text [...]")] + + +def test_search_multipleLines_start(): + assert F.search(re.compile("text"), "This is\ntext oh yeah\nOK") == [(8, 12, "[...] text oh yeah [...]")] + + +def test_search_multipleLines_end(): + assert F.search(re.compile("text"), "This is\nsome text\nOK") == [(13, 17, "[...] some text [...]")] + +def test_search_multipleLines_full(): + assert F.search(re.compile("text"), "This is\ntext\nOK") == [(8, 12, "[...] text [...]")] + + +def test_search_multiple_strMatches(): + assert F.search(re.compile("text"), "text, text and more text") == [ + (0, 4, "text, text and more text"), + (6, 10, "text, text and more text"), + (20, 24, "text, text and more text") + ] + + +def test_search_multiple_strMatches_caseSensitive(): + assert F.search(re.compile("text"), "TeXt, TEXT and more text") == [(20, 24, "TeXt, TEXT and more text")] + + assert F.search(re.compile("text", re.IGNORECASE), "TeXt, TEXT and more text") == [ + (0, 4, "TeXt, TEXT and more text"), + (6, 10, "TeXt, TEXT and more text"), + (20, 24, "TeXt, TEXT and more text") + ] \ No newline at end of file diff --git a/manuskript/tests/test_settingsWindow.py b/manuskript/tests/test_settingsWindow.py index 8edc6f3..71ed09f 100644 --- a/manuskript/tests/test_settingsWindow.py +++ b/manuskript/tests/test_settingsWindow.py @@ -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() diff --git a/manuskript/tests/ui/test_searchMenu.py b/manuskript/tests/ui/test_searchMenu.py new file mode 100644 index 0000000..659c471 --- /dev/null +++ b/manuskript/tests/ui/test_searchMenu.py @@ -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() diff --git a/manuskript/ui/collapsibleDockWidgets.py b/manuskript/ui/collapsibleDockWidgets.py index 08c7331..5d2f18a 100644 --- a/manuskript/ui/collapsibleDockWidgets.py +++ b/manuskript/ui/collapsibleDockWidgets.py @@ -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) diff --git a/manuskript/ui/collapsibleGroupBox.py b/manuskript/ui/collapsibleGroupBox.py index 2b3cc04..e6c1b44 100644 --- a/manuskript/ui/collapsibleGroupBox.py +++ b/manuskript/ui/collapsibleGroupBox.py @@ -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) diff --git a/manuskript/ui/editors/MDFunctions.py b/manuskript/ui/editors/MDFunctions.py index 0fbd8b8..058de34 100644 --- a/manuskript/ui/editors/MDFunctions.py +++ b/manuskript/ui/editors/MDFunctions.py @@ -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 \ No newline at end of file diff --git a/manuskript/ui/editors/blockUserData.py b/manuskript/ui/editors/blockUserData.py index 7f39c2c..44340f5 100644 --- a/manuskript/ui/editors/blockUserData.py +++ b/manuskript/ui/editors/blockUserData.py @@ -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 diff --git a/manuskript/ui/editors/fullScreenEditor.py b/manuskript/ui/editors/fullScreenEditor.py index 712d241..10b8657 100644 --- a/manuskript/ui/editors/fullScreenEditor.py +++ b/manuskript/ui/editors/fullScreenEditor.py @@ -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)) diff --git a/manuskript/ui/editors/locker.py b/manuskript/ui/editors/locker.py index d9a648e..2a007a3 100644 --- a/manuskript/ui/editors/locker.py +++ b/manuskript/ui/editors/locker.py @@ -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)) diff --git a/manuskript/ui/editors/mainEditor.py b/manuskript/ui/editors/mainEditor.py index 967f283..7ee4300 100644 --- a/manuskript/ui/editors/mainEditor.py +++ b/manuskript/ui/editors/mainEditor.py @@ -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) diff --git a/manuskript/ui/editors/tabSplitter.py b/manuskript/ui/editors/tabSplitter.py index 13adfaf..d6f4ff3 100644 --- a/manuskript/ui/editors/tabSplitter.py +++ b/manuskript/ui/editors/tabSplitter.py @@ -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 diff --git a/manuskript/ui/highlighters/basicHighlighter.py b/manuskript/ui/highlighters/basicHighlighter.py index 362ee5a..2e7358e 100644 --- a/manuskript/ui/highlighters/basicHighlighter.py +++ b/manuskript/ui/highlighters/basicHighlighter.py @@ -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) diff --git a/manuskript/ui/highlighters/markdownHighlighter.py b/manuskript/ui/highlighters/markdownHighlighter.py index 4d873b4..3a1564e 100644 --- a/manuskript/ui/highlighters/markdownHighlighter.py +++ b/manuskript/ui/highlighters/markdownHighlighter.py @@ -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) diff --git a/manuskript/ui/highlighters/markdownTokenizer.py b/manuskript/ui/highlighters/markdownTokenizer.py index 9bae886..237b18e 100644 --- a/manuskript/ui/highlighters/markdownTokenizer.py +++ b/manuskript/ui/highlighters/markdownTokenizer.py @@ -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 diff --git a/manuskript/ui/highlighters/searchResultHighlighters/__init__.py b/manuskript/ui/highlighters/searchResultHighlighters/__init__.py new file mode 100644 index 0000000..7af0224 --- /dev/null +++ b/manuskript/ui/highlighters/searchResultHighlighters/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- diff --git a/manuskript/ui/highlighters/searchResultHighlighters/abstractSearchResultHighlighter.py b/manuskript/ui/highlighters/searchResultHighlighters/abstractSearchResultHighlighter.py new file mode 100644 index 0000000..393c1bc --- /dev/null +++ b/manuskript/ui/highlighters/searchResultHighlighters/abstractSearchResultHighlighter.py @@ -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 diff --git a/manuskript/ui/highlighters/searchResultHighlighters/abstractSpecificSearchResultHighlighter.py b/manuskript/ui/highlighters/searchResultHighlighters/abstractSpecificSearchResultHighlighter.py new file mode 100644 index 0000000..3a310e0 --- /dev/null +++ b/manuskript/ui/highlighters/searchResultHighlighters/abstractSpecificSearchResultHighlighter.py @@ -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 diff --git a/manuskript/ui/highlighters/searchResultHighlighters/characterSearchResultHighlighter.py b/manuskript/ui/highlighters/searchResultHighlighters/characterSearchResultHighlighter.py new file mode 100644 index 0000000..16a200d --- /dev/null +++ b/manuskript/ui/highlighters/searchResultHighlighters/characterSearchResultHighlighter.py @@ -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) diff --git a/manuskript/ui/highlighters/searchResultHighlighters/flatDataSearchResultHighlighter.py b/manuskript/ui/highlighters/searchResultHighlighters/flatDataSearchResultHighlighter.py new file mode 100644 index 0000000..4d68fc9 --- /dev/null +++ b/manuskript/ui/highlighters/searchResultHighlighters/flatDataSearchResultHighlighter.py @@ -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) diff --git a/manuskript/ui/highlighters/searchResultHighlighters/outlineSearchResultHighlighter.py b/manuskript/ui/highlighters/searchResultHighlighters/outlineSearchResultHighlighter.py new file mode 100644 index 0000000..801f7cd --- /dev/null +++ b/manuskript/ui/highlighters/searchResultHighlighters/outlineSearchResultHighlighter.py @@ -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 diff --git a/manuskript/ui/highlighters/searchResultHighlighters/plotSearchResultHighlighter.py b/manuskript/ui/highlighters/searchResultHighlighters/plotSearchResultHighlighter.py new file mode 100644 index 0000000..94578a3 --- /dev/null +++ b/manuskript/ui/highlighters/searchResultHighlighters/plotSearchResultHighlighter.py @@ -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) diff --git a/manuskript/ui/highlighters/searchResultHighlighters/plotStepSearchResultHighlighter.py b/manuskript/ui/highlighters/searchResultHighlighters/plotStepSearchResultHighlighter.py new file mode 100644 index 0000000..7b7b146 --- /dev/null +++ b/manuskript/ui/highlighters/searchResultHighlighters/plotStepSearchResultHighlighter.py @@ -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 diff --git a/manuskript/ui/highlighters/searchResultHighlighters/searchResultHighlighter.py b/manuskript/ui/highlighters/searchResultHighlighters/searchResultHighlighter.py new file mode 100644 index 0000000..eeb1aa8 --- /dev/null +++ b/manuskript/ui/highlighters/searchResultHighlighters/searchResultHighlighter.py @@ -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) diff --git a/manuskript/ui/highlighters/searchResultHighlighters/widgetSelectionHighlighter.py b/manuskript/ui/highlighters/searchResultHighlighters/widgetSelectionHighlighter.py new file mode 100644 index 0000000..1533387 --- /dev/null +++ b/manuskript/ui/highlighters/searchResultHighlighters/widgetSelectionHighlighter.py @@ -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") diff --git a/manuskript/ui/highlighters/searchResultHighlighters/worldSearchResultHighlighter.py b/manuskript/ui/highlighters/searchResultHighlighters/worldSearchResultHighlighter.py new file mode 100644 index 0000000..0556b0c --- /dev/null +++ b/manuskript/ui/highlighters/searchResultHighlighters/worldSearchResultHighlighter.py @@ -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) diff --git a/manuskript/ui/mainWindow.py b/manuskript/ui/mainWindow.py index 76f0822..eb8ce7f 100644 --- a/manuskript/ui/mainWindow.py +++ b/manuskript/ui/mainWindow.py @@ -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", "

One paragraph
summary

")) 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 diff --git a/manuskript/ui/mainWindow.ui b/manuskript/ui/mainWindow.ui index 3da6713..d036b5f 100644 --- a/manuskript/ui/mainWindow.ui +++ b/manuskript/ui/mainWindow.ui @@ -124,7 +124,7 @@ QTabWidget::Rounded - 0 + 2 true @@ -815,75 +815,126 @@ 0 0 - 204 - 606 + 429 + 719 QFormLayout::AllNonFixedFieldsGrow - + + + + Name + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + Allow POV + + + false + + + false + + + false + + + + + + Motivation - + - + Goal - + - + Conflict - + - + Epiphany - + - + <html><head/><body><p align="right">One sentence<br/>summary</p></body></html> - + - + <html><head/><body><p align="right">One paragraph<br/>summary</p></body></html> - + - + @@ -914,44 +965,13 @@ - + Importance - - - - - 0 - 0 - - - - - - - - Name - - - - - - - - - - - - - - - - - @@ -1547,7 +1567,7 @@ false - 0 + 35 @@ -2095,7 +2115,7 @@ 0 0 1112 - 30 + 21 @@ -2127,6 +2147,10 @@ &Help + + + + @@ -2183,6 +2207,7 @@ + @@ -2818,6 +2843,43 @@ B&lockquote + + + + .. + + + Search + + + Ctrl+F + + + + + &Technical Support + + + How to obtain technical support for Manuskript. + + + F1 + + + + + &Locate log file... + + + Locate log file + + + Locate the diagnostic log file used for this session. + + + Shift+F1 + + diff --git a/manuskript/ui/search.py b/manuskript/ui/search.py index 06441ae..f177c12 100644 --- a/manuskript/ui/search.py +++ b/manuskript/ui/search.py @@ -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(), + "

#" + str(index.row()) + " - " + extra + "

" + context + "

") + return True + return False + + def mouseLeave(self): + self._tooltipRowIndex = -1 diff --git a/manuskript/ui/searchMenu.py b/manuskript/ui/searchMenu.py new file mode 100644 index 0000000..59468f8 --- /dev/null +++ b/manuskript/ui/searchMenu.py @@ -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) diff --git a/manuskript/ui/search_ui.py b/manuskript/ui/search_ui.py index d9f5c31..5052977 100644 --- a/manuskript/ui/search_ui.py +++ b/manuskript/ui/search_ui.py @@ -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...")) diff --git a/manuskript/ui/search_ui.ui b/manuskript/ui/search_ui.ui index 1b63fdc..89eb0a0 100644 --- a/manuskript/ui/search_ui.ui +++ b/manuskript/ui/search_ui.ui @@ -35,7 +35,7 @@ 0 - + diff --git a/manuskript/ui/settings_ui.py b/manuskript/ui/settings_ui.py index 0cd39f5..b34d982 100644 --- a/manuskript/ui/settings_ui.py +++ b/manuskript/ui/settings_ui.py @@ -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")) diff --git a/manuskript/ui/settings_ui.ui b/manuskript/ui/settings_ui.ui index 6563bcb..6b1a658 100644 --- a/manuskript/ui/settings_ui.ui +++ b/manuskript/ui/settings_ui.ui @@ -6,7 +6,7 @@ 0 0 - 658 + 681 598 @@ -54,7 +54,7 @@ - 2 + 0 @@ -98,93 +98,139 @@ Application settings - - QFormLayout::FieldsStayAtSizeHint - - - - - - 50 - false - - - - Style: - - - - - - - - 50 - false - - - - - - - - - 50 - false - - - - Language: - - - - - - - - 50 - false - - - - - - - - - 50 - false - - - - Font size: - - - - - - - - 50 - false - - - - - - - - - 50 - false - - - - Restarting Manuskript ensures all settings take effect. - - - true - - + + + + + + + 50 + false + + + + Restarting Manuskript ensures all settings take effect. + + + true + + + + + + + QLayout::SetDefaultConstraint + + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + + 50 + false + + + + Style: + + + + + + + + 50 + false + + + + + + + + + 50 + false + + + + Language: + + + + + + + + 50 + false + + + + + + + + + 50 + false + + + + Font size: + + + + + + + + 50 + false + + + + + + + + + + + + + 50 + false + + + + Show progress in chars next + to words + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + @@ -817,7 +863,7 @@ - 3 + 0 @@ -1055,6 +1101,59 @@ + + + + + 75 + true + + + + Char/Word Counter + + + + 9 + + + 9 + + + 9 + + + 9 + + + + + + 50 + false + + + + Count spaces as chars + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + @@ -1095,6 +1194,19 @@ + + + + + 50 + false + + + + Show char c&ount + + + @@ -1165,6 +1277,19 @@ + + + + + 50 + false + + + + Sho&w char count + + + @@ -1224,6 +1349,11 @@ + rdoTreeTextCC + rdoTreeTextWC + rdoTreeTextProgress + rdoTreeTextSummary + rdoTreeTextNothing @@ -2974,7 +3104,7 @@ - 1 + 0 diff --git a/manuskript/ui/views/MDEditCompleter.py b/manuskript/ui/views/MDEditCompleter.py index e0db680..0101238 100644 --- a/manuskript/ui/views/MDEditCompleter.py +++ b/manuskript/ui/views/MDEditCompleter.py @@ -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) diff --git a/manuskript/ui/views/MDEditView.py b/manuskript/ui/views/MDEditView.py index e8eb564..dcd5e3a 100644 --- a/manuskript/ui/views/MDEditView.py +++ b/manuskript/ui/views/MDEditView.py @@ -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. diff --git a/manuskript/ui/views/characterTreeView.py b/manuskript/ui/views/characterTreeView.py index 6997f90..ea5798c 100644 --- a/manuskript/ui/views/characterTreeView.py +++ b/manuskript/ui/views/characterTreeView.py @@ -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 diff --git a/manuskript/ui/views/corkDelegate.py b/manuskript/ui/views/corkDelegate.py index 70ff19e..bd776c1 100644 --- a/manuskript/ui/views/corkDelegate.py +++ b/manuskript/ui/views/corkDelegate.py @@ -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) diff --git a/manuskript/ui/views/corkView.py b/manuskript/ui/views/corkView.py index e77e49d..864a02a 100644 --- a/manuskript/ui/views/corkView.py +++ b/manuskript/ui/views/corkView.py @@ -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 = "" diff --git a/manuskript/ui/views/dndView.py b/manuskript/ui/views/dndView.py index c028964..b2abf08 100644 --- a/manuskript/ui/views/dndView.py +++ b/manuskript/ui/views/dndView.py @@ -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: diff --git a/manuskript/ui/views/lineEditView.py b/manuskript/ui/views/lineEditView.py index 0366016..b61042f 100644 --- a/manuskript/ui/views/lineEditView.py +++ b/manuskript/ui/views/lineEditView.py @@ -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() diff --git a/manuskript/ui/views/outlineDelegates.py b/manuskript/ui/views/outlineDelegates.py index 48bba8e..4fdc0f2 100644 --- a/manuskript/ui/views/outlineDelegates.py +++ b/manuskript/ui/views/outlineDelegates.py @@ -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: diff --git a/manuskript/ui/views/propertiesView.py b/manuskript/ui/views/propertiesView.py index 861dc74..0c5f95b 100644 --- a/manuskript/ui/views/propertiesView.py +++ b/manuskript/ui/views/propertiesView.py @@ -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) diff --git a/manuskript/ui/views/textEditView.py b/manuskript/ui/views/textEditView.py index 41324eb..b4f07e9 100644 --- a/manuskript/ui/views/textEditView.py +++ b/manuskript/ui/views/textEditView.py @@ -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 " 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) diff --git a/manuskript/ui/views/treeDelegates.py b/manuskript/ui/views/treeDelegates.py index fa59702..040b4f5 100644 --- a/manuskript/ui/views/treeDelegates.py +++ b/manuskript/ui/views/treeDelegates.py @@ -34,8 +34,8 @@ class treeTitleDelegate(QStyledItemDelegate): opt = QStyleOptionViewItem(option) self.initStyleOption(opt, index) - iconRect = style.subElementRect(style.SE_ItemViewItemDecoration, opt) - textRect = style.subElementRect(style.SE_ItemViewItemText, opt) + iconRect = style.subElementRect(style.SE_ItemViewItemDecoration, opt, None) + textRect = style.subElementRect(style.SE_ItemViewItemText, opt, None) # Background style.drawPrimitive(style.PE_PanelItemViewItem, opt, painter) @@ -111,6 +111,9 @@ class treeTitleDelegate(QStyledItemDelegate): elif settings.viewSettings["Tree"]["InfoFolder"] == "WC": extraText = item.wordCount() extraText = " ({})".format(extraText) + elif settings.viewSettings["Tree"]["InfoFolder"] == "CC": + extraText = item.charCount() + extraText = " ({})".format(extraText) elif settings.viewSettings["Tree"]["InfoFolder"] == "Progress": extraText = int(toFloat(item.data(Outline.goalPercentage)) * 100) if extraText: @@ -124,6 +127,9 @@ class treeTitleDelegate(QStyledItemDelegate): if settings.viewSettings["Tree"]["InfoText"] == "WC": extraText = item.wordCount() extraText = " ({})".format(extraText) + elif settings.viewSettings["Tree"]["InfoText"] == "CC": + extraText = item.charCount() + extraText = " ({})".format(extraText) elif settings.viewSettings["Tree"]["InfoText"] == "Progress": extraText = int(toFloat(item.data(Outline.goalPercentage)) * 100) if extraText: diff --git a/manuskript/ui/views/treeView.py b/manuskript/ui/views/treeView.py index 3fa7271..2655808 100644 --- a/manuskript/ui/views/treeView.py +++ b/manuskript/ui/views/treeView.py @@ -69,7 +69,7 @@ class treeView(QTreeView, dndView, outlineBasics): return menu def expandCurrentIndex(self, index=None): - if index is None or type(index) == bool: + if index == None or type(index) == bool: index = self._indexesToOpen[0] # self.currentIndex() self.expand(index) @@ -78,7 +78,7 @@ class treeView(QTreeView, dndView, outlineBasics): self.expandCurrentIndex(index=idx) def collapseCurrentIndex(self, index=None): - if index is None or type(index) == bool: + if index == None or type(index) == bool: index = self._indexesToOpen[0] # self.currentIndex() self.collapse(index) diff --git a/manuskript/ui/views/webView.py b/manuskript/ui/views/webView.py index 4d6462c..23e09d1 100644 --- a/manuskript/ui/views/webView.py +++ b/manuskript/ui/views/webView.py @@ -22,16 +22,13 @@ else: if features['qtwebkit']: from PyQt5.QtWebKitWidgets import QWebView - print("Debug: Web rendering engine used: QWebView") webEngine = "QtWebKit" webView = QWebView elif features['qtwebengine']: from PyQt5 import QtWebEngineWidgets - print("Debug: Web rendering engine used: QWebEngineView") webEngine = "QtWebEngine" webView = QtWebEngineWidgets.QWebEngineView else: from PyQt5.QtWidgets import QTextEdit - print("Debug: Web rendering engine used: QTextEdit") webEngine = "QTextEdit" webView = QTextEdit diff --git a/manuskript/ui/welcome.py b/manuskript/ui/welcome.py index 8055c3a..fce923b 100644 --- a/manuskript/ui/welcome.py +++ b/manuskript/ui/welcome.py @@ -2,7 +2,7 @@ # --!-- coding: utf8 --!-- import locale -import imp +import importlib import os from PyQt5.QtCore import QSettings, QRegExp, Qt, QDir @@ -21,6 +21,9 @@ from manuskript.models.worldModel import worldModel from manuskript.ui.welcome_ui import Ui_welcome from manuskript.ui import style as S +import logging +LOGGER = logging.getLogger(__name__) + try: locale.setlocale(locale.LC_ALL, '') except: @@ -57,8 +60,7 @@ class welcome(QWidget, Ui_welcome): sttgs = QSettings() lastDirectory = sttgs.value("lastAccessedDirectory", defaultValue=".", type=str) if lastDirectory != '.': - print(qApp.translate("lastAccessedDirectoryInfo", "Last accessed directory \"{}\" loaded.").format( - lastDirectory)) + LOGGER.info("Last accessed directory \"{}\" loaded.".format(lastDirectory)) return lastDirectory def setLastAccessedDirectory(self, dir): @@ -371,7 +373,7 @@ class welcome(QWidget, Ui_welcome): Qt.FindChildrenRecursively): # Update self.template to reflect the changed name values templateIndex = t.property("templateIndex") - if templateIndex is not None : + if templateIndex != None : self.template[1][templateIndex] = ( self.template[1][templateIndex][0], t.text()) @@ -422,10 +424,12 @@ class welcome(QWidget, Ui_welcome): self.tree.expandAll() def loadDefaultDatas(self): + """Initialize a basic Manuskript project.""" # Empty settings - imp.reload(settings) + importlib.reload(settings) settings.initDefaultValues() + self.mw.loadEmptyDatas() if self.template: t = [i for i in self._templates if i[0] == self.template[0]] @@ -433,20 +437,10 @@ class welcome(QWidget, Ui_welcome): settings.viewMode = "simple" # Tasks - self.mw.mdlFlatData = QStandardItemModel(2, 8, self.mw) - - # Persos - # self.mw.mdlPersos = QStandardItemModel(0, 0, self.mw) - self.mw.mdlCharacter = characterModel(self.mw) - # self.mdlPersosProxy = None # persosProxyModel() # None - # self.mw.mdlPersosProxy = persosProxyModel(self.mw) - - # self.mw.mdlPersosInfos = QStandardItemModel(1, 0, self.mw) - # self.mw.mdlPersosInfos.insertColumn(0, [QStandardItem("ID")]) - # self.mw.mdlPersosInfos.setHorizontalHeaderLabels(["Description"]) + self.mw.mdlFlatData.setRowCount(2) # data from: infos.txt, summary.txt + self.mw.mdlFlatData.setColumnCount(8) # version_1.py: len(infos.txt) == 8 # Labels - self.mw.mdlLabels = QStandardItemModel(self.mw) for color, text in [ (Qt.transparent, ""), (Qt.yellow, self.tr("Idea")), @@ -458,7 +452,6 @@ class welcome(QWidget, Ui_welcome): self.mw.mdlLabels.appendRow(QStandardItem(iconFromColor(color), text)) # Status - self.mw.mdlStatus = QStandardItemModel(self.mw) for text in [ "", self.tr("TODO"), @@ -468,14 +461,9 @@ class welcome(QWidget, Ui_welcome): ]: self.mw.mdlStatus.appendRow(QStandardItem(text)) - # Plot - self.mw.mdlPlots = plotModel(self.mw) + # Plot (nothing special needed) # Outline - self.mw.mdlOutline = outlineModel(self.mw) - - # World - self.mw.mdlWorld = worldModel(self.mw) root = self.mw.mdlOutline.rootItem _type = "md" @@ -509,3 +497,5 @@ class welcome(QWidget, Ui_welcome): if self.template and self.template[1]: addElement(root, self.template[1]) + + # World (nothing special needed) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 703d87e..76df07f 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -21,6 +21,9 @@ grade: stable confinement: strict base: core18 icon: icons/Manuskript/manuskript.svg +layout: + /usr/share/pandoc/data/templates: + bind: $SNAP/usr/share/pandoc/data/templates apps: manuskript: