diff --git a/manuskript/import_export/opml.py b/manuskript/import_export/opml.py index e178ddc3..9dc63277 100644 --- a/manuskript/import_export/opml.py +++ b/manuskript/import_export/opml.py @@ -2,69 +2,112 @@ # --!-- coding: utf8 --!-- # Import/export outline cards in OPML format - +from PyQt5.QtWidgets import QMessageBox from manuskript.models.outlineModel import outlineItem from manuskript.enums import Outline -import xmltodict +from lxml import etree as ET from manuskript.functions import mainWindow -from PyQt5.QtCore import QModelIndex -def exportOpml(): - return True - - -def importOpml(opmlFilePath): - with open(opmlFilePath, 'r') as opmlFile: - opmlContent = saveNewlines(opmlFile.read()) - +def importOpml(opmlFilePath, idx): + ret = False mw = mainWindow() + + try: + with open(opmlFilePath, 'r') as opmlFile: + opmlContent = saveNewlines(opmlFile.read()) + except: + # TODO: Translation + QMessageBox.critical(mw, mw.tr("OPML Import"), + mw.tr("File open failed.")) + return False + mdl = mw.mdlOutline - dict = xmltodict.parse(opmlContent, strip_whitespace=False) + if idx.internalPointer() is not None: + parentItem = idx.internalPointer() + else: + parentItem = mdl.rootItem - opmlNode = dict['opml'] - bodyNode = opmlNode['body'] + try: + parsed = ET.fromstring(bytes(opmlContent, 'utf-8')) - outline = bodyNode['outline'] + opmlNode = parsed + bodyNode = opmlNode.find("body") - for element in outline: - parseItems(element, mdl.rootItem) + if bodyNode is not None: + outlineEls = bodyNode.findall("outline") - mdl.layoutChanged.emit() + if outlineEls is not None: + for element in outlineEls: + parseItems(element, parentItem) - mw.treeRedacOutline.viewport().update() + mdl.layoutChanged.emit() + mw.treeRedacOutline.viewport().update() + ret = True + except: + pass - return True + # TODO: Translation + if ret: + QMessageBox.information(mw, mw.tr("OPML Import"), + mw.tr("Import Complete.")) + else: + QMessageBox.critical(mw, mw.tr("OPML Import"), + mw.tr("This does not appear to be a valid OPML file.")) + + return ret def parseItems(underElement, parentItem): - if '@text' in underElement: - card = outlineItem(parent=parentItem, title=underElement['@text']) + text = underElement.get('text') + if text is not None: + """ + In the case where the title is exceptionally long, trim it so it isn't + distracting in the tab label + """ + title = text[0:32] + if len(title) < len(text): + title += '...' - text = "" + card = outlineItem(parent=parentItem, title=title) + + body = "" summary = "" - if '@_note' in underElement: - text = restoreNewLines(underElement['@_note']) - summary = text[0:128] + note = underElement.get('_note') + if note is not None and not isWhitespaceOnly(note): + body = restoreNewLines(note) + summary = body[0:128] + else: + """ + There's no note (body), but there is a title. Fill the + body with the title to support cards that consist only + of a title. + """ + body = text card.setData(Outline.summaryFull.value, summary) - if 'outline' in underElement: - elements = underElement['outline'] - - for el in elements: + children = underElement.findall('outline') + if children is not None and len(children) > 0: + for el in children: parseItems(el, card) else: card.setData(Outline.type.value, 'md') - card.setData(Outline.text.value, text) + card.setData(Outline.text.value, body) - # I assume I don't have to do the following - # parentItem.appendChild(card) + # I assume I don't have to do the following + # parentItem.appendChild(card) return +""" +Since XML parsers are notorious for stripping out significant newlines, +save them in a form we can restore after the parse. +""" + + def saveNewlines(inString): inString = inString.replace("\r\n", "\n") inString = inString.replace("\n", "{{lf}}") @@ -72,6 +115,22 @@ def saveNewlines(inString): return inString +""" +Restore any significant newlines +""" + + def restoreNewLines(inString): return inString.replace("{{lf}}", "\n") + +""" +Determine whether or not a string only contains whitespace. +""" + + +def isWhitespaceOnly(inString): + str = restoreNewLines(inString) + str = ''.join(str.split()) + + return len(str) is 0 diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index 92850ff8..36d40e17 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -27,7 +27,6 @@ from manuskript.ui.mainWindow import Ui_MainWindow from manuskript.ui.tools.frequencyAnalyzer import frequencyAnalyzer from manuskript.ui.views.outlineDelegates import outlineCharacterDelegate from manuskript.ui.views.plotDelegate import plotDelegate -from manuskript.import_export import opml as opmlInputExport # Spellcheck support from manuskript.ui.views.textEditView import textEditView @@ -98,13 +97,12 @@ 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.actImport, self.actSettings]: + 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.importOutline) self.actCompile.triggered.connect(self.doCompile) self.actLabels.triggered.connect(self.settingsLabel) self.actStatus.triggered.connect(self.settingsStatus) @@ -447,7 +445,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.actImport, self.actSettings]: + self.actCompile, self.actSettings]: i.setEnabled(True) # Add project name to Window's name @@ -491,7 +489,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.actImport, self.actSettings]: + self.actCompile, self.actSettings]: i.setEnabled(False) # Set Window's name - no project loaded @@ -627,10 +625,6 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.statusBar().showMessage( self.tr("Project {} loaded with some errors.").format(project), 5000) - def importOutline(self, project): - opmlInputExport.importOpml('/home/cstevenson/End Plan 2.opml') - return True - ############################################################################### # MAIN CONNECTIONS ############################################################################### diff --git a/manuskript/ui/editors/mainEditor.py b/manuskript/ui/editors/mainEditor.py index ab5c1b60..71571e7d 100644 --- a/manuskript/ui/editors/mainEditor.py +++ b/manuskript/ui/editors/mainEditor.py @@ -14,6 +14,7 @@ from manuskript.ui import style from manuskript.ui.editors.editorWidget import editorWidget from manuskript.ui.editors.fullScreenEditor import fullScreenEditor from manuskript.ui.editors.mainEditor_ui import Ui_mainEditor +from manuskript.import_export import opml as opmlInputExport locale.setlocale(locale.LC_ALL, '') @@ -44,6 +45,10 @@ class mainEditor(QWidget, Ui_mainEditor): self.btnRedacFullscreen.clicked.connect( self.showFullScreen, AUC) + self.btnImport.clicked.connect( + lambda v: self.importOPML() + ) + # self.tab.setDocumentMode(False) # Bug in Qt < 5.5: doesn't always load icons from custom theme. @@ -217,6 +222,7 @@ class mainEditor(QWidget, Ui_mainEditor): self.btnRedacFolderText.setVisible(visible) self.btnRedacFolderCork.setVisible(visible) self.btnRedacFolderOutline.setVisible(visible) + self.btnImport.setVisible(visible) self.sldCorkSizeFactor.setVisible(visible and self.btnRedacFolderCork.isChecked()) self.btnRedacFullscreen.setVisible(not visible) @@ -296,6 +302,20 @@ class mainEditor(QWidget, Ui_mainEditor): if self.currentEditor(): self._fullScreen = fullScreenEditor(self.currentEditor().currentIndex) + def importOPML(self): + from PyQt5.QtWidgets import QFileDialog + options = QFileDialog.Options() + options |= QFileDialog.DontUseNativeDialog + fileName, _ = QFileDialog.getOpenFileName(self, "Import OPML", "", + "OPML Files (*.opml)", options=options) + if fileName: + if len(self.mw.treeRedacOutline.selectionModel(). + selection().indexes()) == 0: + idx = QModelIndex() + else: + idx = self.mw.treeRedacOutline.currentIndex() + opmlInputExport.importOpml(fileName, idx) + ############################################################################### # DICT AND STUFF LIKE THAT ############################################################################### diff --git a/manuskript/ui/editors/mainEditor_ui.py b/manuskript/ui/editors/mainEditor_ui.py index 269f0fc1..22fd1f0c 100644 --- a/manuskript/ui/editors/mainEditor_ui.py +++ b/manuskript/ui/editors/mainEditor_ui.py @@ -55,6 +55,14 @@ class Ui_mainEditor(object): self.btnRedacFolderOutline.setObjectName("btnRedacFolderOutline") self.buttonGroup.addButton(self.btnRedacFolderOutline) self.horizontalLayout_19.addWidget(self.btnRedacFolderOutline) + self.btnImport = QtWidgets.QPushButton(mainEditor) + self.btnImport.setText("") + icon = QtGui.QIcon.fromTheme("document-open") + self.btnImport.setIcon(icon) + self.btnImport.setFlat(True) + self.btnImport.setObjectName("btnImport") + self.buttonGroup.addButton(self.btnImport) + self.horizontalLayout_19.addWidget(self.btnImport) self.sldCorkSizeFactor = QtWidgets.QSlider(mainEditor) self.sldCorkSizeFactor.setMinimumSize(QtCore.QSize(100, 0)) self.sldCorkSizeFactor.setMaximumSize(QtCore.QSize(200, 16777215)) @@ -109,6 +117,8 @@ class Ui_mainEditor(object): self.btnRedacFolderCork.setText(_translate("mainEditor", "Index cards")) self.btnRedacFolderOutline.setText(_translate("mainEditor", "Outline")) self.btnRedacFullscreen.setShortcut(_translate("mainEditor", "F11")) + # TODO: Translation + self.btnImport.setToolTip(_translate("mainEditor", "Import items from an OPML file into the current folder")) from manuskript.ui.editors.tabSplitter import tabSplitter from manuskript.ui.editors.textFormat import textFormat diff --git a/manuskript/ui/editors/mainEditor_ui.ui b/manuskript/ui/editors/mainEditor_ui.ui index 68a3a68f..ce73c973 100644 --- a/manuskript/ui/editors/mainEditor_ui.ui +++ b/manuskript/ui/editors/mainEditor_ui.ui @@ -111,6 +111,23 @@ + + + + + Import items from an OPML file into the current folder + + + + + + + + + true + + + diff --git a/manuskript/ui/mainWindow.py b/manuskript/ui/mainWindow.py index ad5d0933..a2a9a74e 100644 --- a/manuskript/ui/mainWindow.py +++ b/manuskript/ui/mainWindow.py @@ -1123,9 +1123,6 @@ class Ui_MainWindow(object): self.actSaveAs.setObjectName("actSaveAs") self.actQuit = QtWidgets.QAction(MainWindow) icon = QtGui.QIcon.fromTheme("application-exit") - self.actImport = QtWidgets.QAction(MainWindow) - self.actImport.setIcon(icon) - self.actImport.setObjectName("actImport") self.actQuit.setIcon(icon) self.actQuit.setObjectName("actQuit") self.actShowHelp = QtWidgets.QAction(MainWindow) @@ -1186,7 +1183,6 @@ class Ui_MainWindow(object): self.menuFile.addAction(self.menuRecents.menuAction()) self.menuFile.addAction(self.actSave) self.menuFile.addAction(self.actSaveAs) - self.menuFile.addAction(self.actImport) self.menuFile.addAction(self.actCloseProject) self.menuFile.addSeparator() self.menuFile.addAction(self.actCompile) @@ -1328,7 +1324,6 @@ class Ui_MainWindow(object): self.actSave.setShortcut(_translate("MainWindow", "Ctrl+S")) self.actSaveAs.setText(_translate("MainWindow", "Sa&ve as...")) self.actSaveAs.setShortcut(_translate("MainWindow", "Ctrl+Shift+S")) - self.actImport.setText("Import") self.actQuit.setText(_translate("MainWindow", "&Quit")) self.actQuit.setShortcut(_translate("MainWindow", "Ctrl+Q")) self.actShowHelp.setText(_translate("MainWindow", "&Show help texts"))