diff --git a/manuskript/exporter/basic.py b/manuskript/exporter/basic.py index 5acbe9e0..bc1c1fc0 100644 --- a/manuskript/exporter/basic.py +++ b/manuskript/exporter/basic.py @@ -129,7 +129,7 @@ class basicFormat: @classmethod def isValid(cls): return True - + @classmethod def projectPath(cls): return os.path.dirname(os.path.abspath(mainWindow().currentProject)) diff --git a/manuskript/importer/__init__.py b/manuskript/importer/__init__.py new file mode 100644 index 00000000..d5a0aef7 --- /dev/null +++ b/manuskript/importer/__init__.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +from manuskript.importer.folderImporter import folderImporter +from manuskript.importer.markdownImporter import markdownImporter +from manuskript.importer.opmlImporter import opmlImporter +from manuskript.importer.pandocImporters import markdownPandocImporter, \ + odtPandocImporter, ePubPandocImporter, docXPandocImporter, HTMLPandocImporter, \ + rstPandocImporter, LaTeXPandocImporter, OPMLPandocImporter + +importers = [ + # Internal + markdownImporter, + folderImporter, + opmlImporter, + + # Pandoc + markdownPandocImporter, + odtPandocImporter, + ePubPandocImporter, + docXPandocImporter, + HTMLPandocImporter, + rstPandocImporter, + LaTeXPandocImporter, + OPMLPandocImporter, + ] diff --git a/manuskript/importer/abstractImporter.py b/manuskript/importer/abstractImporter.py new file mode 100644 index 00000000..440068b7 --- /dev/null +++ b/manuskript/importer/abstractImporter.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- +import os +import shutil +import subprocess + +from PyQt5.QtCore import QSettings +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QCheckBox, QHBoxLayout, \ + QLabel, QSpinBox, QComboBox, QLineEdit +from manuskript.ui.collapsibleGroupBox2 import collapsibleGroupBox2 +from manuskript.ui import style + + +class abstractImporter: + """ + abstractImporter is used to import documents into manuskript. + + The startImport function must be subclassed. It takes a filePath (str to + the document to import), and must return `outlineItem`s. + """ + + name = "" + description = "" + fileFormat = "" # File format accepted. For example: "OPML Files (*.opml)" + # For folder, use "<>" + icon = "" + engine = "Internal" + + def __init__(self): + self.settings = {} + + def startImport(self, filePath, parentItem, settingsWidget): + """ + Takes a str path to the file/folder to import, and the settingsWidget + returnend by `self.settingsWidget()` containing the user set settings, + and return `outlineItem`s. + """ + pass + + @classmethod + def isValid(cls): + return False + + + def settingsWidget(self, widget): + """ + Takes a QWidget that can be modified and must be returned. + """ + return widget + + def addPage(self, widget, title): + """ + Convenience function to add a page to the settingsWidget `widget`, at + the end. + + Returns the page widget. + """ + w = QWidget(widget) + w.setLayout(QVBoxLayout()) + widget.toolBox.insertItem(widget.toolBox.count(), w, title) + widget.toolBox.layout().setSpacing(0) + return w + + def addGroup(self, parent, title): + """ + Adds a collapsible group to the given widget. + """ + g = collapsibleGroupBox2(title=title) + parent.layout().addWidget(g) + g.setLayout(QVBoxLayout()) + return g + + def addSetting(self, name, type, label, widget=None, default=None, + tooltip=None, min=None, max=None, vals=None, suffix=""): + + self.settings[name] = self.setting(name, type, label, widget, default, + tooltip, min, max, vals, suffix) + + def widget(self, name): + if name in self.settings: + return self.settings[name].widget() + + def getSetting(self, name): + if name in self.settings: + return self.settings[name] + + + class setting: + """ + A class used to store setting, and display a widget for the user to + modify it. + """ + def __init__(self, name, type, label, widget=None, default=None, + tooltip=None, min=None, max=None, vals=None, suffix=""): + self.name = name + self.type = type + self.label = label + self._widget = widget + self.default = default + self.min = min + self.max = max + self.vals = vals.split("|") if vals else [] + self.suffix = suffix + self.tooltip = tooltip + + def widget(self, parent=None): + """ + Returns the widget used, or creates it if not done yet. If parent + is given, widget is inserted in parent's layout. + """ + if self._widget: + return self._widget + + else: + + if "checkbox" in self.type: + self._widget = QCheckBox(self.label) + if self.default: + self._widget.setChecked(True) + if parent: + parent.layout().addWidget(self._widget) + + elif "number" in self.type: + l = QHBoxLayout() + label = QLabel(self.label, parent) + label.setWordWrap(True) + l.addWidget(label, 8) + self._widget = QSpinBox() + self._widget.setValue(self.default if self.default else 0) + if self.min: + self._widget.setMinimum(self.min) + if self.max: + self._widget.setMaximum(self.max) + if self.suffix: + self._widget.setSuffix(self.suffix) + l.addWidget(self._widget, 2) + if parent: + parent.layout().addLayout(l) + + elif "combo" in self.type: + l = QHBoxLayout() + label = QLabel(self.label, parent) + label.setWordWrap(True) + l.addWidget(label, 6) + self._widget = QComboBox() + self._widget.addItems(self.vals) + if self.default: + self._widget.setCurrentText(self.default) + l.addWidget(self._widget, 2) + if parent: + parent.layout().addLayout(l) + + elif "text" in self.type: + l = QHBoxLayout() + label = QLabel(self.label, parent) + label.setWordWrap(True) + l.addWidget(label, 5) + self._widget = QLineEdit() + self._widget.setStyleSheet(style.lineEditSS()) + if self.default: + self._widget.setText(self.default) + l.addWidget(self._widget, 3) + if parent: + parent.layout().addLayout(l) + + elif "label" in self.type: + self._widget = QLabel(self.label, parent) + self._widget.setWordWrap(True) + if parent: + parent.layout().addWidget(self._widget) + + if self.tooltip: + self._widget.setToolTip(self.tooltip) + + return self._widget + + def value(self): + """ + Return the value contained in the widget. + """ + if not self._widget: + return self.default + + else: + + if "checkbox" in self.type: + return self._widget.isChecked() + + elif "number" in self.type: + return self._widget.value() + + elif "combo" in self.type: + return self._widget.currentText() + + elif "text" in self.type: + return self._widget.text() + + + diff --git a/manuskript/importer/folderImporter.py b/manuskript/importer/folderImporter.py new file mode 100644 index 00000000..4a3929cc --- /dev/null +++ b/manuskript/importer/folderImporter.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os +from manuskript.importer.abstractImporter import abstractImporter +from manuskript.models.outlineModel import outlineItem +from manuskript.enums import Outline +from PyQt5.QtWidgets import qApp + + +class folderImporter(abstractImporter): + + name = "Folder" + description = "" + fileFormat = "<>" + icon = "folder" + + @classmethod + def isValid(cls): + return True + + def startImport(self, filePath, parentItem, settingsWidget, fromString=None): + """ + Imports from a folder. + """ + ext = self.getSetting("ext").value() + ext = [e.strip().replace("*", "") for e in ext.split(",")] + + sorting = self.getSetting("sortItems").value() + + items = [] + stack = {} + + for dirpath, dirnames, filenames in os.walk(filePath): + + if dirpath in stack: + item = stack[dirpath] + else: + # It's the parent folder, and we are not including it + # so every item is attached to parentItem + item = parentItem + + def addFile(f): + fName, fExt = os.path.splitext(f) + if fExt in ext: + child = outlineItem(title=fName, _type="md", parent=item) + with open(os.path.join(dirpath, f), "r") as fr: + child._data[Outline.text] = fr.read() + items.append(child) + + def addFolder(d): + child = outlineItem(title=d, parent=item) + items.append(child) + stack[os.path.join(dirpath, d)] = child + + if not self.getSetting("separateFolderFiles").value(): + # Import folder and files together (only makes differences if + # they are sorted, really) + allFiles = dirnames + filenames + if sorting: + allFiles = sorted(allFiles) + + for f in allFiles: + if f in dirnames: + addFolder(f) + else: + addFile(f) + + else: + # Import first folders, then files + if sorting: + dirnames = sorted(dirnames) + filenames = sorted(filenames) + + # Import folders + for d in dirnames: + addFolder(d) + + # Import files + for f in filenames: + addFile(f) + + return items + + def settingsWidget(self, widget): + """ + Takes a QWidget that can be modified and must be returned. + """ + + # Add group + group = self.addGroup(widget.toolBox.widget(0), + qApp.translate("Import", "Folder import")) + #group = cls.addPage(widget, "Folder import") + + self.addSetting("info", "label", + qApp.translate("Import", """Info: Imports a whole + directory structure. Folders are added as folders, and + plaintext documents within (you chose which ones by extension) + are added as scene.
 """)) + + self.addSetting("ext", "text", + qApp.translate("Import", "Include only those extensions:"), + default="*.txt, *.md", + tooltip=qApp.translate("Import", "Coma separated values")), + + self.addSetting("sortItems", "checkbox", + qApp.translate("Import", "Sort items by name"), + default=True), + + self.addSetting("separateFolderFiles", "checkbox", + qApp.translate("Import", "Import folder then files"), + default=True), + + for s in self.settings: + self.settings[s].widget(group) + + return widget + + + + diff --git a/manuskript/importer/markdownImporter.py b/manuskript/importer/markdownImporter.py new file mode 100644 index 00000000..a57b6be2 --- /dev/null +++ b/manuskript/importer/markdownImporter.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +from manuskript.importer.abstractImporter import abstractImporter +from manuskript.models.outlineModel import outlineItem +from manuskript.enums import Outline +from PyQt5.QtWidgets import qApp +import re, os + + +class markdownImporter(abstractImporter): + + name = "Markdown" + description = "" + fileFormat = "Markdown files (*.md *.txt *)" + icon = "text-x-markdown" + + @classmethod + def isValid(cls): + return True + + def startImport(self, filePath, parentItem, settingsWidget, fromString=None): + """ + Very simple import from markdown. We just look at ATX headers (we + ignore setext for the sake of simplicity, for now.) + + **A difficulty:** in the following example, we can do things with + markdown headers (like go from level 1 to level 4 and back to level 2) + that we cannot do in an outline. + + ``` + # Level 1 + # Level 1 + ## Level 2 + ### Level 3 + #### Level 4 + ##### Level 5 + ### Level 3 + # Level 1 + #### Level 4? → Level 2 + ### Level 3? → Level 2 + ## Level 2 → Level 2 + #### Level 4? → Level 3 + ``` + + I think the current version of the imported manages that quite well. + + **A question:** In the following sample, the first Level 1 becomes a + text element, because it has no other sub elements. But the content of + second Level 1 becomes a text element, with no name. What name should + we give it? + + ``` + # Level 1 + Some texte content. + Level 1 will become a text element. + # Level 1 + This content has no name. + ## Level 2 + ... + ``` + """ + + if not fromString: + # Read file + with open(filePath, "r") as f: + txt = f.read() + else: + txt = fromString + + items = [] + + parent = parentItem + lastLevel = 0 + content = "" + + def saveContent(content, parent): + if content.strip(): + child = outlineItem(title=parent.title(), parent=parent, _type="md") + child._data[Outline.text] = content + items.append(child) + return "" + + def addTitle(name, parent, level): + child = outlineItem(title=name, parent=parent) + child.__miLevel = level + items.append(child) + return child + + ATXHeader = re.compile(r"(\#+)\s*(.+?)\s*\#*$") + setextHeader1 = re.compile(r"([^\#-=].+)\n(===+)$", re.MULTILINE) + setextHeader2 = re.compile(r"([^\#-=].+)\n(---+)$", re.MULTILINE) + + # We store the level of each item in a temporary var + parent.__miLevel = 0 # markdown importer header level + + txt = txt.split("\n") + skipNextLine = False + for i in range(len(txt)): + + l = txt[i] + l2 = "\n".join(txt[i:i+2]) + + header = False + + if skipNextLine: + # Last line was a setext-style header. + skipNextLine = False + continue + + # Check ATX Header + m = ATXHeader.match(l) + if m: + header = True + level = len(m.group(1)) + name = m.group(2) + + # Check setext header + m = setextHeader1.match(l2) + + if not header and m and len(m.group(1)) == len(m.group(2)): + header = True + level = 1 + name = m.group(1) + skipNextLine = True + + m = setextHeader2.match(l2) + if not header and m and len(m.group(1)) == len(m.group(2)): + header = True + level = 2 + name = m.group(1) + skipNextLine = True + + if header: + + # save content + content = saveContent(content, parent) + + # get parent level + while parent.__miLevel >= level: + parent = parent.parent() + + # create title + child = addTitle(name, parent, level) + child.__miLevel = level + + # title becomes the new parent + parent = child + + lastLevel = level + + else: + content += l + "\n" + + saveContent(content, parent) + + # Clean up + for i in items: + if i.childCount() == 1 and i.children()[0].isText(): + # We have a folder with only one text item + # So we make it a text item + i._data[Outline.type] = "md" + i._data[Outline.text] = i.children()[0].text() + c = i.removeChild(0) + items.remove(c) + + return items + + def settingsWidget(self, widget): + """ + Takes a QWidget that can be modified and must be returned. + """ + + # Add group + group = self.addGroup(widget.toolBox.widget(0), + qApp.translate("Import", "Markdown import")) + #group = cls.addPage(widget, "Folder import") + + self.addSetting("info", "label", + qApp.translate("Import", """Info: A very simple + parser that will go through a markdown document and + create items for each titles.
 """)) + + for s in self.settings: + self.settings[s].widget(group) + + return widget diff --git a/manuskript/importer/opmlImporter.py b/manuskript/importer/opmlImporter.py new file mode 100644 index 00000000..b7b8b994 --- /dev/null +++ b/manuskript/importer/opmlImporter.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +from PyQt5.QtWidgets import qApp, QMessageBox +from manuskript.models.outlineModel import outlineItem +from manuskript.enums import Outline +from lxml import etree as ET +from manuskript.functions import mainWindow +from manuskript.importer.abstractImporter import abstractImporter + +class opmlImporter(abstractImporter): + + name = "OPML" + description = "" + fileFormat = "OPML Files (*.opml *.xml)" + icon = "text-x-opml+xml" + + @classmethod + def isValid(cls): + return True + + @classmethod + def startImport(cls, filePath, parentItem, settingsWidget, fromString=None): + """ + Import/export outline cards in OPML format. + """ + ret = False + + if filePath != "": + # We have a filePath, so we read the file + try: + with open(filePath, 'rb') as opmlFile: + #opmlContent = cls.saveNewlines(opmlFile.read()) + opmlContent = opmlFile.read() + except: + QMessageBox.critical(settingsWidget, + qApp.translate("Import", "OPML Import"), + qApp.translate("Import", "File open failed.")) + return None + + elif fromString == "": + # We have neither filePath nor fromString, so we leave + return None + + else: + # We load from string + opmlContent = bytes(fromString, "utf-8") + + parsed = ET.fromstring(opmlContent) + + opmlNode = parsed + bodyNode = opmlNode.find("body") + items = [] + + if bodyNode is not None: + outlineEls = bodyNode.findall("outline") + + if outlineEls is not None: + for element in outlineEls: + items.extend(cls.parseItems(element, parentItem)) + ret = True + + if not ret: + QMessageBox.critical( + settingsWidget, + qApp.translate("Import", "OPML Import"), + qApp.translate("Import", "This does not appear to be a valid OPML file.")) + + return None + + return items + + @classmethod + def parseItems(cls, underElement, parentItem=None): + items = [] + title = underElement.get('text') + if title is not 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): + #body = cls.restoreNewLines(note) + body = note + + children = underElement.findall('outline') + if children is not None and len(children) > 0: + for el in children: + items.extend(cls.parseItems(el, card)) + else: + card.setData(Outline.type.value, 'md') + card.setData(Outline.text.value, body) + + return items + + @classmethod + def saveNewlines(cls, inString): + """ + Since XML parsers are notorious for stripping out significant newlines, + save them in a form we can restore after the parse. + """ + inString = inString.replace("\r\n", "\n") + inString = inString.replace("\n", "{{lf}}") + + return inString + + @classmethod + def restoreNewLines(cls, inString): + """ + Restore any significant newlines + """ + return inString.replace("{{lf}}", "\n") + + @classmethod + def isWhitespaceOnly(cls, inString): + """ + Determine whether or not a string only contains whitespace. + """ + s = cls.restoreNewLines(inString) + s = ''.join(s.split()) + + return len(s) is 0 diff --git a/manuskript/importer/pandocImporters.py b/manuskript/importer/pandocImporters.py new file mode 100644 index 00000000..023a4b08 --- /dev/null +++ b/manuskript/importer/pandocImporters.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +from manuskript.importer.abstractImporter import abstractImporter +from manuskript.exporter.pandoc import pandocExporter +from manuskript.importer.opmlImporter import opmlImporter +from manuskript.importer.markdownImporter import markdownImporter +from PyQt5.QtWidgets import qApp + + +class pandocImporter(abstractImporter): + + formatFrom = "" + engine = "Pandoc" + extraArgs = [] + + @classmethod + def isValid(cls): + return pandocExporter().isValid() + + def startImport(self, filePath, parentItem, settingsWidget): + + formatTo = self.getSetting("formatTo").value().lower() + wrap = self.getSetting("wrap").value().lower() + + # pandoc --from=markdown filename --to=opml --standalone + args = [ + "--from={}".format(self.formatFrom), + filePath, + "--to={}".format(formatTo), + "--wrap={}".format(wrap), + ] + + if formatTo == "opml": + args.append("--standalone") + + args += self.extraArgs + + r = pandocExporter().run(args) + + if formatTo == "opml": + return self.opmlImporter.startImport("", parentItem, + settingsWidget, fromString=r) + elif formatTo == "markdown": + return self.mdImporter.startImport(filePath, parentItem, + settingsWidget, fromString=r) + + def settingsWidget(self, widget): + """ + Takes a QWidget that can be modified and must be returned. + """ + + # Add group + group = self.addGroup(widget.toolBox.widget(0), + qApp.translate("Import", "Pandoc import")) + + self.addSetting("info", "label", + qApp.translate("Import", """Info: Manuskript can + import from markdown or OPML. Pandoc will + convert your document to either (see option below), and + then it will be imported in manuskript. One or the other + might give better result depending on your document. +
 """)) + + self.addSetting("formatTo", "combo", + qApp.translate("Import", "Import using:"), + vals="markdown|OPML") + + self.addSetting("wrap", "combo", + qApp.translate("Import", "Wrap lines:"), + vals="auto|none|preserve", + default="none", + tooltip=qApp.translate("Import", """

Should pandoc create + cosmetic / non-semantic line-breaks?

+ auto: wraps at 72 characters.
+ none: no line wrap.
+ preserve: tries to preserves line wrap from the + original document.

""")) + + for s in self.settings: + self.settings[s].widget(group) + + self.mdImporter = markdownImporter() + widget = self.mdImporter.settingsWidget(widget) + self.opmlImporter = opmlImporter() + widget = self.opmlImporter.settingsWidget(widget) + + return widget + + +class markdownPandocImporter(pandocImporter): + + name = "Markdown" + description = "Markdown, using pandoc" + fileFormat = "Markdown files (*.md *.txt *)" + icon = "text-x-markdown" + formatFrom = "markdown" + +class ePubPandocImporter(pandocImporter): + + name = "ePub" + description = "" + fileFormat = "ePub files (*.epub)" + icon = "application-epub+zip" + formatFrom = "epub" + +class docXPandocImporter(pandocImporter): + + name = "DocX" + description = "" + fileFormat = "DocX files (*.docx)" + icon = "application-vnd.openxmlformats-officedocument.wordprocessingml.document" + formatFrom = "docx" + +class odtPandocImporter(pandocImporter): + + name = "ODT" + description = "" + fileFormat = "Open Document files (*.odt)" + icon = "application-vnd.oasis.opendocument.text" + formatFrom = "odt" + +class rstPandocImporter(pandocImporter): + + name = "reStructuredText" + description = "" + fileFormat = "reStructuredText files (*.rst)" + icon = "text-plain" + formatFrom = "rst" + +class HTMLPandocImporter(pandocImporter): + + name = "HTML" + description = "" + fileFormat = "HTML files (*.htm *.html)" + icon = "text-html" + formatFrom = "html" + +class LaTeXPandocImporter(pandocImporter): + + name = "LaTeX" + description = "" + fileFormat = "LaTeX files (*.tex)" + icon = "text-x-tex" + formatFrom = "latex" + +class OPMLPandocImporter(pandocImporter): + + name = "OPML" + description = "" + fileFormat = "OPML files (*.opml *.xml)" + icon = "text-x-opml+xml" + formatFrom = "opml" + + + diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index b47892e5..4429d01c 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -21,6 +21,7 @@ from manuskript.settingsWindow import settingsWindow from manuskript.ui import style from manuskript.ui.about import aboutDialog from manuskript.ui.collapsibleDockWidgets import collapsibleDockWidgets +from manuskript.ui.importers.importer import importerDialog from manuskript.ui.exporters.exporter import exporterDialog from manuskript.ui.helpLabel import helpLabel from manuskript.ui.mainWindow import Ui_MainWindow @@ -97,12 +98,13 @@ class MainWindow(QMainWindow, Ui_MainWindow): # Main Menu for i in [self.actSave, self.actSaveAs, self.actCloseProject, self.menuEdit, self.menuView, self.menuTools, self.menuHelp, - self.actCompile, self.actSettings]: + self.actImport, self.actCompile, self.actSettings]: i.setEnabled(False) self.actOpen.triggered.connect(self.welcome.openFile) self.actSave.triggered.connect(self.saveDatas) self.actSaveAs.triggered.connect(self.welcome.saveAsFile) + self.actImport.triggered.connect(self.doImport) self.actCompile.triggered.connect(self.doCompile) self.actLabels.triggered.connect(self.settingsLabel) self.actStatus.triggered.connect(self.settingsStatus) @@ -381,6 +383,9 @@ class MainWindow(QMainWindow, Ui_MainWindow): def openIndex(self, index): self.treeRedacOutline.setCurrentIndex(index) + def openIndexes(self, indexes, newTab=True): + self.mainEditor.openIndexes(indexes, newTab=True) + ############################################################################### # LOAD AND SAVE ############################################################################### @@ -408,7 +413,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.makeConnections() # Load settings - if settings.openIndexes: + if settings.openIndexes and settings.openIndexes != [""]: self.mainEditor.tabSplitter.restoreOpenIndexes(settings.openIndexes) self.generateViewMenu() self.mainEditor.sldCorkSizeFactor.setValue(settings.corkSizeFactor) @@ -460,7 +465,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): i.setEnabled(False) for i in [self.actSave, self.actSaveAs, self.actCloseProject, self.menuEdit, self.menuView, self.menuTools, self.menuHelp, - self.actCompile, self.actSettings]: + self.actImport, self.actCompile, self.actSettings]: i.setEnabled(True) # Add project name to Window's name @@ -504,7 +509,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): i.setEnabled(True) for i in [self.actSave, self.actSaveAs, self.actCloseProject, self.menuEdit, self.menuView, self.menuTools, self.menuHelp, - self.actCompile, self.actSettings]: + self.actImport, self.actCompile, self.actSettings]: i.setEnabled(False) # Set Window's name - no project loaded @@ -1306,9 +1311,17 @@ class MainWindow(QMainWindow, Ui_MainWindow): # POV in settings / views ############################################################################### - # COMPILE + # IMPORT / EXPORT ############################################################################### + def doImport(self): + self.dialog = importerDialog(mw=self) + self.dialog.show() + + r = self.dialog.geometry() + r2 = self.geometry() + self.dialog.move(r2.center() - r.center()) + def doCompile(self): self.dialog = exporterDialog(mw=self) self.dialog.show() diff --git a/manuskript/models/outlineModel.py b/manuskript/models/outlineModel.py index 52617786..e3f61e57 100644 --- a/manuskript/models/outlineModel.py +++ b/manuskript/models/outlineModel.py @@ -821,10 +821,56 @@ class outlineItem(): return qApp.translate("outlineModel", "{} words").format( locale.format("%d", wc, grouping=True)) + def copy(self): + """ + Returns a copy of item, with no parent, and no ID. + """ + item = outlineItem(xml=self.toXML()) + item.setData(Outline.ID.value, None) + return item - ############################################################################### - # XML - ############################################################################### + def split(self, splitMark, recursive=True): + """ + Split scene at splitMark. If multiple splitMark, multiple splits. + + If called on a folder and recursive is True, then it is recursively + applied to every children. + """ + if self.isFolder() and recursive: + for c in self.children(): + c.split(splitMark) + + else: + txt = self.text().split(splitMark) + + if len(txt) == 1: + # Mark not found + return False + + else: + + # Stores the new text + self.setData(Outline.text.value, txt[0]) + + k = 1 + for subTxt in txt[1:]: + # Create a copy + item = self.copy() + + # Change title adding _k + item.setData(Outline.title.value, + "{}_{}".format(item.title(), k+1)) + + # Set text + item.setData(Outline.text.value, subTxt) + + # Inserting item + self.parent().insertChild(self.row()+k, item) + k += 1 + + ############################################################################### + # XML + ############################################################################### def toXML(self): item = ET.Element("outlineItem") @@ -884,10 +930,9 @@ class outlineItem(): elif child.tag == "revision": self.appendRevision(child.attrib["timestamp"], child.attrib["text"]) - - ############################################################################### - # IDS - ############################################################################### + ############################################################################### + # IDS + ############################################################################### def getUniqueID(self): self.setData(Outline.ID.value, self._model.rootItem.findUniqueID()) @@ -900,7 +945,7 @@ class outlineItem(): self.IDs = self.listAllIDs() if max([self.IDs.count(i) for i in self.IDs if i]) != 1: - print("There are some doublons:", [i for i in self.IDs if i and self.IDs.count(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]) def checkChildren(item): for c in item.children(): @@ -918,8 +963,9 @@ class outlineItem(): return IDs def findUniqueID(self): - k = 0 - while str(k) in self.IDs: + IDs = [int(i) for i in self.IDs] + k = 1 + while k in IDs: k += 1 self.IDs.append(str(k)) return str(k) diff --git a/manuskript/ui/editors/editorWidget.py b/manuskript/ui/editors/editorWidget.py index 75abb568..60ee4008 100644 --- a/manuskript/ui/editors/editorWidget.py +++ b/manuskript/ui/editors/editorWidget.py @@ -55,6 +55,8 @@ class editorWidget(QWidget, Ui_editorWidget_ui): self.mw = mainWindow() self._tabWidget = None # set by mainEditor on creation + self._model = None + # def setModel(self, model): # self._model = model # self.setView() @@ -83,8 +85,10 @@ class editorWidget(QWidget, Ui_editorWidget_ui): if r.isValid(): count = r.internalPointer().childCount() + elif self._model: + count = self._model.rootItem.childCount() else: - count = self.mw.mdlOutline.rootItem.childCount() + count = 0 for c in range(count): self.corkView.itemDelegate().sizeHintChanged.emit(r.child(c, 0)) @@ -102,8 +106,10 @@ class editorWidget(QWidget, Ui_editorWidget_ui): if self.currentIndex.isValid(): item = self.currentIndex.internalPointer() + elif self._model: + item = self._model.rootItem else: - item = self.mw.mdlOutline.rootItem + return i = self._tabWidget.indexOf(self) self._tabWidget.setTabText(i, item.title()) @@ -202,7 +208,7 @@ class editorWidget(QWidget, Ui_editorWidget_ui): self.txtEdits = [] - if item != self.mw.mdlOutline.rootItem: + if item != self._model.rootItem: addTitle(item) addChildren(item) @@ -211,7 +217,7 @@ class editorWidget(QWidget, Ui_editorWidget_ui): elif item and item.isFolder() and self.folderView == "cork": self.stack.setCurrentIndex(2) - self.corkView.setModel(self.mw.mdlOutline) + self.corkView.setModel(self._model) self.corkView.setRootIndex(self.currentIndex) try: self.corkView.selectionModel().selectionChanged.connect(mainWindow().redacMetadata.selectionChanged, AUC) @@ -225,7 +231,7 @@ class editorWidget(QWidget, Ui_editorWidget_ui): self.outlineView.setModelCharacters(mainWindow().mdlCharacter) self.outlineView.setModelLabels(mainWindow().mdlLabels) self.outlineView.setModelStatus(mainWindow().mdlStatus) - self.outlineView.setModel(self.mw.mdlOutline) + self.outlineView.setModel(self._model) self.outlineView.setRootIndex(self.currentIndex) try: @@ -242,9 +248,9 @@ class editorWidget(QWidget, Ui_editorWidget_ui): self.txtRedacText.setCurrentModelIndex(QModelIndex()) try: - self.mw.mdlOutline.dataChanged.connect(self.modelDataChanged, AUC) - self.mw.mdlOutline.rowsInserted.connect(self.updateIndexFromID, AUC) - self.mw.mdlOutline.rowsRemoved.connect(self.updateIndexFromID, AUC) + self._model.dataChanged.connect(self.modelDataChanged, AUC) + self._model.rowsInserted.connect(self.updateIndexFromID, AUC) + self._model.rowsRemoved.connect(self.updateIndexFromID, AUC) #self.mw.mdlOutline.rowsAboutToBeRemoved.connect(self.rowsAboutToBeRemoved, AUC) except TypeError: pass @@ -254,8 +260,8 @@ class editorWidget(QWidget, Ui_editorWidget_ui): def setCurrentModelIndex(self, index=None): if index.isValid(): self.currentIndex = index - self.currentID = self.mw.mdlOutline.ID(index) - # self._model = index.model() + self._model = index.model() + self.currentID = self._model.ID(index) else: self.currentIndex = QModelIndex() self.currentID = None @@ -267,7 +273,7 @@ class editorWidget(QWidget, Ui_editorWidget_ui): Index might have changed (through drag an drop), so we keep current item's ID and update index. Item might have been deleted too. """ - idx = self.mw.mdlOutline.getIndexByID(self.currentID) + idx = self._model.getIndexByID(self.currentID) # If we have an ID but the ID does not exist, it has been deleted if self.currentID and idx == QModelIndex(): diff --git a/manuskript/ui/editors/mainEditor_ui.ui b/manuskript/ui/editors/mainEditor_ui.ui index 68a3a68f..1110a34f 100644 --- a/manuskript/ui/editors/mainEditor_ui.ui +++ b/manuskript/ui/editors/mainEditor_ui.ui @@ -50,7 +50,9 @@ - + + + Alt+Up diff --git a/manuskript/ui/exporters/manuskript/plainTextSettings_ui.py b/manuskript/ui/exporters/manuskript/plainTextSettings_ui.py index 77598ba5..a6c8f98c 100644 --- a/manuskript/ui/exporters/manuskript/plainTextSettings_ui.py +++ b/manuskript/ui/exporters/manuskript/plainTextSettings_ui.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'manuskript/ui/exporters/manuskript/plainTextSettings_ui.ui' # -# Created by: PyQt5 UI code generator 5.4.2 +# Created by: PyQt5 UI code generator 5.9 # # WARNING! All changes made in this file will be lost! @@ -28,9 +28,10 @@ class Ui_exporterSettings(object): "}") self.toolBox.setObjectName("toolBox") self.content = QtWidgets.QWidget() - self.content.setGeometry(QtCore.QRect(0, 0, 491, 842)) + self.content.setGeometry(QtCore.QRect(0, 0, 497, 834)) self.content.setObjectName("content") self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.content) + self.verticalLayout_5.setContentsMargins(0, 0, 0, 0) self.verticalLayout_5.setObjectName("verticalLayout_5") self.label = QtWidgets.QLabel(self.content) self.label.setObjectName("label") @@ -111,9 +112,10 @@ class Ui_exporterSettings(object): self.verticalLayout_5.addItem(spacerItem1) self.toolBox.addItem(self.content, "") self.separations = QtWidgets.QWidget() - self.separations.setGeometry(QtCore.QRect(0, 0, 511, 522)) + self.separations.setGeometry(QtCore.QRect(0, 0, 511, 534)) self.separations.setObjectName("separations") self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.separations) + self.verticalLayout_8.setContentsMargins(0, 0, 0, 0) self.verticalLayout_8.setObjectName("verticalLayout_8") self.label_3 = QtWidgets.QLabel(self.separations) font = QtGui.QFont() @@ -319,10 +321,11 @@ class Ui_exporterSettings(object): self.verticalLayout_8.addItem(spacerItem6) self.toolBox.addItem(self.separations, "") self.transformations = QtWidgets.QWidget() - self.transformations.setGeometry(QtCore.QRect(0, 0, 511, 522)) + self.transformations.setGeometry(QtCore.QRect(0, 0, 511, 534)) self.transformations.setStyleSheet("QGroupBox{font-weight:bold;}") self.transformations.setObjectName("transformations") self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.transformations) + self.verticalLayout_6.setContentsMargins(0, 0, 0, 0) self.verticalLayout_6.setObjectName("verticalLayout_6") self.grpTransTypo = collapsibleGroupBox2(self.transformations) self.grpTransTypo.setStyleSheet("") @@ -481,10 +484,11 @@ class Ui_exporterSettings(object): self.verticalLayout_6.addItem(spacerItem10) self.toolBox.addItem(self.transformations, "") self.preview = QtWidgets.QWidget() - self.preview.setGeometry(QtCore.QRect(0, 0, 511, 522)) + self.preview.setGeometry(QtCore.QRect(0, 0, 511, 534)) self.preview.setStyleSheet("QGroupBox{font-weight:bold;}") self.preview.setObjectName("preview") self.verticalLayout_11 = QtWidgets.QVBoxLayout(self.preview) + self.verticalLayout_11.setContentsMargins(0, 0, 0, 0) self.verticalLayout_11.setObjectName("verticalLayout_11") self.groupBox = QtWidgets.QGroupBox(self.preview) self.groupBox.setObjectName("groupBox") diff --git a/manuskript/ui/exporters/manuskript/plainTextSettings_ui.ui b/manuskript/ui/exporters/manuskript/plainTextSettings_ui.ui index 80649472..15c1a5e4 100644 --- a/manuskript/ui/exporters/manuskript/plainTextSettings_ui.ui +++ b/manuskript/ui/exporters/manuskript/plainTextSettings_ui.ui @@ -53,8 +53,8 @@ QToolBox::tab:selected, QToolBox::tab:hover{ 0 0 - 491 - 842 + 497 + 834 @@ -233,7 +233,7 @@ QToolBox::tab:selected, QToolBox::tab:hover{ 0 0 511 - 522 + 534 @@ -773,7 +773,7 @@ QToolBox::tab:selected, QToolBox::tab:hover{ 0 0 511 - 522 + 534 @@ -1162,7 +1162,7 @@ QToolBox::tab:selected, QToolBox::tab:hover{ 0 0 511 - 522 + 534 diff --git a/manuskript/ui/importers/__init__.py b/manuskript/ui/importers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manuskript/ui/importers/generalSettings.py b/manuskript/ui/importers/generalSettings.py new file mode 100644 index 00000000..3940aab7 --- /dev/null +++ b/manuskript/ui/importers/generalSettings.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- +import json +import os + +from PyQt5.QtCore import Qt, QSize, QSortFilterProxyModel, QModelIndex +from PyQt5.QtGui import QIcon, QFontMetrics, QFont +from PyQt5.QtWidgets import QWidget, QTableWidgetItem, QListWidgetItem, QTreeView + +from manuskript.functions import mainWindow, writablePath +from manuskript.ui.importers.generalSettings_ui import Ui_generalSettings +from manuskript.enums import Outline +from manuskript.ui import style + + +class generalSettings(QWidget, Ui_generalSettings): + def __init__(self, parent=None): + QWidget.__init__(self, parent) + self.setupUi(self) + + self.mw = mainWindow() + self.txtGeneralSplitScenes.setStyleSheet(style.lineEditSS()) + + # TreeView to select parent + # We use a proxy to display only folders + proxy = QSortFilterProxyModel() + proxy.setFilterKeyColumn(Outline.type.value) + proxy.setFilterFixedString("folder") + proxy.setSourceModel(self.mw.mdlOutline) + self.treeGeneralParent.setModel(proxy) + for i in range(1, self.mw.mdlOutline.columnCount()): + self.treeGeneralParent.hideColumn(i) + self.treeGeneralParent.setCurrentIndex(self.getParentIndex()) + self.chkGeneralParent.toggled.connect(self.treeGeneralParent.setVisible) + self.treeGeneralParent.hide() + + def getParentIndex(self): + """ + Returns the currently selected index in the mainWindow. + """ + if len(self.mw.treeRedacOutline.selectionModel(). + selection().indexes()) == 0: + idx = QModelIndex() + else: + idx = self.mw.treeRedacOutline.currentIndex() + return idx + + def importUnderID(self): + """ + Returns the ID of the item selected in treeGeneralParent, if checked. + """ + if self.chkGeneralParent.isChecked(): + idx = self.treeGeneralParent.currentIndex() + # We used a filter proxy model, so we have to map back to source + # to get an index from mdlOutline + idx = self.treeGeneralParent.model().mapToSource(idx) + if idx.isValid(): + return idx.internalPointer().ID() + + return "0" # 0 is root's ID + + def importInTopLevelFolder(self): + """ + Should the import be flat in the parent folder, or create a top-level + folder? + """ + return self.chkGeneralTopLevel.isChecked() + + def trimLongTitles(self): + return self.chkGeneralTrimTitles.isChecked() + + def splitScenes(self): + """ + Return wheter the user wants to split scenes. + If unchecked, returns False. + If checked, returns the escaped split mark, or default (in placeholderText). + """ + if self.chkGeneralSplitScenes.isChecked(): + split = self.txtGeneralSplitScenes.text() + + if not split: + split = self.txtGeneralSplitScenes.placeholderText() + + split = split.replace("\\n", "\n") + split = split.replace("\\t", "\t") + return split + + else: + return False + diff --git a/manuskript/ui/importers/generalSettings_ui.py b/manuskript/ui/importers/generalSettings_ui.py new file mode 100644 index 00000000..6ca528a9 --- /dev/null +++ b/manuskript/ui/importers/generalSettings_ui.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'manuskript/ui/importers/generalSettings_ui.ui' +# +# Created by: PyQt5 UI code generator 5.9 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_generalSettings(object): + def setupUi(self, generalSettings): + generalSettings.setObjectName("generalSettings") + generalSettings.resize(267, 401) + self.verticalLayout_2 = QtWidgets.QVBoxLayout(generalSettings) + self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) + self.verticalLayout_2.setSpacing(10) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.toolBox = QtWidgets.QToolBox(generalSettings) + self.toolBox.setStyleSheet("QToolBox::tab{\n" +" background-color: #BBB;\n" +" padding: 2px;\n" +" border: none;\n" +"}\n" +"\n" +"QToolBox::tab:selected, QToolBox::tab:hover{\n" +" background-color:skyblue;\n" +"}") + self.toolBox.setObjectName("toolBox") + self.general = QtWidgets.QWidget() + self.general.setGeometry(QtCore.QRect(0, 0, 267, 378)) + self.general.setObjectName("general") + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.general) + self.verticalLayout_5.setContentsMargins(6, 6, 6, 6) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.formLayout_4 = QtWidgets.QFormLayout() + self.formLayout_4.setRowWrapPolicy(QtWidgets.QFormLayout.WrapLongRows) + self.formLayout_4.setObjectName("formLayout_4") + self.chkGeneralSplitScenes = QtWidgets.QCheckBox(self.general) + self.chkGeneralSplitScenes.setObjectName("chkGeneralSplitScenes") + self.formLayout_4.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.chkGeneralSplitScenes) + self.txtGeneralSplitScenes = QtWidgets.QLineEdit(self.general) + self.txtGeneralSplitScenes.setText("") + self.txtGeneralSplitScenes.setObjectName("txtGeneralSplitScenes") + self.formLayout_4.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.txtGeneralSplitScenes) + self.chkGeneralTrimTitles = QtWidgets.QCheckBox(self.general) + self.chkGeneralTrimTitles.setObjectName("chkGeneralTrimTitles") + self.formLayout_4.setWidget(4, QtWidgets.QFormLayout.SpanningRole, self.chkGeneralTrimTitles) + self.treeGeneralParent = QtWidgets.QTreeView(self.general) + self.treeGeneralParent.setHeaderHidden(True) + self.treeGeneralParent.setObjectName("treeGeneralParent") + self.formLayout_4.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.treeGeneralParent) + self.chkGeneralParent = QtWidgets.QCheckBox(self.general) + self.chkGeneralParent.setObjectName("chkGeneralParent") + self.formLayout_4.setWidget(0, QtWidgets.QFormLayout.SpanningRole, self.chkGeneralParent) + self.chkGeneralTopLevel = QtWidgets.QCheckBox(self.general) + self.chkGeneralTopLevel.setObjectName("chkGeneralTopLevel") + self.formLayout_4.setWidget(2, QtWidgets.QFormLayout.SpanningRole, self.chkGeneralTopLevel) + self.verticalLayout_5.addLayout(self.formLayout_4) + self.toolBox.addItem(self.general, "") + self.verticalLayout_2.addWidget(self.toolBox) + + self.retranslateUi(generalSettings) + self.toolBox.setCurrentIndex(0) + self.toolBox.layout().setSpacing(0) + QtCore.QMetaObject.connectSlotsByName(generalSettings) + + def retranslateUi(self, generalSettings): + _translate = QtCore.QCoreApplication.translate + generalSettings.setWindowTitle(_translate("generalSettings", "Form")) + self.chkGeneralSplitScenes.setText(_translate("generalSettings", "Split scenes at:")) + self.txtGeneralSplitScenes.setPlaceholderText(_translate("generalSettings", "\\n---\\n")) + self.chkGeneralTrimTitles.setText(_translate("generalSettings", "Trim long titles (> 32 chars)")) + self.chkGeneralParent.setText(_translate("generalSettings", "Import under:")) + self.chkGeneralTopLevel.setText(_translate("generalSettings", "Import in a top-level folder")) + self.toolBox.setItemText(self.toolBox.indexOf(self.general), _translate("generalSettings", "General")) + diff --git a/manuskript/ui/importers/generalSettings_ui.ui b/manuskript/ui/importers/generalSettings_ui.ui new file mode 100644 index 00000000..955fc21a --- /dev/null +++ b/manuskript/ui/importers/generalSettings_ui.ui @@ -0,0 +1,136 @@ + + + generalSettings + + + + 0 + 0 + 267 + 401 + + + + Form + + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QToolBox::tab{ + background-color: #BBB; + padding: 2px; + border: none; +} + +QToolBox::tab:selected, QToolBox::tab:hover{ + background-color:skyblue; +} + + + 0 + + + 0 + + + + + 0 + 0 + 267 + 378 + + + + General + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + QFormLayout::WrapLongRows + + + + + Split scenes at: + + + + + + + + + + \n---\n + + + + + + + Trim long titles (> 32 chars) + + + + + + + true + + + + + + + Import under: + + + + + + + Import in a top-level folder + + + + + + + + + + + + + + diff --git a/manuskript/ui/importers/importer.py b/manuskript/ui/importers/importer.py new file mode 100644 index 00000000..f09ae44b --- /dev/null +++ b/manuskript/ui/importers/importer.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- +import json +import os + +from PyQt5.QtCore import Qt, QTimer, QUrl +from PyQt5.QtGui import QBrush, QColor, QIcon, QDesktopServices +from PyQt5.QtWidgets import QWidget, QFileDialog, QMessageBox, QStyle + +from manuskript.functions import lightBlue, writablePath, appPath +from manuskript.ui.importers.importer_ui import Ui_importer +from manuskript.ui.importers.generalSettings import generalSettings +from manuskript.ui import style +from manuskript import importer +from manuskript.models.outlineModel import outlineModel, outlineItem +from manuskript.enums import Outline +from manuskript.exporter.pandoc import pandocExporter + +class importerDialog(QWidget, Ui_importer): + + formatsIcon = { + ".epub": "application-epub+zip", + ".odt": "application-vnd.oasis.opendocument.text", + ".docx": "application-vnd.openxmlformats-officedocument.wordprocessingml.document", + ".md": "text-x-markdown", + ".rst": "text-plain", + ".tex": "text-x-tex", + ".opml": "text-x-opml+xml", + ".xml": "text-x-opml+xml", + ".html": "text-html", + } + + def __init__(self, parent=None, mw=None): + QWidget.__init__(self, parent) + self.setupUi(self) + + # Var + self.mw = mw + self.fileName = "" + self.setStyleSheet(style.mainWindowSS()) + self.tree.setStyleSheet("QTreeView{background:transparent;}") + self.editor.setStyleSheet("QWidget{background:transparent;}") + self.editor.toggleSpellcheck(False) + + # Register importFormats: + self.importers = importer.importers + + # Populate combo box with formats + self.populateImportList() + + # Connections + self.btnChoseFile.clicked.connect(self.selectFile) + self.btnClearFileName.clicked.connect(self.setFileName) + self.btnPreview.clicked.connect(self.preview) + self.btnImport.clicked.connect(self.doImport) + self.cmbImporters.currentTextChanged.connect(self.updateSettings) + + self.setFileName("") + self.updateSettings() + + ############################################################################ + # Combobox / Formats + ############################################################################ + + def populateImportList(self): + + def addFormat(name, icon, identifier): + # Identifier serves to distingues 2 importers that would have the + # same name. + self.cmbImporters.addItem(QIcon.fromTheme(icon), name, identifier) + + def addHeader(name): + self.cmbImporters.addItem(name, "header") + self.cmbImporters.setItemData(self.cmbImporters.count() - 1, QBrush(QColor(Qt.darkBlue)), Qt.ForegroundRole) + self.cmbImporters.setItemData(self.cmbImporters.count() - 1, QBrush(lightBlue()), Qt.BackgroundRole) + item = self.cmbImporters.model().item(self.cmbImporters.count() - 1) + item.setFlags(Qt.ItemIsEnabled) + + lastEngine = "" + + for f in self.importers: + # Header + if f.engine != lastEngine: + addHeader(f.engine) + lastEngine = f.engine + + addFormat(f.name, f.icon, "{}:{}".format(f.engine, f.name)) + if not f.isValid(): + item = self.cmbImporters.model().item(self.cmbImporters.count() - 1) + item.setFlags(Qt.NoItemFlags) + + if not pandocExporter().isValid(): + self.cmbImporters.addItem( + self.style().standardIcon(QStyle.SP_MessageBoxWarning), + "Install pandoc to import from much more formats", + "::URL::http://pandoc.org/installing.html") + + self.cmbImporters.setCurrentIndex(1) + + def currentFormat(self): + formatIdentifier = self.cmbImporters.currentData() + + if formatIdentifier == "header": + return None + + F = [F for F in self.importers + if formatIdentifier == "{}:{}".format(F.engine, F.name)][0] + # We instantiate the class + return F() + + ############################################################################ + # Import file + ############################################################################ + + def selectFile(self): + """ + Called to select a file in the file system. Uses QFileDialog. + """ + + # We find the current selected format + F = self._format + + options = QFileDialog.Options() + options |= QFileDialog.DontUseNativeDialog + if F.fileFormat == "<>": + options = QFileDialog.DontUseNativeDialog | QFileDialog.ShowDirsOnly + fileName = QFileDialog.getExistingDirectory(self, "Select import folder", + "", options=options) + else: + fileName, _ = QFileDialog.getOpenFileName(self, "Import from file", "", + F.fileFormat, options=options) + self.setFileName(fileName) + + def setFileName(self, fileName): + """ + Updates Ui with given filename. Filename can be empty. + """ + if fileName: + self.fileName = fileName + self.lblFileName.setText(os.path.basename(fileName)) + self.lblFileName.setToolTip(fileName) + ext = os.path.splitext(fileName)[1] + icon = None + if ext and ext in self.formatsIcon: + icon = QIcon.fromTheme(self.formatsIcon[ext]) + elif os.path.isdir(fileName): + icon = QIcon.fromTheme("folder") + + if icon: + self.lblIcon.setVisible(True) + h = self.lblFileName.height() + self.lblIcon.setPixmap(icon.pixmap(h, h)) + else: + self.lblIcon.hide() + + else: + self.fileName = None + self.lblFileName.setText("") + + hasFile = True if fileName else False + + self.btnClearFileName.setVisible(hasFile) + self.lblIcon.setVisible(hasFile) + self.btnChoseFile.setVisible(not hasFile) + self.btnPreview.setEnabled(hasFile) + self.btnImport.setEnabled(hasFile) + + ############################################################################ + # UI + ############################################################################ + + def updateSettings(self): + """ + When the current format change (through the combobox), we update the + settings widget using the current format provided settings widget. + """ + + # We check if we have to open an URL + data = self.cmbImporters.currentData() + if data and data[:7] == "::URL::" and data[7:]: + # FIXME: use functions.openURL after merge with feature/Exporters + QDesktopServices.openUrl(QUrl(data[7:])) + return + + F = self.currentFormat() + self._format = F + + # Checking if we have a valid importer (otherwise a header) + if not F: + self.grpSettings.setEnabled(False) + self.grpPreview.setEnabled(False) + return + self.grpSettings.setEnabled(True) + self.grpPreview.setEnabled(True) + + self.settingsWidget = generalSettings() + #TODO: custom format widget + self.settingsWidget = F.settingsWidget(self.settingsWidget) + + # Set the settings widget in place + self.setGroupWidget(self.grpSettings, self.settingsWidget) + self.grpSettings.setMinimumWidth(200) + + # Clear file name + self.setFileName("") + + def setGroupWidget(self, group, widget): + """ + Sets the given widget as main widget for QGroupBox group. + """ + + # Removes every items from given layout. + l = group.layout() + while l.count(): + item = l.itemAt(0) + l.removeItem(item) + item.widget().deleteLater() + + l.addWidget(widget) + widget.setParent(group) + + ############################################################################ + # Preview / Import + ############################################################################ + + def preview(self): + + # Creating a temporary outlineModel + previewModel = outlineModel(self) + previewModel.loadFromXML( + self.mw.mdlOutline.saveToXML(), + fromString=True) + + # Inserting elements + result = self.startImport(previewModel) + + if result: + self.tree.setModel(previewModel) + for i in range(1, previewModel.columnCount()): + self.tree.hideColumn(i) + self.tree.selectionModel().currentChanged.connect(self.editor.setCurrentModelIndex) + self.previewSplitter.setStretchFactor(0, 10) + self.previewSplitter.setStretchFactor(1, 40) + + def doImport(self): + """ + Called by the Import button. + """ + self.startImport(self.mw.mdlOutline) + + # Signal every views that important model changes have happened. + self.mw.mdlOutline.layoutChanged.emit() + + # I'm getting seg fault over this message sometimes... + # Using status bar message instead... + #QMessageBox.information(self, self.tr("Import status"), + #self.tr("Import Complete.")) + self.mw.statusBar().showMessage("Import complete!", 5000) + + self.close() + + def startImport(self, outlineModel): + """ + Where most of the magic happens. + Is used by preview and by doImport (actual import). + + `outlineModel` is the model where the imported items are added. + + FIXME: Optimisation: when adding many outlineItems, outlineItem.updateWordCount + is a bottleneck. It gets called a crazy number of time, and its not + necessary. + """ + + items = [] + + # We find the current selected format + F = self._format + + # Parent item + ID = self.settingsWidget.importUnderID() + parentItem = outlineModel.getItemByID(ID) + + # Import in top-level folder? + if self.settingsWidget.importInTopLevelFolder(): + parent = outlineItem(title=os.path.basename(self.fileName), + parent=parentItem) + parentItem = parent + items.append(parent) + + # Calling the importer + rItems = F.startImport(self.fileName, + parentItem, + self.settingsWidget) + + items.extend(rItems) + + # Do transformations + items = self.doTransformations(items) + + return True + + def doTransformations(self, items): + """ + Do general transformations. + """ + + # Trim long titles + if self.settingsWidget.trimLongTitles(): + for item in items: + if len(item.title()) > 32: + item.setData(Outline.title.value, item.title()[:32]) + + # Split at + if self.settingsWidget.splitScenes(): + for item in items: + item.split(self.settingsWidget.splitScenes(), recursive=False) + + return items + + diff --git a/manuskript/ui/importers/importer_ui.py b/manuskript/ui/importers/importer_ui.py new file mode 100644 index 00000000..d23c8e4e --- /dev/null +++ b/manuskript/ui/importers/importer_ui.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'manuskript/ui/importers/importer_ui.ui' +# +# Created by: PyQt5 UI code generator 5.9 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_importer(object): + def setupUi(self, importer): + importer.setObjectName("importer") + importer.resize(867, 560) + self.verticalLayout = QtWidgets.QVBoxLayout(importer) + self.verticalLayout.setObjectName("verticalLayout") + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.label = QtWidgets.QLabel(importer) + self.label.setObjectName("label") + self.horizontalLayout.addWidget(self.label) + self.cmbImporters = QtWidgets.QComboBox(importer) + self.cmbImporters.setObjectName("cmbImporters") + self.horizontalLayout.addWidget(self.cmbImporters) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.btnChoseFile = QtWidgets.QPushButton(importer) + icon = QtGui.QIcon.fromTheme("document-import") + self.btnChoseFile.setIcon(icon) + self.btnChoseFile.setObjectName("btnChoseFile") + self.horizontalLayout.addWidget(self.btnChoseFile) + self.lblIcon = QtWidgets.QLabel(importer) + self.lblIcon.setText("") + self.lblIcon.setObjectName("lblIcon") + self.horizontalLayout.addWidget(self.lblIcon) + self.lblFileName = QtWidgets.QLabel(importer) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.lblFileName.setFont(font) + self.lblFileName.setText("") + self.lblFileName.setObjectName("lblFileName") + self.horizontalLayout.addWidget(self.lblFileName) + self.btnClearFileName = QtWidgets.QPushButton(importer) + self.btnClearFileName.setText("") + icon = QtGui.QIcon.fromTheme("edit-clear") + self.btnClearFileName.setIcon(icon) + self.btnClearFileName.setFlat(True) + self.btnClearFileName.setObjectName("btnClearFileName") + self.horizontalLayout.addWidget(self.btnClearFileName) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem1) + self.btnPreview = QtWidgets.QPushButton(importer) + icon = QtGui.QIcon.fromTheme("document-print-preview") + self.btnPreview.setIcon(icon) + self.btnPreview.setFlat(True) + self.btnPreview.setObjectName("btnPreview") + self.horizontalLayout.addWidget(self.btnPreview) + self.btnImport = QtWidgets.QPushButton(importer) + icon = QtGui.QIcon.fromTheme("document-import") + self.btnImport.setIcon(icon) + self.btnImport.setObjectName("btnImport") + self.horizontalLayout.addWidget(self.btnImport) + self.verticalLayout.addLayout(self.horizontalLayout) + self.splitter = QtWidgets.QSplitter(importer) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setChildrenCollapsible(False) + self.splitter.setObjectName("splitter") + self.grpSettings = QtWidgets.QGroupBox(self.splitter) + self.grpSettings.setObjectName("grpSettings") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.grpSettings) + self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) + self.verticalLayout_3.setSpacing(0) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.grpPreview = QtWidgets.QGroupBox(self.splitter) + self.grpPreview.setObjectName("grpPreview") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.grpPreview) + self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) + self.verticalLayout_2.setSpacing(0) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.previewSplitter = QtWidgets.QSplitter(self.grpPreview) + self.previewSplitter.setOrientation(QtCore.Qt.Horizontal) + self.previewSplitter.setObjectName("previewSplitter") + self.tree = QtWidgets.QTreeView(self.previewSplitter) + self.tree.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.tree.setHeaderHidden(True) + self.tree.setObjectName("tree") + self.editor = editorWidget(self.previewSplitter) + self.editor.setObjectName("editor") + self.verticalLayout_2.addWidget(self.previewSplitter) + self.verticalLayout.addWidget(self.splitter) + + self.retranslateUi(importer) + QtCore.QMetaObject.connectSlotsByName(importer) + + def retranslateUi(self, importer): + _translate = QtCore.QCoreApplication.translate + importer.setWindowTitle(_translate("importer", "Import")) + self.label.setText(_translate("importer", "Format:")) + self.btnChoseFile.setText(_translate("importer", "Chose file")) + self.btnClearFileName.setToolTip(_translate("importer", "Clear file")) + self.btnPreview.setText(_translate("importer", "Preview")) + self.btnImport.setText(_translate("importer", "Import")) + self.grpSettings.setTitle(_translate("importer", "Settings")) + self.grpPreview.setTitle(_translate("importer", "Preview")) + +from manuskript.ui.editors.editorWidget import editorWidget diff --git a/manuskript/ui/importers/importer_ui.ui b/manuskript/ui/importers/importer_ui.ui new file mode 100644 index 00000000..a7f2f907 --- /dev/null +++ b/manuskript/ui/importers/importer_ui.ui @@ -0,0 +1,211 @@ + + + importer + + + + 0 + 0 + 867 + 560 + + + + Import + + + + + + + + Format: + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Chose file + + + + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup + + + + + + + + + + + + + + + 75 + true + + + + + + + + + + + Clear file + + + + + + + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Preview + + + + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup + + + true + + + + + + + Import + + + + + + + + + + + + Qt::Horizontal + + + false + + + + Settings + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + Preview + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + QAbstractItemView::NoEditTriggers + + + true + + + + + + + + + + + + + + editorWidget + QWidget +
manuskript.ui.editors.editorWidget.h
+ 1 +
+
+ + +
diff --git a/manuskript/ui/mainWindow.py b/manuskript/ui/mainWindow.py index 73237964..26b92db6 100644 --- a/manuskript/ui/mainWindow.py +++ b/manuskript/ui/mainWindow.py @@ -1189,12 +1189,17 @@ class Ui_MainWindow(object): icon = QtGui.QIcon.fromTheme("stock_view-details") self.actAbout.setIcon(icon) self.actAbout.setObjectName("actAbout") + self.actImport = QtWidgets.QAction(MainWindow) + icon = QtGui.QIcon.fromTheme("document-import") + self.actImport.setIcon(icon) + self.actImport.setObjectName("actImport") self.menuFile.addAction(self.actOpen) self.menuFile.addAction(self.menuRecents.menuAction()) self.menuFile.addAction(self.actSave) self.menuFile.addAction(self.actSaveAs) self.menuFile.addAction(self.actCloseProject) self.menuFile.addSeparator() + self.menuFile.addAction(self.actImport) self.menuFile.addAction(self.actCompile) self.menuFile.addSeparator() self.menuFile.addAction(self.actQuit) @@ -1360,6 +1365,8 @@ class Ui_MainWindow(object): self.actToolFrequency.setText(_translate("MainWindow", "&Frequency Analyzer")) self.actAbout.setText(_translate("MainWindow", "&About")) self.actAbout.setToolTip(_translate("MainWindow", "About Manuskript")) + self.actImport.setText(_translate("MainWindow", "Import…")) + self.actImport.setShortcut(_translate("MainWindow", "F7")) from manuskript.ui.cheatSheet import cheatSheet from manuskript.ui.editors.mainEditor import mainEditor diff --git a/manuskript/ui/mainWindow.ui b/manuskript/ui/mainWindow.ui index 05063d73..d13d8752 100644 --- a/manuskript/ui/mainWindow.ui +++ b/manuskript/ui/mainWindow.ui @@ -2116,6 +2116,7 @@ + @@ -2482,6 +2483,17 @@ QListView::item:hover { About Manuskript
+ + + + + + Import… + + + F7 + + diff --git a/manuskript/ui/views/outlineBasics.py b/manuskript/ui/views/outlineBasics.py index 8c21a8ec..7b12f794 100644 --- a/manuskript/ui/views/outlineBasics.py +++ b/manuskript/ui/views/outlineBasics.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # --!-- coding: utf8 --!-- from PyQt5.QtCore import Qt, QSignalMapper, QSize -from PyQt5.QtGui import QIcon +from PyQt5.QtGui import QIcon, QCursor from PyQt5.QtWidgets import QAbstractItemView, qApp, QMenu, QAction from PyQt5.QtWidgets import QListWidget, QWidgetAction, QListWidgetItem, QLineEdit @@ -14,7 +14,7 @@ from manuskript.models.outlineModel import outlineItem class outlineBasics(QAbstractItemView): def __init__(self, parent=None): - pass + self._indexesToOpen = None def getSelection(self): sel = [] @@ -39,11 +39,44 @@ class outlineBasics(QAbstractItemView): menu = QMenu(self) - # Open items - self.actOpen = QAction(QIcon.fromTheme("go-right"), qApp.translate("outlineBasics", "Open Item"), menu) + # Get index under cursor + pos = self.viewport().mapFromGlobal(QCursor.pos()) + mouseIndex = self.indexAt(pos) + + # Get index's title + if mouseIndex.isValid(): + title = mouseIndex.internalPointer().title() + + elif self.rootIndex().parent().isValid(): + # mouseIndex is the background of an item, so we check the parent + mouseIndex = self.rootIndex().parent() + title = mouseIndex.internalPointer().title() + + else: + title = self.tr("Root") + + if len(title) > 25: + title = title[:25] + "…" + + # Open Item action + self.actOpen = QAction(QIcon.fromTheme("go-right"), + qApp.translate("outlineBasics", "Open {}".format(title)), + menu) self.actOpen.triggered.connect(self.openItem) menu.addAction(self.actOpen) + # Open item(s) in new tab + if mouseIndex in sel and len(sel) > 1: + actionTitle = self.tr("Open {} items in new tabs").format(len(sel)) + self._indexesToOpen = sel + else: + actionTitle = self.tr("Open {} in a new tab").format(title) + self._indexesToOpen = [mouseIndex] + + self.actNewTab = QAction(QIcon.fromTheme("go-right"), actionTitle, menu) + self.actNewTab.triggered.connect(self.openItemsInNewTabs) + menu.addAction(self.actNewTab) + menu.addSeparator() # Rename / add / remove items @@ -185,7 +218,6 @@ class outlineBasics(QAbstractItemView): self.actAddText.setEnabled(False) if len(sel) == 0: - self.actOpen.setEnabled(False) self.actCopy.setEnabled(False) self.actCut.setEnabled(False) self.actRename.setEnabled(False) @@ -201,10 +233,15 @@ class outlineBasics(QAbstractItemView): return menu def openItem(self): - idx = self.currentIndex() + #idx = self.currentIndex() + idx = self._indexesToOpen[0] from manuskript.functions import MW MW.openIndex(idx) + def openItemsInNewTabs(self): + from manuskript.functions import MW + MW.openIndexes(self._indexesToOpen) + def rename(self): if len(self.getSelection()) == 1: index = self.currentIndex() diff --git a/manuskript/ui/views/treeView.py b/manuskript/ui/views/treeView.py index 53a063f7..4e28d1c9 100644 --- a/manuskript/ui/views/treeView.py +++ b/manuskript/ui/views/treeView.py @@ -31,30 +31,13 @@ class treeView(QTreeView, dndView, outlineBasics): def makePopupMenu(self): menu = outlineBasics.makePopupMenu(self) - first = menu.actions()[0] + first = menu.actions()[3] # Open item in new tab - sel = self.selectedIndexes() + #sel = self.selectedIndexes() pos = self.viewport().mapFromGlobal(QCursor.pos()) mouseIndex = self.indexAt(pos) - if mouseIndex.isValid(): - mouseTitle = mouseIndex.internalPointer().title() - else: - mouseTitle = self.tr("Root") - - if mouseIndex in sel and len(sel) > 1: - actionTitle = self.tr("Open {} items in new tabs").format(len(sel)) - self._indexesToOpen = sel - else: - actionTitle = self.tr("Open {} in a new tab").format(mouseTitle) - self._indexesToOpen = [mouseIndex] - - self.actNewTab = QAction(actionTitle, menu) - self.actNewTab.triggered.connect(self.openNewTab) - menu.insertAction(first, self.actNewTab) - menu.insertSeparator(first) - # Expand /collapse item if mouseIndex.isValid(): # index = self.currentIndex() @@ -83,9 +66,6 @@ class treeView(QTreeView, dndView, outlineBasics): return menu - def openNewTab(self): - mainWindow().mainEditor.openIndexes(self._indexesToOpen, newTab=True) - def expandCurrentIndex(self, index=None): if index is None or type(index) == bool: index = self._indexesToOpen[0] # self.currentIndex()