From bc70501373c7ce6b62f2b23e92da8cd6b6865512 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Thu, 9 Nov 2017 20:30:54 +0100 Subject: [PATCH 1/5] Fixes a bug when entering non-digit values for item's goal in metadata --- manuskript/functions.py | 9 ++++++--- manuskript/ui/views/propertiesView.py | 2 ++ manuskript/ui/views/propertiesView_ui.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/manuskript/functions.py b/manuskript/functions.py index b7873ad7..04d57b7a 100644 --- a/manuskript/functions.py +++ b/manuskript/functions.py @@ -24,9 +24,12 @@ def wordCount(text): def toInt(text): if text: - return int(text) - else: - return 0 + try: + return int(text) + except ValueError: + pass + + return 0 def toFloat(text): diff --git a/manuskript/ui/views/propertiesView.py b/manuskript/ui/views/propertiesView.py index e0fa7191..30857ef8 100644 --- a/manuskript/ui/views/propertiesView.py +++ b/manuskript/ui/views/propertiesView.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # --!-- coding: utf8 --!-- from PyQt5.QtWidgets import QWidget +from PyQt5.QtGui import QIntValidator from manuskript.enums import Outline from manuskript.ui.views.propertiesView_ui import Ui_propertiesView @@ -19,6 +20,7 @@ class propertiesView(QWidget, Ui_propertiesView): self.chkCompile.setModel(mdlOutline) self.txtTitle.setModel(mdlOutline) self.txtGoal.setModel(mdlOutline) + self.txtGoal.setValidator(QIntValidator(0, 9999999)) def getIndexes(self, sourceView): """Returns a list of indexes from list of QItemSelectionRange""" diff --git a/manuskript/ui/views/propertiesView_ui.py b/manuskript/ui/views/propertiesView_ui.py index 5b8a7e11..e4846fae 100644 --- a/manuskript/ui/views/propertiesView_ui.py +++ b/manuskript/ui/views/propertiesView_ui.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'manuskript/ui/views/propertiesView_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! From 3b17c4e2b4a44545b4dfdac3456e92047f15fa9b Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Thu, 9 Nov 2017 23:01:42 +0100 Subject: [PATCH 2/5] Adds: Menu Documents. Edit operations (copy, cut, paste, duplicate, remove), and Move up and down. --- icons/NumixMsk/128x128/places/folder-copy.svg | 157 ++++++++ icons/NumixMsk/16x16/actions/merge.svg | 6 + icons/NumixMsk/16x16/actions/split.svg | 7 + icons/NumixMsk/16x16/places/folder-copy.svg | 117 ++++++ icons/NumixMsk/22x22/actions/merge.svg | 6 + icons/NumixMsk/22x22/actions/split.svg | 7 + icons/NumixMsk/22x22/places/folder-copy.svg | 180 +++++++++ icons/NumixMsk/24x24/actions/merge.svg | 6 + icons/NumixMsk/24x24/actions/split.svg | 7 + icons/NumixMsk/24x24/places/folder-copy.svg | 180 +++++++++ icons/NumixMsk/256x256/places/folder-copy.svg | 162 ++++++++ icons/NumixMsk/32x32/actions/merge.svg | 6 + icons/NumixMsk/32x32/actions/split.svg | 7 + icons/NumixMsk/32x32/places/folder-copy.svg | 167 +++++++++ icons/NumixMsk/48x48/actions/merge.svg | 6 + icons/NumixMsk/48x48/actions/split.svg | 7 + icons/NumixMsk/48x48/places/folder-copy.svg | 352 ++++++++++++++++++ icons/NumixMsk/64x64/actions/merge.svg | 6 + icons/NumixMsk/64x64/actions/split.svg | 7 + icons/NumixMsk/64x64/places/folder-copy.svg | 193 ++++++++++ icons/NumixMsk/96x96/places/folder-copy.svg | 350 +++++++++++++++++ manuskript/mainWindow.py | 92 ++++- manuskript/models/outlineModel.py | 53 ++- manuskript/ui/editors/editorWidget.py | 36 +- manuskript/ui/editors/mainEditor.py | 19 +- manuskript/ui/mainWindow.py | 76 ++++ manuskript/ui/mainWindow.ui | 132 ++++++- manuskript/ui/views/outlineBasics.py | 56 +++ 28 files changed, 2374 insertions(+), 26 deletions(-) create mode 100644 icons/NumixMsk/128x128/places/folder-copy.svg create mode 100644 icons/NumixMsk/16x16/actions/merge.svg create mode 100644 icons/NumixMsk/16x16/actions/split.svg create mode 100644 icons/NumixMsk/16x16/places/folder-copy.svg create mode 100644 icons/NumixMsk/22x22/actions/merge.svg create mode 100644 icons/NumixMsk/22x22/actions/split.svg create mode 100644 icons/NumixMsk/22x22/places/folder-copy.svg create mode 100644 icons/NumixMsk/24x24/actions/merge.svg create mode 100644 icons/NumixMsk/24x24/actions/split.svg create mode 100644 icons/NumixMsk/24x24/places/folder-copy.svg create mode 100644 icons/NumixMsk/256x256/places/folder-copy.svg create mode 100644 icons/NumixMsk/32x32/actions/merge.svg create mode 100644 icons/NumixMsk/32x32/actions/split.svg create mode 100644 icons/NumixMsk/32x32/places/folder-copy.svg create mode 100644 icons/NumixMsk/48x48/actions/merge.svg create mode 100644 icons/NumixMsk/48x48/actions/split.svg create mode 100644 icons/NumixMsk/48x48/places/folder-copy.svg create mode 100644 icons/NumixMsk/64x64/actions/merge.svg create mode 100644 icons/NumixMsk/64x64/actions/split.svg create mode 100644 icons/NumixMsk/64x64/places/folder-copy.svg create mode 100644 icons/NumixMsk/96x96/places/folder-copy.svg diff --git a/icons/NumixMsk/128x128/places/folder-copy.svg b/icons/NumixMsk/128x128/places/folder-copy.svg new file mode 100644 index 00000000..6474bf44 --- /dev/null +++ b/icons/NumixMsk/128x128/places/folder-copy.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/NumixMsk/16x16/actions/merge.svg b/icons/NumixMsk/16x16/actions/merge.svg new file mode 100644 index 00000000..34bf799a --- /dev/null +++ b/icons/NumixMsk/16x16/actions/merge.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/NumixMsk/16x16/actions/split.svg b/icons/NumixMsk/16x16/actions/split.svg new file mode 100644 index 00000000..e4952319 --- /dev/null +++ b/icons/NumixMsk/16x16/actions/split.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/icons/NumixMsk/16x16/places/folder-copy.svg b/icons/NumixMsk/16x16/places/folder-copy.svg new file mode 100644 index 00000000..b4008db1 --- /dev/null +++ b/icons/NumixMsk/16x16/places/folder-copy.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/icons/NumixMsk/22x22/actions/merge.svg b/icons/NumixMsk/22x22/actions/merge.svg new file mode 100644 index 00000000..e3996fd9 --- /dev/null +++ b/icons/NumixMsk/22x22/actions/merge.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/NumixMsk/22x22/actions/split.svg b/icons/NumixMsk/22x22/actions/split.svg new file mode 100644 index 00000000..f0040158 --- /dev/null +++ b/icons/NumixMsk/22x22/actions/split.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/icons/NumixMsk/22x22/places/folder-copy.svg b/icons/NumixMsk/22x22/places/folder-copy.svg new file mode 100644 index 00000000..09e1ea9b --- /dev/null +++ b/icons/NumixMsk/22x22/places/folder-copy.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/NumixMsk/24x24/actions/merge.svg b/icons/NumixMsk/24x24/actions/merge.svg new file mode 100644 index 00000000..96f0ca7b --- /dev/null +++ b/icons/NumixMsk/24x24/actions/merge.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/NumixMsk/24x24/actions/split.svg b/icons/NumixMsk/24x24/actions/split.svg new file mode 100644 index 00000000..c2478721 --- /dev/null +++ b/icons/NumixMsk/24x24/actions/split.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/icons/NumixMsk/24x24/places/folder-copy.svg b/icons/NumixMsk/24x24/places/folder-copy.svg new file mode 100644 index 00000000..d86840b7 --- /dev/null +++ b/icons/NumixMsk/24x24/places/folder-copy.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/NumixMsk/256x256/places/folder-copy.svg b/icons/NumixMsk/256x256/places/folder-copy.svg new file mode 100644 index 00000000..483a5d8a --- /dev/null +++ b/icons/NumixMsk/256x256/places/folder-copy.svg @@ -0,0 +1,162 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/NumixMsk/32x32/actions/merge.svg b/icons/NumixMsk/32x32/actions/merge.svg new file mode 100644 index 00000000..dbd0db48 --- /dev/null +++ b/icons/NumixMsk/32x32/actions/merge.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/NumixMsk/32x32/actions/split.svg b/icons/NumixMsk/32x32/actions/split.svg new file mode 100644 index 00000000..04d6458a --- /dev/null +++ b/icons/NumixMsk/32x32/actions/split.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/icons/NumixMsk/32x32/places/folder-copy.svg b/icons/NumixMsk/32x32/places/folder-copy.svg new file mode 100644 index 00000000..d9f74d97 --- /dev/null +++ b/icons/NumixMsk/32x32/places/folder-copy.svg @@ -0,0 +1,167 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/NumixMsk/48x48/actions/merge.svg b/icons/NumixMsk/48x48/actions/merge.svg new file mode 100644 index 00000000..d8eb78a9 --- /dev/null +++ b/icons/NumixMsk/48x48/actions/merge.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/NumixMsk/48x48/actions/split.svg b/icons/NumixMsk/48x48/actions/split.svg new file mode 100644 index 00000000..67428247 --- /dev/null +++ b/icons/NumixMsk/48x48/actions/split.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/icons/NumixMsk/48x48/places/folder-copy.svg b/icons/NumixMsk/48x48/places/folder-copy.svg new file mode 100644 index 00000000..e943f0cb --- /dev/null +++ b/icons/NumixMsk/48x48/places/folder-copy.svg @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/NumixMsk/64x64/actions/merge.svg b/icons/NumixMsk/64x64/actions/merge.svg new file mode 100644 index 00000000..c81beca6 --- /dev/null +++ b/icons/NumixMsk/64x64/actions/merge.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/NumixMsk/64x64/actions/split.svg b/icons/NumixMsk/64x64/actions/split.svg new file mode 100644 index 00000000..af0d3d02 --- /dev/null +++ b/icons/NumixMsk/64x64/actions/split.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/icons/NumixMsk/64x64/places/folder-copy.svg b/icons/NumixMsk/64x64/places/folder-copy.svg new file mode 100644 index 00000000..31c3ab64 --- /dev/null +++ b/icons/NumixMsk/64x64/places/folder-copy.svg @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/NumixMsk/96x96/places/folder-copy.svg b/icons/NumixMsk/96x96/places/folder-copy.svg new file mode 100644 index 00000000..6fce318f --- /dev/null +++ b/icons/NumixMsk/96x96/places/folder-copy.svg @@ -0,0 +1,350 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index 4429d01c..19670e2c 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -53,7 +53,10 @@ class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self): QMainWindow.__init__(self) self.setupUi(self) + + # Var self.currentProject = None + self._lastFocus = None self.readSettings() @@ -101,6 +104,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.actImport, self.actCompile, self.actSettings]: i.setEnabled(False) + # Main Menu:: File self.actOpen.triggered.connect(self.welcome.openFile) self.actSave.triggered.connect(self.saveDatas) self.actSaveAs.triggered.connect(self.welcome.saveAsFile) @@ -111,10 +115,21 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.actSettings.triggered.connect(self.settingsWindow) self.actCloseProject.triggered.connect(self.closeProject) self.actQuit.triggered.connect(self.close) - self.actToolFrequency.triggered.connect(self.frequencyAnalyzer) - self.actAbout.triggered.connect(self.about) - self.generateViewMenu() + # Main menu:: Documents + self.actCopy.triggered.connect(self.documentsCopy) + self.actCut.triggered.connect(self.documentsCut) + self.actPaste.triggered.connect(self.documentsPaste) + self.actDuplicate.triggered.connect(self.documentsDuplicate) + self.actDelete.triggered.connect(self.documentsDelete) + self.actMoveUp.triggered.connect(self.documentsMoveUp) + self.actMoveDown.triggered.connect(self.documentsMoveDown) + self.actSplitDialog.triggered.connect(self.documentsSplitDialog) + self.actSplitCursor.triggered.connect(self.documentsSplitCursor) + self.actMerge.triggered.connect(self.documentsMerge) + + # Main Menu:: view + self.generateViewMenu() self.actModeGroup = QActionGroup(self) self.actModeSimple.setActionGroup(self.actModeGroup) self.actModeFiction.setActionGroup(self.actModeGroup) @@ -123,6 +138,10 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.actModeFiction.triggered.connect(self.setViewModeFiction) self.actModeSnowflake.setEnabled(False) + # Main Menu:: Tool + self.actToolFrequency.triggered.connect(self.frequencyAnalyzer) + self.actAbout.triggered.connect(self.about) + self.makeUIConnections() # self.loadProject(os.path.join(appPath(), "test_project.zip")) @@ -182,6 +201,33 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.toolbar.setVisible(True) self.stack.setCurrentIndex(1) + ############################################################################### + # GENERAL / UI STUFF + ############################################################################### + + def tabMainChanged(self): + "Called when main tab changes." + self.menuDocuments.menuAction().setVisible(self.tabMain.currentIndex() == self.TabRedac) + + def focusChanged(self, old, new): + """ + We get notified by qApp when focus changes, from old to new widget. + """ + + # Determine which item had focus last, to send the keyboard shortcuts + # to the right place + + targets = [ + self.treeRedacOutline, + self.mainEditor + ] + + while new is not None: + if new in targets: + self._lastFocus = new + break + new = new.parent() + ############################################################################### # SUMMARY ############################################################################### @@ -386,6 +432,33 @@ class MainWindow(QMainWindow, Ui_MainWindow): def openIndexes(self, indexes, newTab=True): self.mainEditor.openIndexes(indexes, newTab=True) + # Menu Documents ############################################################# + + # Functions called by the menu Documents + # self._lastFocus is the last editor that had focus (either treeView or + # mainEditor). So we just pass along the signal. + + def documentsCopy(self): + if self._lastFocus: self._lastFocus.copy() + def documentsCut(self): + if self._lastFocus: self._lastFocus.cut() + def documentsPaste(self): + if self._lastFocus: self._lastFocus.paste() + def documentsDuplicate(self): + if self._lastFocus: self._lastFocus.duplicate() + def documentsDelete(self): + if self._lastFocus: self._lastFocus.delete() + def documentsMoveUp(self): + if self._lastFocus: self._lastFocus.moveUp() + def documentsMoveDown(self): + if self._lastFocus: self._lastFocus.moveDown() + def documentsSplitDialog(self): + print("documentsSplitDialog::FIXME") + def documentsSplitCursor(self): + print("documentsSplitCursor::FIXME") + def documentsMerge(self): + print("documentsMerge::FIXME") + ############################################################################### # LOAD AND SAVE ############################################################################### @@ -667,6 +740,9 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.btnOutlineRemoveItem.clicked.connect(self.outlineRemoveItemsOutline, AUC) self.tabMain.currentChanged.connect(self.toolbar.setCurrentGroup) + self.tabMain.currentChanged.connect(self.tabMainChanged) + + qApp.focusChanged.connect(self.focusChanged) def makeConnections(self): @@ -988,16 +1064,6 @@ class MainWindow(QMainWindow, Ui_MainWindow): # Custom "tab" bar on the left self.lstTabs.setIconSize(QSize(48, 48)) for i in range(self.tabMain.count()): - #icons = ["general-128px.png", - #"summary-128px.png", - #"characters-128px.png", - #"plot-128px.png", - #"world-128px.png", - #"outline-128px.png", - #"editor-128px.png", - #"" - #] - #self.tabMain.setTabIcon(i, QIcon(appPath("icons/Custom/Tabs/{}".format(icons[i])))) icons = [QIcon.fromTheme("stock_view-details"), #info QIcon.fromTheme("application-text-template"), #applications-publishing diff --git a/manuskript/models/outlineModel.py b/manuskript/models/outlineModel.py index e3f61e57..8831c550 100644 --- a/manuskript/models/outlineModel.py +++ b/manuskript/models/outlineModel.py @@ -273,6 +273,19 @@ class outlineModel(QAbstractItemModel): if items is None: return False + # We check if parent is not a child of one of the items + if self.isParentAChildOfItems(parent, items): + return False + + return True + + def isParentAChildOfItems(self, parent, items): + """ + Takes a parent index, and a list of outlineItems items. Check whether + parent is in a child of one of the items. + Return True in that case, False if not. + """ + # Get the parent item if not parent.isValid(): parentItem = self.rootItem @@ -286,9 +299,9 @@ class outlineModel(QAbstractItemModel): # Is item in the path? It would mean that it tries to get dropped # as a children of himself. if item.ID() in path: - return False + return True - return True + return False def decodeMimeData(self, data): if not data.hasFormat("application/xml"): @@ -345,12 +358,13 @@ class outlineModel(QAbstractItemModel): if action == Qt.IgnoreAction: return True # What is that? - # Strangely, on some cases, we get a call to dropMimeData though - # self.canDropMimeData returned False. - # See https://github.com/olivierkes/manuskript/issues/169 to reproduce. - # So we double check for safety. - if not self.canDropMimeData(data, action, row, column, parent): - return False + if action == Qt.MoveAction: + # Strangely, on some cases, we get a call to dropMimeData though + # self.canDropMimeData returned False. + # See https://github.com/olivierkes/manuskript/issues/169 to reproduce. + # So we double check for safety. + if not self.canDropMimeData(data, action, row, column, parent): + return False items = self.decodeMimeData(data) if items is None: @@ -366,6 +380,21 @@ class outlineModel(QAbstractItemModel): else: beginRow = self.rowCount() + 1 + if action == Qt.CopyAction: + # Behavior if parent is a text item + # For example, we select a text and do: CTRL+C CTRL+V + if parent.isValid() and not parent.internalPointer().isFolder(): + # We insert copy in parent folder, just below + beginRow = parent.row() + 1 + parent = parent.parent() + + if parent.isValid() and parent.internalPointer().isFolder(): + while self.isParentAChildOfItems(parent, items): + # We are copying a folder on itself. Assume duplicates. + # Copy not in, but next to + beginRow = parent.row() + 1 + parent = parent.parent() + if not items: return False @@ -373,7 +402,7 @@ class outlineModel(QAbstractItemModel): if action == Qt.CopyAction: for item in items: - item.getUniqueID() + item.getUniqueID(recursive=True) return r @@ -934,9 +963,13 @@ class outlineItem(): # IDS ############################################################################### - def getUniqueID(self): + def getUniqueID(self, recursive=False): self.setData(Outline.ID.value, self._model.rootItem.findUniqueID()) + if recursive: + for c in self.children(): + c.getUniqueID(recursive) + def checkIDs(self): """This is called when a model is loaded. diff --git a/manuskript/ui/editors/editorWidget.py b/manuskript/ui/editors/editorWidget.py index 60ee4008..309d8eee 100644 --- a/manuskript/ui/editors/editorWidget.py +++ b/manuskript/ui/editors/editorWidget.py @@ -266,7 +266,8 @@ class editorWidget(QWidget, Ui_editorWidget_ui): self.currentIndex = QModelIndex() self.currentID = None - self.setView() + if self._model: + self.setView() def updateIndexFromID(self): """ @@ -323,3 +324,36 @@ class editorWidget(QWidget, Ui_editorWidget_ui): def setDict(self, dct): self.currentDict = dct self.dictChanged.emit(dct) + + ############################################################################### + # FUNCTIONS FOR MENU ACCESS + ############################################################################### + + def getCurrentItemView(self): + if self.folderView == "outline": + return self.outlineView + elif self.folderView == "cork": + return self.corkView + else: + return None + + def copy(self): + if self.getCurrentItemView(): self.getCurrentItemView().copy() + def cut(self): + if self.getCurrentItemView(): self.getCurrentItemView().cut() + def paste(self): + if self.getCurrentItemView(): self.getCurrentItemView().paste() + def duplicate(self): + if self.getCurrentItemView(): self.getCurrentItemView().duplicate() + def delete(self): + if self.getCurrentItemView(): self.getCurrentItemView().delete() + def moveUp(self): + if self.getCurrentItemView(): self.getCurrentItemView().moveUp() + def moveDown(self): + if self.getCurrentItemView(): self.getCurrentItemView().moveDown() + def documentsSplitDialog(self): + print("documentsSplitDialog::FIXME") + def documentsSplitCursor(self): + print("documentsSplitCursor::FIXME") + def documentsMerge(self): + print("documentsMerge::FIXME") diff --git a/manuskript/ui/editors/mainEditor.py b/manuskript/ui/editors/mainEditor.py index f4591fa7..b4f920f7 100644 --- a/manuskript/ui/editors/mainEditor.py +++ b/manuskript/ui/editors/mainEditor.py @@ -172,7 +172,6 @@ class mainEditor(QWidget, Ui_mainEditor): ts = ts.secondTab return r - ############################################################################### # SELECTION AND UPDATES ############################################################################### @@ -245,6 +244,24 @@ class mainEditor(QWidget, Ui_mainEditor): return title + ############################################################################### + # FUNCTIONS FOR MENU ACCESS + ############################################################################### + + def copy(self): self.currentEditor().copy() + def cut(self): self.currentEditor().cut() + def paste(self): self.currentEditor().paste() + def duplicate(self): self.currentEditor().duplicate() + def delete(self): self.currentEditor().delete() + def moveUp(self): self.currentEditor().moveUp() + def moveDown(self): self.currentEditor().moveDown() + def documentsSplitDialog(self): + print("documentsSplitDialog::FIXME") + def documentsSplitCursor(self): + print("documentsSplitCursor::FIXME") + def documentsMerge(self): + print("documentsMerge::FIXME") + ############################################################################### # UI ############################################################################### diff --git a/manuskript/ui/mainWindow.py b/manuskript/ui/mainWindow.py index 26b92db6..d0005c49 100644 --- a/manuskript/ui/mainWindow.py +++ b/manuskript/ui/mainWindow.py @@ -1054,6 +1054,8 @@ class Ui_MainWindow(object): self.menuView.setObjectName("menuView") self.menuMode = QtWidgets.QMenu(self.menuView) self.menuMode.setObjectName("menuMode") + self.menuDocuments = QtWidgets.QMenu(self.menubar) + self.menuDocuments.setObjectName("menuDocuments") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") @@ -1193,6 +1195,46 @@ class Ui_MainWindow(object): icon = QtGui.QIcon.fromTheme("document-import") self.actImport.setIcon(icon) self.actImport.setObjectName("actImport") + self.actCopy = QtWidgets.QAction(MainWindow) + icon = QtGui.QIcon.fromTheme("edit-copy") + self.actCopy.setIcon(icon) + self.actCopy.setObjectName("actCopy") + self.actCut = QtWidgets.QAction(MainWindow) + icon = QtGui.QIcon.fromTheme("edit-cut") + self.actCut.setIcon(icon) + self.actCut.setObjectName("actCut") + self.actPaste = QtWidgets.QAction(MainWindow) + icon = QtGui.QIcon.fromTheme("edit-paste") + self.actPaste.setIcon(icon) + self.actPaste.setObjectName("actPaste") + self.actSplitDialog = QtWidgets.QAction(MainWindow) + icon = QtGui.QIcon.fromTheme("split") + self.actSplitDialog.setIcon(icon) + self.actSplitDialog.setObjectName("actSplitDialog") + self.actSplitCursor = QtWidgets.QAction(MainWindow) + icon = QtGui.QIcon.fromTheme("split") + self.actSplitCursor.setIcon(icon) + self.actSplitCursor.setObjectName("actSplitCursor") + self.actMerge = QtWidgets.QAction(MainWindow) + icon = QtGui.QIcon.fromTheme("merge") + self.actMerge.setIcon(icon) + self.actMerge.setObjectName("actMerge") + self.actDuplicate = QtWidgets.QAction(MainWindow) + icon = QtGui.QIcon.fromTheme("folder-copy") + self.actDuplicate.setIcon(icon) + self.actDuplicate.setObjectName("actDuplicate") + self.actDelete = QtWidgets.QAction(MainWindow) + icon = QtGui.QIcon.fromTheme("edit-delete") + self.actDelete.setIcon(icon) + self.actDelete.setObjectName("actDelete") + self.actMoveUp = QtWidgets.QAction(MainWindow) + icon = QtGui.QIcon.fromTheme("arrow-up") + self.actMoveUp.setIcon(icon) + self.actMoveUp.setObjectName("actMoveUp") + self.actMoveDown = QtWidgets.QAction(MainWindow) + icon = QtGui.QIcon.fromTheme("arrow-down") + self.actMoveDown.setIcon(icon) + self.actMoveDown.setObjectName("actMoveDown") self.menuFile.addAction(self.actOpen) self.menuFile.addAction(self.menuRecents.menuAction()) self.menuFile.addAction(self.actSave) @@ -1215,8 +1257,21 @@ class Ui_MainWindow(object): self.menuMode.addAction(self.actModeSnowflake) self.menuView.addAction(self.menuMode.menuAction()) self.menuView.addSeparator() + self.menuDocuments.addAction(self.actCopy) + self.menuDocuments.addAction(self.actCut) + self.menuDocuments.addAction(self.actPaste) + self.menuDocuments.addAction(self.actDuplicate) + self.menuDocuments.addAction(self.actDelete) + self.menuDocuments.addSeparator() + self.menuDocuments.addAction(self.actMoveUp) + self.menuDocuments.addAction(self.actMoveDown) + self.menuDocuments.addSeparator() + self.menuDocuments.addAction(self.actMerge) + self.menuDocuments.addAction(self.actSplitDialog) + self.menuDocuments.addAction(self.actSplitCursor) self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuEdit.menuAction()) + self.menubar.addAction(self.menuDocuments.menuAction()) self.menubar.addAction(self.menuView.menuAction()) self.menubar.addAction(self.menuTools.menuAction()) self.menubar.addAction(self.menuHelp.menuAction()) @@ -1334,6 +1389,7 @@ class Ui_MainWindow(object): self.menuEdit.setTitle(_translate("MainWindow", "&Edit")) self.menuView.setTitle(_translate("MainWindow", "&View")) self.menuMode.setTitle(_translate("MainWindow", "&Mode")) + self.menuDocuments.setTitle(_translate("MainWindow", "&Documents")) self.dckCheatSheet.setWindowTitle(_translate("MainWindow", "&Cheat sheet")) self.dckSearch.setWindowTitle(_translate("MainWindow", "Sea&rch")) self.dckNavigation.setWindowTitle(_translate("MainWindow", "&Navigation")) @@ -1367,6 +1423,26 @@ class Ui_MainWindow(object): self.actAbout.setToolTip(_translate("MainWindow", "About Manuskript")) self.actImport.setText(_translate("MainWindow", "Import…")) self.actImport.setShortcut(_translate("MainWindow", "F7")) + self.actCopy.setText(_translate("MainWindow", "Copy")) + self.actCopy.setShortcut(_translate("MainWindow", "Ctrl+C")) + self.actCut.setText(_translate("MainWindow", "Cut")) + self.actCut.setShortcut(_translate("MainWindow", "Ctrl+X")) + self.actPaste.setText(_translate("MainWindow", "Paste")) + self.actPaste.setShortcut(_translate("MainWindow", "Ctrl+V")) + self.actSplitDialog.setText(_translate("MainWindow", "Split…")) + self.actSplitDialog.setShortcut(_translate("MainWindow", "Ctrl+Shift+K")) + self.actSplitCursor.setText(_translate("MainWindow", "Split at cursor")) + self.actSplitCursor.setShortcut(_translate("MainWindow", "Ctrl+K")) + self.actMerge.setText(_translate("MainWindow", "Merge")) + self.actMerge.setShortcut(_translate("MainWindow", "Ctrl+M")) + self.actDuplicate.setText(_translate("MainWindow", "&Duplicate")) + self.actDuplicate.setShortcut(_translate("MainWindow", "Ctrl+D")) + self.actDelete.setText(_translate("MainWindow", "Delete")) + self.actDelete.setShortcut(_translate("MainWindow", "Del")) + self.actMoveUp.setText(_translate("MainWindow", "Move Up")) + self.actMoveUp.setShortcut(_translate("MainWindow", "Ctrl+Shift+Up")) + self.actMoveDown.setText(_translate("MainWindow", "Move Down")) + self.actMoveDown.setShortcut(_translate("MainWindow", "Ctrl+Shift+Down")) 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 d13d8752..e87bd792 100644 --- a/manuskript/ui/mainWindow.ui +++ b/manuskript/ui/mainWindow.ui @@ -2158,8 +2158,26 @@ + + + &Documents + + + + + + + + + + + + + + + @@ -2485,7 +2503,9 @@ QListView::item:hover { - + + + Import… @@ -2494,6 +2514,116 @@ QListView::item:hover { F7 + + + + + + Copy + + + Ctrl+C + + + + + + + + Cut + + + Ctrl+X + + + + + + + + Paste + + + Ctrl+V + + + + + + + + Split… + + + Ctrl+Shift+K + + + + + + + + Split at cursor + + + Ctrl+K + + + + + + + + Merge + + + Ctrl+M + + + + + + + + &Duplicate + + + Ctrl+D + + + + + + + + Delete + + + Del + + + + + + + + Move Up + + + Ctrl+Shift+Up + + + + + + + + Move Down + + + Ctrl+Shift+Down + + diff --git a/manuskript/ui/views/outlineBasics.py b/manuskript/ui/views/outlineBasics.py index 7b12f794..36c6c4b3 100644 --- a/manuskript/ui/views/outlineBasics.py +++ b/manuskript/ui/views/outlineBasics.py @@ -286,6 +286,62 @@ class outlineBasics(QAbstractItemView): def delete(self): self.model().removeIndexes(self.getSelection()) + def duplicate(self): + self.copy() + self.paste() + + def move(self, delta=1): + """ + Move selected items up or down. + """ + + # we store selected indexes + currentID = self.model().ID(self.currentIndex()) + selIDs = [self.model().ID(i) for i in self.selectedIndexes()] + + # Block signals + self.blockSignals(True) + self.selectionModel().blockSignals(True) + + # Move each index individually + for idx in self.selectedIndexes(): + self.moveIndex(idx, delta) + + # Done the hardcore way, so inform views + self.model().layoutChanged.emit() + + # restore selection + selIdx = [self.model().getIndexByID(ID) for ID in selIDs] + sm = self.selectionModel() + sm.clear() + [sm.select(idx, sm.Select) for idx in selIdx] + sm.setCurrentIndex(self.model().getIndexByID(currentID), sm.Select) + #self.setSelectionModel(sm) + + # Unblock signals + self.blockSignals(False) + self.selectionModel().blockSignals(False) + + def moveIndex(self, index, delta=1): + """ + Move the item represented by index. +1 means down, -1 means up. + """ + + if not index.isValid(): + return + + if index.parent().isValid(): + parentItem = index.parent().internalPointer() + else: + parentItem = index.model().rootItem + + parentItem.childItems.insert(index.row() + delta, + parentItem.childItems.pop(index.row())) + parentItem.updateWordCount(emit=False) + + def moveUp(self): self.move(-1) + def moveDown(self): self.move(+1) + def setPOV(self, POV): for i in self.getSelection(): self.model().setData(i.sibling(i.row(), Outline.POV.value), str(POV)) From a153606811a7ba0ca45ae76c9fc17915769a792a Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Fri, 10 Nov 2017 16:26:23 +0100 Subject: [PATCH 3/5] Adds: split dialog, split at cursor --- manuskript/mainWindow.py | 23 ++++++++++- manuskript/models/outlineModel.py | 33 +++++++++++++++- manuskript/settings.py | 8 +++- manuskript/ui/editors/editorWidget.py | 50 +++++++++++++++++++++--- manuskript/ui/editors/mainEditor.py | 6 +-- manuskript/ui/tools/splitDialog.py | 55 +++++++++++++++++++++++++++ manuskript/ui/views/outlineBasics.py | 50 ++++++++++++++++++++---- manuskript/ui/views/textEditView.py | 2 +- 8 files changed, 206 insertions(+), 21 deletions(-) create mode 100644 manuskript/ui/tools/splitDialog.py diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index 19670e2c..5618f25b 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -439,23 +439,42 @@ class MainWindow(QMainWindow, Ui_MainWindow): # mainEditor). So we just pass along the signal. def documentsCopy(self): + "Copy selected item(s)." if self._lastFocus: self._lastFocus.copy() def documentsCut(self): + "Cut selected item(s)." if self._lastFocus: self._lastFocus.cut() def documentsPaste(self): + "Paste clipboard item(s) into selected item." if self._lastFocus: self._lastFocus.paste() def documentsDuplicate(self): + "Duplicate selected item(s)." if self._lastFocus: self._lastFocus.duplicate() def documentsDelete(self): + "Delete selected item(s)." if self._lastFocus: self._lastFocus.delete() def documentsMoveUp(self): + "Move up selected item(s)." if self._lastFocus: self._lastFocus.moveUp() def documentsMoveDown(self): + "Move Down selected item(s)." if self._lastFocus: self._lastFocus.moveDown() + def documentsSplitDialog(self): - print("documentsSplitDialog::FIXME") + "Opens a dialog to split selected items." + if self._lastFocus: self._lastFocus.splitDialog() + # current items or selected items? + pass + # use outlineBasics, to do that on all selected items. + # use editorWidget to do that on selected text. + def documentsSplitCursor(self): - print("documentsSplitCursor::FIXME") + """ + Split current item (open in text editor) at cursor position. If there is + a text selection, that selection becomes the title of the new scene. + """ + if self._lastFocus and self._lastFocus == self.mainEditor: + self.mainEditor.splitCursor() def documentsMerge(self): print("documentsMerge::FIXME") diff --git a/manuskript/models/outlineModel.py b/manuskript/models/outlineModel.py index 8831c550..c38260a0 100644 --- a/manuskript/models/outlineModel.py +++ b/manuskript/models/outlineModel.py @@ -652,6 +652,7 @@ class outlineItem(): if column == Outline.text.value: wc = wordCount(data) self.setData(Outline.wordCount.value, wc) + self.emitDataChanged(cols=[Outline.text.value]) # new in 0.5.0 if column == Outline.compile.value: self.emitDataChanged(cols=[Outline.title.value, Outline.compile.value], recursive=True) @@ -894,9 +895,39 @@ class outlineItem(): item.setData(Outline.text.value, subTxt) # Inserting item - self.parent().insertChild(self.row()+k, item) + #self.parent().insertChild(self.row()+k, item) + self._model.insertItem(item, self.row()+k, self.parent().index()) k += 1 + def splitAt(self, position, length=0): + """ + Splits note at position p. + + If length is bigger than 0, it describes the length of the title, made + from the character following position. + """ + + txt = self.text() + + # Stores the new text + self.setData(Outline.text.value, txt[:position]) + + # Create a copy + item = self.copy() + + # Update title + if length > 0: + title = txt[position:position+length].replace("\n", "") + else: + title = "{}_{}".format(item.title(), 2) + item.setData(Outline.title.value, title) + + # Set text + item.setData(Outline.text.value, txt[position+length:]) + + # Inserting item using the model to signal views + self._model.insertItem(item, self.row()+1, self.parent().index()) + ############################################################################### # XML ############################################################################### diff --git a/manuskript/settings.py b/manuskript/settings.py index 4beb4b5b..5cb1bb43 100644 --- a/manuskript/settings.py +++ b/manuskript/settings.py @@ -96,13 +96,14 @@ frequencyAnalyzer = { viewMode = "fiction" # simple, fiction saveToZip = True +dontShowDeleteWarning = False def save(filename=None, protocol=None): global spellcheck, dict, corkSliderFactor, viewSettings, corkSizeFactor, folderView, lastTab, openIndexes, \ autoSave, autoSaveDelay, saveOnQuit, autoSaveNoChanges, autoSaveNoChangesDelay, outlineViewColumns, \ corkBackground, corkStyle, fullScreenTheme, defaultTextType, textEditor, revisions, frequencyAnalyzer, viewMode, \ - saveToZip + saveToZip, dontShowDeleteWarning allSettings = { "viewSettings": viewSettings, @@ -127,6 +128,7 @@ def save(filename=None, protocol=None): "frequencyAnalyzer": frequencyAnalyzer, "viewMode": viewMode, "saveToZip": saveToZip, + "dontShowDeleteWarning": dontShowDeleteWarning, } #pp=pprint.PrettyPrinter(indent=4, compact=False) @@ -294,3 +296,7 @@ def load(string, fromString=False, protocol=None): if "saveToZip" in allSettings: global saveToZip saveToZip = allSettings["saveToZip"] + + if "dontShowDeleteWarning" in allSettings: + global dontShowDeleteWarning + dontShowDeleteWarning = allSettings["dontShowDeleteWarning"] diff --git a/manuskript/ui/editors/editorWidget.py b/manuskript/ui/editors/editorWidget.py index 309d8eee..10903072 100644 --- a/manuskript/ui/editors/editorWidget.py +++ b/manuskript/ui/editors/editorWidget.py @@ -8,6 +8,7 @@ from manuskript import settings from manuskript.functions import AUC, mainWindow from manuskript.ui.editors.editorWidget_ui import Ui_editorWidget_ui from manuskript.ui.views.textEditView import textEditView +from manuskript.ui.tools.splitDialog import splitDialog class editorWidget(QWidget, Ui_editorWidget_ui): @@ -330,7 +331,9 @@ class editorWidget(QWidget, Ui_editorWidget_ui): ############################################################################### def getCurrentItemView(self): - if self.folderView == "outline": + if self.stack.currentIndex() == 0: + return self.txtRedacText + elif self.folderView == "outline": return self.outlineView elif self.folderView == "cork": return self.corkView @@ -351,9 +354,46 @@ class editorWidget(QWidget, Ui_editorWidget_ui): if self.getCurrentItemView(): self.getCurrentItemView().moveUp() def moveDown(self): if self.getCurrentItemView(): self.getCurrentItemView().moveDown() - def documentsSplitDialog(self): - print("documentsSplitDialog::FIXME") - def documentsSplitCursor(self): - print("documentsSplitCursor::FIXME") + + def splitDialog(self): + """ + Opens a dialog to split selected items. + """ + if self.getCurrentItemView() == self.txtRedacText: + # Text editor + if not self.currentIndex.isValid(): + return + + sel = self.txtRedacText.textCursor().selectedText() + # selectedText uses \u2029 instead of \n, no idea why. + sel = sel.replace("\u2029", "\n") + splitDialog(self, [self.currentIndex], mark=sel) + + elif self.getCurrentItemView(): + # One of the view + self.getCurrentItemView().splitDialog() + + def splitCursor(self): + """ + Splits items at cursor position. If there is a selection, that selection + becomes the new item's title. + + Call context: Only works when editing a file. + """ + + if not self.currentIndex.isValid(): + return + + if self.getCurrentItemView() == self.txtRedacText: + c = self.txtRedacText.textCursor() + + title = c.selectedText() + # selection can be backward + pos = min(c.selectionStart(), c.selectionEnd()) + + item = self.currentIndex.internalPointer() + + item.splitAt(pos, len(title)) + def documentsMerge(self): print("documentsMerge::FIXME") diff --git a/manuskript/ui/editors/mainEditor.py b/manuskript/ui/editors/mainEditor.py index b4f920f7..3c51f1dc 100644 --- a/manuskript/ui/editors/mainEditor.py +++ b/manuskript/ui/editors/mainEditor.py @@ -255,10 +255,8 @@ class mainEditor(QWidget, Ui_mainEditor): def delete(self): self.currentEditor().delete() def moveUp(self): self.currentEditor().moveUp() def moveDown(self): self.currentEditor().moveDown() - def documentsSplitDialog(self): - print("documentsSplitDialog::FIXME") - def documentsSplitCursor(self): - print("documentsSplitCursor::FIXME") + def splitDialog(self): self.currentEditor().splitDialog() + def splitCursor(self): self.currentEditor().splitCursor() def documentsMerge(self): print("documentsMerge::FIXME") diff --git a/manuskript/ui/tools/splitDialog.py b/manuskript/ui/tools/splitDialog.py new file mode 100644 index 00000000..d77456d8 --- /dev/null +++ b/manuskript/ui/tools/splitDialog.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- +from PyQt5.QtWidgets import QInputDialog + + +class splitDialog(QInputDialog): + """ + Opens a dialog to split indexes. + """ + def __init__(self, parent, indexes, mark=None): + """ + @param parent: a QWidget, for the dialog. + @param indexes: a list of QModelIndex in the outlineModel + @param default: the default split mark + """ + QInputDialog.__init__(self, parent) + + description = self.tr(""" +

Split selected item(s) at the given mark.

+ +

If one of the selected item is a folder, it will be applied + recursively to all of it's children items.

+ +

The split mark can contain folling escret ape sequences: +

    +
  • \\n: line break
  • +
  • \\t: tab
  • +
+

+ +

Mark:

+ """) + + if not mark: + mark = "\\n---\\n" + mark = mark.replace("\n", "\\n") + mark = mark.replace("\t", "\\t") + + self.setLabelText(description) + self.setTextValue(mark) + self.setWindowTitle(self.tr("Split item(s)")) + + r = self.exec() + + mark = self.textValue() + + if r and mark: + + mark = mark.replace("\\n", "\n") + mark = mark.replace("\\t", "\t") + + for idx in indexes: + if idx.isValid(): + item = idx.internalPointer() + item.split(mark) diff --git a/manuskript/ui/views/outlineBasics.py b/manuskript/ui/views/outlineBasics.py index 36c6c4b3..f43f5b43 100644 --- a/manuskript/ui/views/outlineBasics.py +++ b/manuskript/ui/views/outlineBasics.py @@ -2,14 +2,16 @@ # --!-- coding: utf8 --!-- from PyQt5.QtCore import Qt, QSignalMapper, QSize from PyQt5.QtGui import QIcon, QCursor -from PyQt5.QtWidgets import QAbstractItemView, qApp, QMenu, QAction -from PyQt5.QtWidgets import QListWidget, QWidgetAction, QListWidgetItem, QLineEdit +from PyQt5.QtWidgets import QAbstractItemView, qApp, QMenu, QAction, \ + QListWidget, QWidgetAction, QListWidgetItem, \ + QLineEdit, QInputDialog, QMessageBox, QCheckBox from manuskript import settings from manuskript.enums import Outline from manuskript.functions import mainWindow from manuskript.functions import toInt, customIcons from manuskript.models.outlineModel import outlineItem +from manuskript.ui.tools.splitDialog import splitDialog class outlineBasics(QAbstractItemView): @@ -53,7 +55,7 @@ class outlineBasics(QAbstractItemView): title = mouseIndex.internalPointer().title() else: - title = self.tr("Root") + title = qApp.translate("outlineBasics", "Root") if len(title) > 25: title = title[:25] + "…" @@ -67,10 +69,10 @@ class outlineBasics(QAbstractItemView): # 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)) + actionTitle = qApp.translate("outlineBasics", "Open {} items in new tabs").format(len(sel)) self._indexesToOpen = sel else: - actionTitle = self.tr("Open {} in a new tab").format(title) + actionTitle = qApp.translate("outlineBasics", "Open {} in a new tab").format(title) self._indexesToOpen = [mouseIndex] self.actNewTab = QAction(QIcon.fromTheme("go-right"), actionTitle, menu) @@ -284,6 +286,27 @@ class outlineBasics(QAbstractItemView): self.delete() def delete(self): + """ + Shows a warning, and then deletes currently selected indexes. + """ + if not settings.dontShowDeleteWarning: + msg = QMessageBox(QMessageBox.Warning, + qApp.translate("outlineBasics", "About to remove"), + qApp.translate("outlineBasics", + "

You're about to delete {} item(s).

Are you sure?

" + ).format(len(self.getSelection())), + QMessageBox.Yes | QMessageBox.Cancel) + + chk = QCheckBox("&Don't show this warning in the future.") + msg.setCheckBox(chk) + ret = msg.exec() + + if ret == QMessageBox.Cancel: + return + + if chk.isChecked(): + settings.dontShowDeleteWarning = True + self.model().removeIndexes(self.getSelection()) def duplicate(self): @@ -295,7 +318,7 @@ class outlineBasics(QAbstractItemView): Move selected items up or down. """ - # we store selected indexes + # we store selected indexesret currentID = self.model().ID(self.currentIndex()) selIDs = [self.model().ID(i) for i in self.selectedIndexes()] @@ -316,7 +339,7 @@ class outlineBasics(QAbstractItemView): sm.clear() [sm.select(idx, sm.Select) for idx in selIdx] sm.setCurrentIndex(self.model().getIndexByID(currentID), sm.Select) - #self.setSelectionModel(sm) + #self.setSmsgBoxelectionModel(sm) # Unblock signals self.blockSignals(False) @@ -342,6 +365,19 @@ class outlineBasics(QAbstractItemView): def moveUp(self): self.move(-1) def moveDown(self): self.move(+1) + def splitDialog(self): + """ + Opens a dialog to split selected items. + + Call context: if at least one index is selected. Folder or text. + """ + + indexes = self.getSelection() + if len(indexes) == 0: + return + + splitDialog(self, indexes) + def setPOV(self, POV): for i in self.getSelection(): self.model().setData(i.sibling(i.row(), Outline.POV.value), str(POV)) diff --git a/manuskript/ui/views/textEditView.py b/manuskript/ui/views/textEditView.py index bb98996a..1c2513f5 100644 --- a/manuskript/ui/views/textEditView.py +++ b/manuskript/ui/views/textEditView.py @@ -93,7 +93,7 @@ class textEditView(QTextEdit): default_locale = QLocale.system().name() if default_locale is None: default_locale = enchant.list_dicts()[0][0] - + return default_locale def setModel(self, model): From bb57d3d0570c047e2ec4f0a995076cd7f57acabb Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Fri, 10 Nov 2017 17:21:02 +0100 Subject: [PATCH 4/5] Adds: merge --- manuskript/functions.py | 3 +++ manuskript/mainWindow.py | 5 ++-- manuskript/models/outlineModel.py | 17 ++++++++++++ manuskript/ui/editors/editorWidget.py | 23 +++++++++++++--- manuskript/ui/editors/mainEditor.py | 3 +-- manuskript/ui/tools/splitDialog.py | 23 +++++++++++++--- manuskript/ui/views/outlineBasics.py | 38 +++++++++++++++++++++++++-- manuskript/ui/views/textEditView.py | 15 +++++++++++ 8 files changed, 114 insertions(+), 13 deletions(-) diff --git a/manuskript/functions.py b/manuskript/functions.py index 04d57b7a..03e2a792 100644 --- a/manuskript/functions.py +++ b/manuskript/functions.py @@ -358,3 +358,6 @@ def customIcons(): ] return sorted(r) + +def statusMessage(message, duration=5000): + mainWindow().statusBar().showMessage(message, duration) diff --git a/manuskript/mainWindow.py b/manuskript/mainWindow.py index 5618f25b..61773d9a 100644 --- a/manuskript/mainWindow.py +++ b/manuskript/mainWindow.py @@ -214,7 +214,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): We get notified by qApp when focus changes, from old to new widget. """ - # Determine which item had focus last, to send the keyboard shortcuts + # Determine which view had focus last, to send the keyboard shortcuts # to the right place targets = [ @@ -476,7 +476,8 @@ class MainWindow(QMainWindow, Ui_MainWindow): if self._lastFocus and self._lastFocus == self.mainEditor: self.mainEditor.splitCursor() def documentsMerge(self): - print("documentsMerge::FIXME") + "Merges selected item(s)." + if self._lastFocus: self._lastFocus.merge() ############################################################################### # LOAD AND SAVE diff --git a/manuskript/models/outlineModel.py b/manuskript/models/outlineModel.py index c38260a0..2748ba28 100644 --- a/manuskript/models/outlineModel.py +++ b/manuskript/models/outlineModel.py @@ -928,6 +928,23 @@ class outlineItem(): # Inserting item using the model to signal views self._model.insertItem(item, self.row()+1, self.parent().index()) + def mergeWith(self, items, sep="\n---\n"): + """ + Merges item with several other items. Merge is basic, it merges only + the text. + + @param items: list of `outlineItem`s. + @param sep: a text added between each item's text. + """ + + # Merges the texts + text = [self.text()] + text.extend([i.text() for i in items]) + self.setData(Outline.text.value, sep.join(text)) + + # Removes other items + self._model.removeIndexes([i.index() for i in items]) + ############################################################################### # XML ############################################################################### diff --git a/manuskript/ui/editors/editorWidget.py b/manuskript/ui/editors/editorWidget.py index 10903072..623b62ad 100644 --- a/manuskript/ui/editors/editorWidget.py +++ b/manuskript/ui/editors/editorWidget.py @@ -331,6 +331,12 @@ class editorWidget(QWidget, Ui_editorWidget_ui): ############################################################################### def getCurrentItemView(self): + """ + Returns the current item view, between txtRedacText, outlineView and + corkView. If folder/text view, returns None. (Because handled + differently) + """ + if self.stack.currentIndex() == 0: return self.txtRedacText elif self.folderView == "outline": @@ -370,7 +376,7 @@ class editorWidget(QWidget, Ui_editorWidget_ui): splitDialog(self, [self.currentIndex], mark=sel) elif self.getCurrentItemView(): - # One of the view + # One of the views self.getCurrentItemView().splitDialog() def splitCursor(self): @@ -395,5 +401,16 @@ class editorWidget(QWidget, Ui_editorWidget_ui): item.splitAt(pos, len(title)) - def documentsMerge(self): - print("documentsMerge::FIXME") + def merge(self): + """ + Merges selected items together. + + Call context: Multiple selection, same parent. + """ + if self.getCurrentItemView() == self.txtRedacText: + # Text editor, nothing to merge + pass + + elif self.getCurrentItemView(): + # One of the views + self.getCurrentItemView().merge() diff --git a/manuskript/ui/editors/mainEditor.py b/manuskript/ui/editors/mainEditor.py index 3c51f1dc..606c4668 100644 --- a/manuskript/ui/editors/mainEditor.py +++ b/manuskript/ui/editors/mainEditor.py @@ -257,8 +257,7 @@ class mainEditor(QWidget, Ui_mainEditor): def moveDown(self): self.currentEditor().moveDown() def splitDialog(self): self.currentEditor().splitDialog() def splitCursor(self): self.currentEditor().splitCursor() - def documentsMerge(self): - print("documentsMerge::FIXME") + def merge(self): self.currentEditor().merge() ############################################################################### # UI diff --git a/manuskript/ui/tools/splitDialog.py b/manuskript/ui/tools/splitDialog.py index d77456d8..544b5339 100644 --- a/manuskript/ui/tools/splitDialog.py +++ b/manuskript/ui/tools/splitDialog.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # --!-- coding: utf8 --!-- from PyQt5.QtWidgets import QInputDialog +from manuskript.functions import mainWindow class splitDialog(QInputDialog): @@ -38,7 +39,16 @@ class splitDialog(QInputDialog): self.setLabelText(description) self.setTextValue(mark) - self.setWindowTitle(self.tr("Split item(s)")) + + if len(indexes) == 0: + return + if len(indexes) == 1: + idx = indexes[0] + self.setWindowTitle( + self.tr("Split '{}'").format(self.getItem(idx).title()) + ) + else: + self.setWindowTitle(self.tr("Split items")) r = self.exec() @@ -50,6 +60,11 @@ class splitDialog(QInputDialog): mark = mark.replace("\\t", "\t") for idx in indexes: - if idx.isValid(): - item = idx.internalPointer() - item.split(mark) + item = self.getItem(idx) + item.split(mark) + + def getItem(self, index): + if index.isValid(): + return index.internalPointer() + else: + return mainWindow().mdlOutline.rootItem diff --git a/manuskript/ui/views/outlineBasics.py b/manuskript/ui/views/outlineBasics.py index f43f5b43..ccd655cb 100644 --- a/manuskript/ui/views/outlineBasics.py +++ b/manuskript/ui/views/outlineBasics.py @@ -8,7 +8,7 @@ from PyQt5.QtWidgets import QAbstractItemView, qApp, QMenu, QAction, \ from manuskript import settings from manuskript.enums import Outline -from manuskript.functions import mainWindow +from manuskript.functions import mainWindow, statusMessage from manuskript.functions import toInt, customIcons from manuskript.models.outlineModel import outlineItem from manuskript.ui.tools.splitDialog import splitDialog @@ -374,10 +374,44 @@ class outlineBasics(QAbstractItemView): indexes = self.getSelection() if len(indexes) == 0: - return + # No selection, we use parent + indexes = [self.rootIndex()] splitDialog(self, indexes) + def merge(self): + """ + Merges selected items together. + + Call context: Multiple selection, same parent. + """ + + # Get selection + indexes = self.getSelection() + # Get items + items = [i.internalPointer() for i in indexes if i.isValid()] + # Remove folders + items = [i for i in items if not i.isFolder()] + + # Check that we have at least 2 items + if len(items) < 2: + statusMessage(qApp.translate("outlineBasics", + "Select at least two items. Folders are ignored.")) + return + + # Check that all share the same parent + p = items[0].parent() + for i in items: + if i.parent() != p: + statusMessage(qApp.translate("outlineBasics", + "All items must be on the same level (share the same parent).")) + return + + # Sort items by row + items = sorted(items, key=lambda i: i.row()) + + items[0].mergeWith(items[1:]) + def setPOV(self, POV): for i in self.getSelection(): self.model().setData(i.sibling(i.row(), Outline.POV.value), str(POV)) diff --git a/manuskript/ui/views/textEditView.py b/manuskript/ui/views/textEditView.py index 1c2513f5..a9c7907b 100644 --- a/manuskript/ui/views/textEditView.py +++ b/manuskript/ui/views/textEditView.py @@ -141,6 +141,21 @@ class textEditView(QTextEdit): self.setPlainText("") self.setEnabled(False) + def currentIndex(self): + """ + Getter function used to normalized views acces with QAbstractItemViews. + """ + if self._index: + return self._index + else: + return QModelIndex() + + def getSelection(self): + """ + Getter function used to normalized views acces with QAbstractItemViews. + """ + return [self.currentIndex()] + def setCurrentModelIndexes(self, indexes): self._index = None self._indexes = [] From 4445b55559353981e1cf1fa98178f218b243ecc5 Mon Sep 17 00:00:00 2001 From: Olivier Keshavjee Date: Fri, 10 Nov 2017 17:33:48 +0100 Subject: [PATCH 5/5] Allows pasted items to keep ID if not already in model. --- manuskript/models/outlineModel.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/manuskript/models/outlineModel.py b/manuskript/models/outlineModel.py index 2748ba28..140def2d 100644 --- a/manuskript/models/outlineModel.py +++ b/manuskript/models/outlineModel.py @@ -398,11 +398,23 @@ class outlineModel(QAbstractItemModel): if not items: return False - r = self.insertItems(items, beginRow, parent) - + # 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. if action == Qt.CopyAction: + IDs = self.rootItem.listAllIDs() + for item in items: - item.getUniqueID(recursive=True) + if item.ID() in IDs: + # Recursively remove ID. So will get a new one when inserted. + def stripID(item): + item.setData(Outline.ID.value, None) + for c in item.children(): + stripID(c) + + stripID(item) + + r = self.insertItems(items, beginRow, parent) return r @@ -928,7 +940,7 @@ class outlineItem(): # Inserting item using the model to signal views self._model.insertItem(item, self.row()+1, self.parent().index()) - def mergeWith(self, items, sep="\n---\n"): + def mergeWith(self, items, sep="\n\n"): """ Merges item with several other items. Merge is basic, it merges only the text.