novelWriter 2.2b1__py3-none-any.whl → 2.2rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/METADATA +3 -3
- {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/RECORD +60 -48
- novelwriter/__init__.py +3 -3
- novelwriter/assets/i18n/project_en_GB.json +1 -0
- novelwriter/assets/icons/novelwriter.ico +0 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +8 -1
- novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
- novelwriter/assets/icons/typicons_light/icons.conf +8 -1
- novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
- novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/text/release_notes.htm +4 -4
- novelwriter/common.py +22 -1
- novelwriter/config.py +12 -27
- novelwriter/constants.py +20 -3
- novelwriter/core/buildsettings.py +1 -1
- novelwriter/core/coretools.py +6 -1
- novelwriter/core/index.py +100 -34
- novelwriter/core/options.py +3 -0
- novelwriter/core/project.py +2 -2
- novelwriter/core/projectdata.py +1 -1
- novelwriter/core/tohtml.py +9 -3
- novelwriter/core/tokenizer.py +27 -20
- novelwriter/core/tomd.py +4 -0
- novelwriter/core/toodt.py +11 -4
- novelwriter/dialogs/preferences.py +80 -82
- novelwriter/dialogs/updates.py +25 -14
- novelwriter/enum.py +14 -4
- novelwriter/gui/doceditor.py +282 -177
- novelwriter/gui/dochighlight.py +7 -9
- novelwriter/gui/docviewer.py +142 -319
- novelwriter/gui/docviewerpanel.py +457 -0
- novelwriter/gui/editordocument.py +1 -1
- novelwriter/gui/mainmenu.py +16 -7
- novelwriter/gui/outline.py +10 -6
- novelwriter/gui/projtree.py +461 -376
- novelwriter/gui/sidebar.py +3 -3
- novelwriter/gui/statusbar.py +1 -1
- novelwriter/gui/theme.py +21 -2
- novelwriter/guimain.py +86 -32
- novelwriter/shared.py +23 -1
- novelwriter/tools/dictionaries.py +268 -0
- novelwriter/tools/manusbuild.py +17 -6
- novelwriter/tools/manuscript.py +1 -1
- novelwriter/tools/writingstats.py +1 -1
- novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
- {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/WHEEL +0 -0
- {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.2b1.dist-info → novelWriter-2.2rc1.dist-info}/top_level.txt +0 -0
novelwriter/gui/projtree.py
CHANGED
@@ -31,7 +31,7 @@ from enum import Enum
|
|
31
31
|
from time import time
|
32
32
|
from typing import TYPE_CHECKING
|
33
33
|
|
34
|
-
from PyQt5.QtGui import QDragMoveEvent, QDropEvent, QMouseEvent, QPalette
|
34
|
+
from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QMouseEvent, QPalette
|
35
35
|
from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot
|
36
36
|
from PyQt5.QtWidgets import (
|
37
37
|
QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel,
|
@@ -98,42 +98,37 @@ class GuiProjectView(QWidget):
|
|
98
98
|
# Keyboard Shortcuts
|
99
99
|
self.keyMoveUp = QShortcut(self.projTree)
|
100
100
|
self.keyMoveUp.setKey("Ctrl+Up")
|
101
|
-
self.keyMoveUp.setContext(Qt.WidgetShortcut)
|
101
|
+
self.keyMoveUp.setContext(Qt.ShortcutContext.WidgetShortcut)
|
102
102
|
self.keyMoveUp.activated.connect(lambda: self.projTree.moveTreeItem(-1))
|
103
103
|
|
104
104
|
self.keyMoveDn = QShortcut(self.projTree)
|
105
105
|
self.keyMoveDn.setKey("Ctrl+Down")
|
106
|
-
self.keyMoveDn.setContext(Qt.WidgetShortcut)
|
106
|
+
self.keyMoveDn.setContext(Qt.ShortcutContext.WidgetShortcut)
|
107
107
|
self.keyMoveDn.activated.connect(lambda: self.projTree.moveTreeItem(1))
|
108
108
|
|
109
109
|
self.keyGoPrev = QShortcut(self.projTree)
|
110
110
|
self.keyGoPrev.setKey("Alt+Up")
|
111
|
-
self.keyGoPrev.setContext(Qt.WidgetShortcut)
|
111
|
+
self.keyGoPrev.setContext(Qt.ShortcutContext.WidgetShortcut)
|
112
112
|
self.keyGoPrev.activated.connect(lambda: self.projTree.moveToNextItem(-1))
|
113
113
|
|
114
114
|
self.keyGoNext = QShortcut(self.projTree)
|
115
115
|
self.keyGoNext.setKey("Alt+Down")
|
116
|
-
self.keyGoNext.setContext(Qt.WidgetShortcut)
|
116
|
+
self.keyGoNext.setContext(Qt.ShortcutContext.WidgetShortcut)
|
117
117
|
self.keyGoNext.activated.connect(lambda: self.projTree.moveToNextItem(1))
|
118
118
|
|
119
119
|
self.keyGoUp = QShortcut(self.projTree)
|
120
120
|
self.keyGoUp.setKey("Alt+Left")
|
121
|
-
self.keyGoUp.setContext(Qt.WidgetShortcut)
|
121
|
+
self.keyGoUp.setContext(Qt.ShortcutContext.WidgetShortcut)
|
122
122
|
self.keyGoUp.activated.connect(lambda: self.projTree.moveToLevel(-1))
|
123
123
|
|
124
124
|
self.keyGoDown = QShortcut(self.projTree)
|
125
125
|
self.keyGoDown.setKey("Alt+Right")
|
126
|
-
self.keyGoDown.setContext(Qt.WidgetShortcut)
|
126
|
+
self.keyGoDown.setContext(Qt.ShortcutContext.WidgetShortcut)
|
127
127
|
self.keyGoDown.activated.connect(lambda: self.projTree.moveToLevel(1))
|
128
128
|
|
129
|
-
self.keyUndoMv = QShortcut(self.projTree)
|
130
|
-
self.keyUndoMv.setKey("Ctrl+Shift+Z")
|
131
|
-
self.keyUndoMv.setContext(Qt.WidgetShortcut)
|
132
|
-
self.keyUndoMv.activated.connect(lambda: self.projTree.undoLastMove())
|
133
|
-
|
134
129
|
self.keyContext = QShortcut(self.projTree)
|
135
130
|
self.keyContext.setKey("Ctrl+.")
|
136
|
-
self.keyContext.setContext(Qt.WidgetShortcut)
|
131
|
+
self.keyContext.setContext(Qt.ShortcutContext.WidgetShortcut)
|
137
132
|
self.keyContext.activated.connect(lambda: self.projTree.openContextOnSelected())
|
138
133
|
|
139
134
|
# Signals
|
@@ -142,10 +137,7 @@ class GuiProjectView(QWidget):
|
|
142
137
|
# Function Mappings
|
143
138
|
self.emptyTrash = self.projTree.emptyTrash
|
144
139
|
self.requestDeleteItem = self.projTree.requestDeleteItem
|
145
|
-
self.setTreeItemValues = self.projTree.setTreeItemValues
|
146
|
-
self.propagateCount = self.projTree.propagateCount
|
147
140
|
self.getSelectedHandle = self.projTree.getSelectedHandle
|
148
|
-
self.setSelectedHandle = self.projTree.setSelectedHandle
|
149
141
|
self.changedSince = self.projTree.changedSince
|
150
142
|
self.createNewNote = self.projTree.createNewNote
|
151
143
|
|
@@ -198,17 +190,32 @@ class GuiProjectView(QWidget):
|
|
198
190
|
"""Check if the project tree has focus."""
|
199
191
|
return self.projTree.hasFocus()
|
200
192
|
|
201
|
-
|
193
|
+
##
|
194
|
+
# Public Slots
|
195
|
+
##
|
196
|
+
|
197
|
+
@pyqtSlot(str, str)
|
198
|
+
def renameTreeItem(self, tHandle: str | None = None, name: str = "") -> None:
|
202
199
|
"""External request to rename an item or the currently selected
|
203
200
|
item. This is triggered by the global menu or keyboard shortcut.
|
204
201
|
"""
|
205
202
|
if tHandle is None:
|
206
203
|
tHandle = self.projTree.getSelectedHandle()
|
207
|
-
|
204
|
+
if tHandle:
|
205
|
+
self.projTree.renameTreeItem(tHandle, name=name)
|
206
|
+
return
|
208
207
|
|
209
|
-
|
210
|
-
|
211
|
-
|
208
|
+
@pyqtSlot(str, bool)
|
209
|
+
def setSelectedHandle(self, tHandle: str, doScroll: bool = False) -> None:
|
210
|
+
"""Select an item and optionally scroll it into view."""
|
211
|
+
self.projTree.setSelectedHandle(tHandle, doScroll=doScroll)
|
212
|
+
return
|
213
|
+
|
214
|
+
@pyqtSlot(str)
|
215
|
+
def updateItemValues(self, tHandle: str) -> None:
|
216
|
+
"""Update tree item"""
|
217
|
+
self.projTree.setTreeItemValues(tHandle)
|
218
|
+
return
|
212
219
|
|
213
220
|
@pyqtSlot(str, int, int, int)
|
214
221
|
def updateCounts(self, tHandle: str, cCount: int, wCount: int, pCount: int) -> None:
|
@@ -256,7 +263,7 @@ class GuiProjectToolBar(QWidget):
|
|
256
263
|
self.tbQuick.setShortcut("Ctrl+L")
|
257
264
|
self.tbQuick.setIconSize(QSize(iPx, iPx))
|
258
265
|
self.tbQuick.setMenu(self.mQuick)
|
259
|
-
self.tbQuick.setPopupMode(QToolButton.InstantPopup)
|
266
|
+
self.tbQuick.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
260
267
|
|
261
268
|
# Move Buttons
|
262
269
|
self.tbMoveU = QToolButton(self)
|
@@ -305,7 +312,7 @@ class GuiProjectToolBar(QWidget):
|
|
305
312
|
self.tbAdd.setShortcut("Ctrl+N")
|
306
313
|
self.tbAdd.setIconSize(QSize(iPx, iPx))
|
307
314
|
self.tbAdd.setMenu(self.mAdd)
|
308
|
-
self.tbAdd.setPopupMode(QToolButton.InstantPopup)
|
315
|
+
self.tbAdd.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
309
316
|
|
310
317
|
# More Options Menu
|
311
318
|
self.mMore = QMenu(self)
|
@@ -316,9 +323,6 @@ class GuiProjectToolBar(QWidget):
|
|
316
323
|
self.aCollapse = self.mMore.addAction(self.tr("Collapse All"))
|
317
324
|
self.aCollapse.triggered.connect(lambda: self.projTree.setExpandedFromHandle(None, False))
|
318
325
|
|
319
|
-
self.aMoreUndo = self.mMore.addAction(self.tr("Undo Move"))
|
320
|
-
self.aMoreUndo.triggered.connect(lambda: self.projTree.undoLastMove())
|
321
|
-
|
322
326
|
self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
|
323
327
|
self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash())
|
324
328
|
|
@@ -326,7 +330,7 @@ class GuiProjectToolBar(QWidget):
|
|
326
330
|
self.tbMore.setToolTip(self.tr("More Options"))
|
327
331
|
self.tbMore.setIconSize(QSize(iPx, iPx))
|
328
332
|
self.tbMore.setMenu(self.mMore)
|
329
|
-
self.tbMore.setPopupMode(QToolButton.InstantPopup)
|
333
|
+
self.tbMore.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
330
334
|
|
331
335
|
# Assemble
|
332
336
|
self.outerBox = QHBoxLayout()
|
@@ -353,7 +357,7 @@ class GuiProjectToolBar(QWidget):
|
|
353
357
|
def updateTheme(self) -> None:
|
354
358
|
"""Update theme elements."""
|
355
359
|
qPalette = self.palette()
|
356
|
-
qPalette.setBrush(QPalette.Window, qPalette.base())
|
360
|
+
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
|
357
361
|
self.setPalette(qPalette)
|
358
362
|
|
359
363
|
fadeCol = qPalette.text().color()
|
@@ -431,7 +435,7 @@ class GuiProjectToolBar(QWidget):
|
|
431
435
|
return
|
432
436
|
|
433
437
|
##
|
434
|
-
# Slots
|
438
|
+
# Private Slots
|
435
439
|
##
|
436
440
|
|
437
441
|
@pyqtSlot(str)
|
@@ -471,14 +475,14 @@ class GuiProjectTree(QTreeWidget):
|
|
471
475
|
|
472
476
|
# Internal Variables
|
473
477
|
self._treeMap = {}
|
474
|
-
self._lastMove = {}
|
475
478
|
self._timeChanged = 0.0
|
479
|
+
self._popAlert = None
|
476
480
|
|
477
481
|
# Build GUI
|
478
482
|
# =========
|
479
483
|
|
480
484
|
# Context Menu
|
481
|
-
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
485
|
+
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
482
486
|
self.customContextMenuRequested.connect(self._openContextMenu)
|
483
487
|
|
484
488
|
# Tree Settings
|
@@ -486,7 +490,7 @@ class GuiProjectTree(QTreeWidget):
|
|
486
490
|
cMg = CONFIG.pxInt(6)
|
487
491
|
|
488
492
|
self.setIconSize(QSize(iPx, iPx))
|
489
|
-
self.setFrameStyle(QFrame.NoFrame)
|
493
|
+
self.setFrameStyle(QFrame.Shape.NoFrame)
|
490
494
|
self.setUniformRowHeights(True)
|
491
495
|
self.setAllColumnsShowFocus(True)
|
492
496
|
self.setExpandsOnDoubleClick(False)
|
@@ -499,19 +503,18 @@ class GuiProjectTree(QTreeWidget):
|
|
499
503
|
treeHeader = self.header()
|
500
504
|
treeHeader.setStretchLastSection(False)
|
501
505
|
treeHeader.setMinimumSectionSize(iPx + cMg)
|
502
|
-
treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.Stretch)
|
503
|
-
treeHeader.setSectionResizeMode(self.C_COUNT, QHeaderView.ResizeToContents)
|
504
|
-
treeHeader.setSectionResizeMode(self.C_ACTIVE, QHeaderView.Fixed)
|
505
|
-
treeHeader.setSectionResizeMode(self.C_STATUS, QHeaderView.Fixed)
|
506
|
+
treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.Stretch)
|
507
|
+
treeHeader.setSectionResizeMode(self.C_COUNT, QHeaderView.ResizeMode.ResizeToContents)
|
508
|
+
treeHeader.setSectionResizeMode(self.C_ACTIVE, QHeaderView.ResizeMode.Fixed)
|
509
|
+
treeHeader.setSectionResizeMode(self.C_STATUS, QHeaderView.ResizeMode.Fixed)
|
506
510
|
treeHeader.resizeSection(self.C_ACTIVE, iPx + cMg)
|
507
511
|
treeHeader.resizeSection(self.C_STATUS, iPx + cMg)
|
508
512
|
|
509
513
|
# Allow Move by Drag & Drop
|
510
514
|
self.setDragEnabled(True)
|
511
|
-
self.setDragDropMode(QAbstractItemView.InternalMove)
|
512
|
-
self.setDropIndicatorShown(True)
|
515
|
+
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
513
516
|
|
514
|
-
# Disable built-in
|
517
|
+
# Disable built-in auto scroll as it isn't working in some Qt
|
515
518
|
# releases (see #1561) and instead use our own implementation
|
516
519
|
self.setAutoScroll(False)
|
517
520
|
|
@@ -519,21 +522,21 @@ class GuiProjectTree(QTreeWidget):
|
|
519
522
|
# Due to a bug, this stops working somewhere between Qt 5.15.3
|
520
523
|
# and 5.15.8, so this is also blocked in dropEvent (see #1569)
|
521
524
|
trRoot = self.invisibleRootItem()
|
522
|
-
trRoot.setFlags(trRoot.flags() ^ Qt.ItemIsDropEnabled)
|
525
|
+
trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled)
|
523
526
|
|
524
527
|
# Cached values
|
525
528
|
self._lblActive = self.tr("Active")
|
526
529
|
self._lblInactive = self.tr("Inactive")
|
527
530
|
|
528
531
|
# Set selection options
|
529
|
-
self.setSelectionMode(QAbstractItemView.
|
530
|
-
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
532
|
+
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
533
|
+
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
531
534
|
|
532
535
|
# Connect signals
|
533
536
|
self.itemDoubleClicked.connect(self._treeDoubleClick)
|
534
537
|
self.itemSelectionChanged.connect(self._treeSelectionChange)
|
535
538
|
|
536
|
-
#
|
539
|
+
# Auto Scroll
|
537
540
|
self._scrollMargin = SHARED.theme.baseIconSize
|
538
541
|
self._scrollDirection = 0
|
539
542
|
self._scrollTimer = QTimer()
|
@@ -551,13 +554,13 @@ class GuiProjectTree(QTreeWidget):
|
|
551
554
|
"""Set or update tree widget settings."""
|
552
555
|
# Scroll bars
|
553
556
|
if CONFIG.hideVScroll:
|
554
|
-
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
557
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
555
558
|
else:
|
556
|
-
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
559
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
557
560
|
if CONFIG.hideHScroll:
|
558
|
-
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
561
|
+
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
559
562
|
else:
|
560
|
-
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
563
|
+
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
561
564
|
return
|
562
565
|
|
563
566
|
##
|
@@ -568,7 +571,6 @@ class GuiProjectTree(QTreeWidget):
|
|
568
571
|
"""Clear the GUI content and the related map."""
|
569
572
|
self.clear()
|
570
573
|
self._treeMap = {}
|
571
|
-
self._lastMove = {}
|
572
574
|
self._timeChanged = 0.0
|
573
575
|
return
|
574
576
|
|
@@ -739,7 +741,6 @@ class GuiProjectTree(QTreeWidget):
|
|
739
741
|
|
740
742
|
cItem = pItem.takeChild(tIndex)
|
741
743
|
pItem.insertChild(nIndex, cItem)
|
742
|
-
self._recordLastMove(cItem, pItem, tIndex)
|
743
744
|
|
744
745
|
self._alertTreeChange(tHandle, flush=True)
|
745
746
|
self.setCurrentItem(tItem)
|
@@ -768,19 +769,16 @@ class GuiProjectTree(QTreeWidget):
|
|
768
769
|
self.setCurrentItem(tItem.child(0))
|
769
770
|
return
|
770
771
|
|
771
|
-
def renameTreeItem(self, tHandle: str) ->
|
772
|
+
def renameTreeItem(self, tHandle: str, name: str = "") -> None:
|
772
773
|
"""Open a dialog to edit the label of an item."""
|
773
774
|
tItem = SHARED.project.tree[tHandle]
|
774
|
-
if tItem
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
self._alertTreeChange(tHandle, flush=False)
|
782
|
-
|
783
|
-
return True
|
775
|
+
if tItem:
|
776
|
+
newLabel, dlgOk = GuiEditLabel.getLabel(self, text=name or tItem.itemName)
|
777
|
+
if dlgOk:
|
778
|
+
tItem.setName(newLabel)
|
779
|
+
self.setTreeItemValues(tHandle)
|
780
|
+
self._alertTreeChange(tHandle, flush=False)
|
781
|
+
return
|
784
782
|
|
785
783
|
def saveTreeOrder(self) -> None:
|
786
784
|
"""Build a list of the items in the project tree and send them
|
@@ -843,6 +841,7 @@ class GuiProjectTree(QTreeWidget):
|
|
843
841
|
|
844
842
|
return status
|
845
843
|
|
844
|
+
@pyqtSlot()
|
846
845
|
def emptyTrash(self) -> bool:
|
847
846
|
"""Permanently delete all documents in the Trash folder. This
|
848
847
|
function only asks for confirmation once, and calls the regular
|
@@ -921,15 +920,13 @@ class GuiProjectTree(QTreeWidget):
|
|
921
920
|
logger.info("Action cancelled by user")
|
922
921
|
return False
|
923
922
|
|
924
|
-
wCount = self._getItemWordCount(tHandle)
|
925
923
|
self.propagateCount(tHandle, 0)
|
926
924
|
|
927
925
|
tIndex = trItemP.indexOfChild(trItemS)
|
928
926
|
trItemC = trItemP.takeChild(tIndex)
|
929
927
|
trItemT.addChild(trItemC)
|
930
928
|
|
931
|
-
self._postItemMove(tHandle
|
932
|
-
self._recordLastMove(trItemS, trItemP, tIndex)
|
929
|
+
self._postItemMove(tHandle)
|
933
930
|
self._alertTreeChange(tHandle, flush=flush)
|
934
931
|
|
935
932
|
logger.debug("Moved item '%s' to Trash", tHandle)
|
@@ -1090,46 +1087,6 @@ class GuiProjectTree(QTreeWidget):
|
|
1090
1087
|
logger.info("%d item(s) added to the project tree", count)
|
1091
1088
|
return
|
1092
1089
|
|
1093
|
-
def undoLastMove(self) -> bool:
|
1094
|
-
"""Attempt to undo the last action."""
|
1095
|
-
srcItem = self._lastMove.get("item", None)
|
1096
|
-
dstItem = self._lastMove.get("parent", None)
|
1097
|
-
dstIndex = self._lastMove.get("index", None)
|
1098
|
-
|
1099
|
-
srcOK = isinstance(srcItem, QTreeWidgetItem)
|
1100
|
-
dstOk = isinstance(dstItem, QTreeWidgetItem)
|
1101
|
-
if not srcOK or not dstOk or dstIndex is None:
|
1102
|
-
logger.debug("No tree move to undo")
|
1103
|
-
return False
|
1104
|
-
|
1105
|
-
if srcItem not in self._treeMap.values():
|
1106
|
-
logger.warning("Source item no longer exists")
|
1107
|
-
return False
|
1108
|
-
|
1109
|
-
if dstItem not in self._treeMap.values():
|
1110
|
-
logger.warning("Previous parent item no longer exists")
|
1111
|
-
return False
|
1112
|
-
|
1113
|
-
dstIndex = min(max(0, dstIndex), dstItem.childCount())
|
1114
|
-
sHandle = srcItem.data(self.C_DATA, self.D_HANDLE)
|
1115
|
-
dHandle = dstItem.data(self.C_DATA, self.D_HANDLE)
|
1116
|
-
logger.debug("Moving item '%s' back to '%s', index %d", sHandle, dHandle, dstIndex)
|
1117
|
-
|
1118
|
-
wCount = self._getItemWordCount(sHandle)
|
1119
|
-
self.propagateCount(sHandle, 0)
|
1120
|
-
parItem = srcItem.parent()
|
1121
|
-
srcIndex = parItem.indexOfChild(srcItem)
|
1122
|
-
movItem = parItem.takeChild(srcIndex)
|
1123
|
-
dstItem.insertChild(dstIndex, movItem)
|
1124
|
-
|
1125
|
-
self._postItemMove(sHandle, wCount)
|
1126
|
-
self._alertTreeChange(sHandle, flush=True)
|
1127
|
-
|
1128
|
-
self.setCurrentItem(movItem)
|
1129
|
-
self._lastMove = {}
|
1130
|
-
|
1131
|
-
return True
|
1132
|
-
|
1133
1090
|
def getSelectedHandle(self) -> str | None:
|
1134
1091
|
"""Get the currently selected handle. If multiple items are
|
1135
1092
|
selected, return the first.
|
@@ -1150,7 +1107,7 @@ class GuiProjectTree(QTreeWidget):
|
|
1150
1107
|
|
1151
1108
|
selIndex = self.selectedIndexes()
|
1152
1109
|
if selIndex and doScroll:
|
1153
|
-
self.scrollTo(selIndex[0], QAbstractItemView.PositionAtCenter)
|
1110
|
+
self.scrollTo(selIndex[0], QAbstractItemView.ScrollHint.PositionAtCenter)
|
1154
1111
|
|
1155
1112
|
return True
|
1156
1113
|
|
@@ -1185,6 +1142,15 @@ class GuiProjectTree(QTreeWidget):
|
|
1185
1142
|
tHandle = self.getSelectedHandle()
|
1186
1143
|
if tHandle is not None:
|
1187
1144
|
self.projView.selectedItemChanged.emit(tHandle)
|
1145
|
+
|
1146
|
+
# When selecting multiple items, don't allow including root
|
1147
|
+
# items in the selection and instead deselect them
|
1148
|
+
items = self.selectedItems()
|
1149
|
+
if items and len(items) > 1:
|
1150
|
+
for item in items:
|
1151
|
+
if item.parent() is None:
|
1152
|
+
item.setSelected(False)
|
1153
|
+
|
1188
1154
|
return
|
1189
1155
|
|
1190
1156
|
@pyqtSlot("QTreeWidgetItem*", int)
|
@@ -1215,160 +1181,29 @@ class GuiProjectTree(QTreeWidget):
|
|
1215
1181
|
tItem = None
|
1216
1182
|
tHandle = None
|
1217
1183
|
hasChild = False
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1184
|
+
sItem = self.itemAt(clickPos)
|
1185
|
+
sItems = self.selectedItems()
|
1186
|
+
if isinstance(sItem, QTreeWidgetItem):
|
1187
|
+
tHandle = sItem.data(self.C_DATA, self.D_HANDLE)
|
1221
1188
|
tItem = SHARED.project.tree[tHandle]
|
1222
|
-
hasChild =
|
1189
|
+
hasChild = sItem.childCount() > 0
|
1223
1190
|
|
1224
1191
|
if tItem is None or tHandle is None:
|
1225
1192
|
logger.debug("No item found")
|
1226
1193
|
return False
|
1227
1194
|
|
1228
|
-
ctxMenu =
|
1229
|
-
|
1230
|
-
# Trash Folder
|
1231
|
-
# ============
|
1232
|
-
|
1195
|
+
ctxMenu = _TreeContextMenu(self, tItem)
|
1233
1196
|
trashHandle = SHARED.project.tree.trashRoot
|
1234
|
-
if
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1238
|
-
ctxMenu.
|
1239
|
-
return True
|
1240
|
-
|
1241
|
-
# Document Actions
|
1242
|
-
# ================
|
1243
|
-
|
1244
|
-
isRoot = tItem.isRootType()
|
1245
|
-
isFolder = tItem.isFolderType()
|
1246
|
-
isFile = tItem.isFileType()
|
1247
|
-
|
1248
|
-
if isFile:
|
1249
|
-
aOpenDoc = ctxMenu.addAction(self.tr("Open Document"))
|
1250
|
-
aOpenDoc.triggered.connect(
|
1251
|
-
lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
|
1252
|
-
)
|
1253
|
-
aViewDoc = ctxMenu.addAction(self.tr("View Document"))
|
1254
|
-
aViewDoc.triggered.connect(
|
1255
|
-
lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
|
1256
|
-
)
|
1257
|
-
ctxMenu.addSeparator()
|
1258
|
-
|
1259
|
-
# Edit Item Settings
|
1260
|
-
# ==================
|
1261
|
-
|
1262
|
-
aLabel = ctxMenu.addAction(self.tr("Rename"))
|
1263
|
-
aLabel.triggered.connect(lambda: self.renameTreeItem(tHandle))
|
1264
|
-
|
1265
|
-
if isFile:
|
1266
|
-
aActive = ctxMenu.addAction(self.tr("Toggle Active"))
|
1267
|
-
aActive.triggered.connect(lambda: self._toggleItemActive(tHandle))
|
1268
|
-
|
1269
|
-
checkMark = f" ({nwUnicode.U_CHECK})"
|
1270
|
-
if tItem.isNovelLike():
|
1271
|
-
mStatus = ctxMenu.addMenu(self.tr("Set Status to ..."))
|
1272
|
-
for n, (key, entry) in enumerate(SHARED.project.data.itemStatus.items()):
|
1273
|
-
entryName = entry["name"] + (checkMark if tItem.itemStatus == key else "")
|
1274
|
-
aStatus = mStatus.addAction(entry["icon"], entryName)
|
1275
|
-
aStatus.triggered.connect(
|
1276
|
-
lambda n, key=key: self._changeItemStatus(tHandle, key)
|
1277
|
-
)
|
1278
|
-
mStatus.addSeparator()
|
1279
|
-
aManage1 = mStatus.addAction("Manage Labels ...")
|
1280
|
-
aManage1.triggered.connect(
|
1281
|
-
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_STATUS)
|
1282
|
-
)
|
1283
|
-
else:
|
1284
|
-
mImport = ctxMenu.addMenu(self.tr("Set Importance to ..."))
|
1285
|
-
for n, (key, entry) in enumerate(SHARED.project.data.itemImport.items()):
|
1286
|
-
entryName = entry["name"] + (checkMark if tItem.itemImport == key else "")
|
1287
|
-
aImport = mImport.addAction(entry["icon"], entryName)
|
1288
|
-
aImport.triggered.connect(
|
1289
|
-
lambda n, key=key: self._changeItemImport(tHandle, key)
|
1290
|
-
)
|
1291
|
-
mImport.addSeparator()
|
1292
|
-
aManage2 = mImport.addAction("Manage Labels ...")
|
1293
|
-
aManage2.triggered.connect(
|
1294
|
-
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_IMPORT)
|
1295
|
-
)
|
1296
|
-
|
1297
|
-
# Transform Item
|
1298
|
-
# ==============
|
1299
|
-
|
1300
|
-
if not isRoot:
|
1301
|
-
mTrans = ctxMenu.addMenu(self.tr("Transform"))
|
1302
|
-
|
1303
|
-
trDoc = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
|
1304
|
-
trNote = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
|
1305
|
-
|
1306
|
-
isDocFile = isFile and tItem.isDocumentLayout()
|
1307
|
-
isNoteFile = isFile and tItem.isNoteLayout()
|
1308
|
-
|
1309
|
-
if isNoteFile and tItem.documentAllowed():
|
1310
|
-
aConvert1 = mTrans.addAction(self.tr("Convert to {0}").format(trDoc))
|
1311
|
-
aConvert1.triggered.connect(
|
1312
|
-
lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT)
|
1313
|
-
)
|
1314
|
-
|
1315
|
-
if isDocFile:
|
1316
|
-
aConvert2 = mTrans.addAction(self.tr("Convert to {0}").format(trNote))
|
1317
|
-
aConvert2.triggered.connect(
|
1318
|
-
lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE)
|
1319
|
-
)
|
1320
|
-
|
1321
|
-
if isFolder and tItem.documentAllowed():
|
1322
|
-
aConvert3 = mTrans.addAction(self.tr("Convert to {0}").format(trDoc))
|
1323
|
-
aConvert3.triggered.connect(
|
1324
|
-
lambda: self._covertFolderToFile(tHandle, nwItemLayout.DOCUMENT)
|
1325
|
-
)
|
1326
|
-
|
1327
|
-
if isFolder:
|
1328
|
-
aConvert4 = mTrans.addAction(self.tr("Convert to {0}").format(trNote))
|
1329
|
-
aConvert4.triggered.connect(
|
1330
|
-
lambda: self._covertFolderToFile(tHandle, nwItemLayout.NOTE)
|
1331
|
-
)
|
1332
|
-
|
1333
|
-
if hasChild and isFile:
|
1334
|
-
aMerge1 = mTrans.addAction(self.tr("Merge Child Items into Self"))
|
1335
|
-
aMerge1.triggered.connect(lambda: self._mergeDocuments(tHandle, False))
|
1336
|
-
aMerge2 = mTrans.addAction(self.tr("Merge Child Items into New"))
|
1337
|
-
aMerge2.triggered.connect(lambda: self._mergeDocuments(tHandle, True))
|
1338
|
-
|
1339
|
-
if hasChild and isFolder:
|
1340
|
-
aMerge3 = mTrans.addAction(self.tr("Merge Documents in Folder"))
|
1341
|
-
aMerge3.triggered.connect(lambda: self._mergeDocuments(tHandle, True))
|
1342
|
-
|
1343
|
-
if isFile:
|
1344
|
-
aSplit1 = mTrans.addAction(self.tr("Split Document by Headers"))
|
1345
|
-
aSplit1.triggered.connect(lambda: self._splitDocument(tHandle))
|
1346
|
-
|
1347
|
-
# Expand/Collapse/Delete/Duplicate
|
1348
|
-
# ================================
|
1349
|
-
|
1350
|
-
ctxMenu.addSeparator()
|
1351
|
-
|
1352
|
-
if hasChild:
|
1353
|
-
aExpand = ctxMenu.addAction(self.tr("Expand All"))
|
1354
|
-
aExpand.triggered.connect(lambda: self.setExpandedFromHandle(tHandle, True))
|
1355
|
-
aCollapse = ctxMenu.addAction(self.tr("Collapse All"))
|
1356
|
-
aCollapse.triggered.connect(lambda: self.setExpandedFromHandle(tHandle, False))
|
1357
|
-
aDuplicate = ctxMenu.addAction(self.tr("Duplicate from Here"))
|
1358
|
-
aDuplicate.triggered.connect(lambda: self._duplicateFromHandle(tHandle))
|
1359
|
-
elif isFile:
|
1360
|
-
aDuplicate = ctxMenu.addAction(self.tr("Duplicate Document"))
|
1361
|
-
aDuplicate.triggered.connect(lambda: self._duplicateFromHandle(tHandle))
|
1362
|
-
|
1363
|
-
if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
|
1364
|
-
aDelete = ctxMenu.addAction(self.tr("Delete Permanently"))
|
1365
|
-
aDelete.triggered.connect(lambda: self.permDeleteItem(tHandle))
|
1197
|
+
if trashHandle and tHandle == trashHandle:
|
1198
|
+
ctxMenu.buildTrashMenu()
|
1199
|
+
elif len(sItems) > 1:
|
1200
|
+
handles = [str(x.data(self.C_DATA, self.D_HANDLE)) for x in sItems]
|
1201
|
+
ctxMenu.buildMultiSelectMenu(handles)
|
1366
1202
|
else:
|
1367
|
-
|
1368
|
-
aMoveTrash.triggered.connect(lambda: self.moveItemToTrash(tHandle))
|
1203
|
+
ctxMenu.buildSingleSelectMenu(hasChild)
|
1369
1204
|
|
1370
|
-
# Show Context Menu
|
1371
1205
|
ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
|
1206
|
+
ctxMenu.deleteLater()
|
1372
1207
|
|
1373
1208
|
return True
|
1374
1209
|
|
@@ -1393,29 +1228,45 @@ class GuiProjectTree(QTreeWidget):
|
|
1393
1228
|
for viewing if the user middle-clicked.
|
1394
1229
|
"""
|
1395
1230
|
super().mousePressEvent(event)
|
1396
|
-
|
1397
|
-
if event.button() == Qt.LeftButton:
|
1231
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
1398
1232
|
selItem = self.indexAt(event.pos())
|
1399
1233
|
if not selItem.isValid():
|
1400
1234
|
self.clearSelection()
|
1401
|
-
|
1402
|
-
elif event.button() == Qt.MiddleButton:
|
1235
|
+
elif event.button() == Qt.MouseButton.MiddleButton:
|
1403
1236
|
selItem = self.itemAt(event.pos())
|
1404
|
-
if
|
1405
|
-
|
1406
|
-
|
1407
|
-
|
1408
|
-
|
1409
|
-
if tItem is None:
|
1410
|
-
return
|
1237
|
+
if selItem:
|
1238
|
+
tHandle = selItem.data(self.C_DATA, self.D_HANDLE)
|
1239
|
+
if (tItem := SHARED.project.tree[tHandle]) and tItem.isFileType():
|
1240
|
+
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
|
1241
|
+
return
|
1411
1242
|
|
1412
|
-
|
1413
|
-
|
1243
|
+
def startDrag(self, dropAction: Qt.DropActions) -> None:
|
1244
|
+
"""Capture the drag and drop handling to pop alerts."""
|
1245
|
+
super().startDrag(dropAction)
|
1246
|
+
if self._popAlert:
|
1247
|
+
SHARED.error(self._popAlert)
|
1248
|
+
self._popAlert = None
|
1249
|
+
return
|
1414
1250
|
|
1251
|
+
def dragEnterEvent(self, event: QDragEnterEvent) -> None:
|
1252
|
+
"""Check that we're only dragging items that are siblings, and
|
1253
|
+
not a root level item.
|
1254
|
+
"""
|
1255
|
+
items = self.selectedItems()
|
1256
|
+
if items and (parent := items[0].parent()) and all(x.parent() is parent for x in items):
|
1257
|
+
super().dragEnterEvent(event)
|
1258
|
+
else:
|
1259
|
+
logger.warning("Drag action is not allowed and has been cancelled")
|
1260
|
+
self._popAlert = self.tr(
|
1261
|
+
"Drag and drop is only allowed for single items, non-root "
|
1262
|
+
"items, or multiple items with the same parent."
|
1263
|
+
)
|
1264
|
+
event.mimeData().clear()
|
1265
|
+
event.ignore()
|
1415
1266
|
return
|
1416
1267
|
|
1417
1268
|
def dragMoveEvent(self, event: QDragMoveEvent) -> None:
|
1418
|
-
"""Capture the drag move event to enable edge
|
1269
|
+
"""Capture the drag move event to enable edge auto scroll."""
|
1419
1270
|
y = event.pos().y()
|
1420
1271
|
if y < self._scrollMargin:
|
1421
1272
|
if not self._scrollTimer.isActive():
|
@@ -1429,38 +1280,33 @@ class GuiProjectTree(QTreeWidget):
|
|
1429
1280
|
return
|
1430
1281
|
|
1431
1282
|
def dropEvent(self, event: QDropEvent) -> None:
|
1432
|
-
"""Overload the drop item event to ensure
|
1433
|
-
|
1283
|
+
"""Overload the drop item event to ensure the drag and drop
|
1284
|
+
action is allowed, and update relevant data.
|
1434
1285
|
"""
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
1438
|
-
|
1439
|
-
event.ignore()
|
1440
|
-
return
|
1441
|
-
|
1442
|
-
if not self.indexAt(event.pos()).isValid():
|
1443
|
-
# Needed due to a bug somewhere around Qt 5.15.8 that
|
1444
|
-
# ignores the invisible root item flags
|
1286
|
+
tItem = self.itemAt(event.pos())
|
1287
|
+
dropOn = self.dropIndicatorPosition() == QAbstractItemView.DropIndicatorPosition.OnItem
|
1288
|
+
# Make sure nothing can be dropped on invisible root (see #1569)
|
1289
|
+
if not tItem or tItem.parent() is None and not dropOn:
|
1445
1290
|
logger.error("Invalid drop location")
|
1446
1291
|
event.ignore()
|
1447
1292
|
return
|
1448
1293
|
|
1449
|
-
|
1294
|
+
mItems: dict[str, tuple[QTreeWidgetItem, bool]] = {}
|
1295
|
+
sItems = self.selectedItems()
|
1296
|
+
if sItems and (parent := sItems[0].parent()) and all(x.parent() is parent for x in sItems):
|
1297
|
+
for sItem in sItems:
|
1298
|
+
mHandle = str(sItem.data(self.C_DATA, self.D_HANDLE))
|
1299
|
+
mItems[mHandle] = (sItem, sItem.isExpanded())
|
1300
|
+
self.propagateCount(mHandle, 0)
|
1450
1301
|
|
1451
|
-
|
1452
|
-
pItem = sItem.parent()
|
1453
|
-
pIndex = pItem.indexOfChild(sItem) if pItem else 0
|
1302
|
+
super().dropEvent(event)
|
1454
1303
|
|
1455
|
-
|
1456
|
-
|
1304
|
+
for mHandle, (sItem, isExpanded) in mItems.items():
|
1305
|
+
self._postItemMove(mHandle)
|
1306
|
+
sItem.setExpanded(isExpanded)
|
1307
|
+
self._alertTreeChange(mHandle, flush=False)
|
1457
1308
|
|
1458
|
-
|
1459
|
-
self._postItemMove(sHandle, wCount)
|
1460
|
-
self._recordLastMove(sItem, pItem, pIndex)
|
1461
|
-
self._alertTreeChange(sHandle, flush=True)
|
1462
|
-
|
1463
|
-
sItem.setExpanded(isExpanded)
|
1309
|
+
self.saveTreeOrder()
|
1464
1310
|
|
1465
1311
|
return
|
1466
1312
|
|
@@ -1468,17 +1314,16 @@ class GuiProjectTree(QTreeWidget):
|
|
1468
1314
|
# Internal Functions
|
1469
1315
|
##
|
1470
1316
|
|
1471
|
-
def _postItemMove(self, tHandle: str
|
1317
|
+
def _postItemMove(self, tHandle: str) -> None:
|
1472
1318
|
"""Run various maintenance tasks for a moved item."""
|
1473
1319
|
trItemS = self._getTreeItem(tHandle)
|
1474
1320
|
nwItemS = SHARED.project.tree[tHandle]
|
1475
1321
|
trItemP = trItemS.parent() if trItemS else None
|
1476
1322
|
if trItemP is None or nwItemS is None:
|
1477
1323
|
logger.error("Failed to find new parent item of '%s'", tHandle)
|
1478
|
-
return
|
1324
|
+
return
|
1479
1325
|
|
1480
|
-
# Update item parent handle in the project
|
1481
|
-
# is updated accordingly, and update word count
|
1326
|
+
# Update item parent handle in the project
|
1482
1327
|
pHandle = trItemP.data(self.C_DATA, self.D_HANDLE)
|
1483
1328
|
nwItemS.setParent(pHandle)
|
1484
1329
|
trItemP.setExpanded(True)
|
@@ -1489,19 +1334,16 @@ class GuiProjectTree(QTreeWidget):
|
|
1489
1334
|
for mHandle in mHandles:
|
1490
1335
|
logger.debug("Updating item '%s'", mHandle)
|
1491
1336
|
SHARED.project.tree.updateItemData(mHandle)
|
1492
|
-
|
1493
|
-
# Update the index
|
1494
1337
|
if nwItemS.isInactiveClass():
|
1495
1338
|
SHARED.project.index.deleteHandle(mHandle)
|
1496
1339
|
else:
|
1497
1340
|
SHARED.project.index.reIndexHandle(mHandle)
|
1498
|
-
|
1499
1341
|
self.setTreeItemValues(mHandle)
|
1500
1342
|
|
1501
|
-
#
|
1502
|
-
self.propagateCount(tHandle,
|
1343
|
+
# Update word count
|
1344
|
+
self.propagateCount(tHandle, nwItemS.wordCount, countChildren=True)
|
1503
1345
|
|
1504
|
-
return
|
1346
|
+
return
|
1505
1347
|
|
1506
1348
|
def _getItemWordCount(self, tHandle: str) -> int:
|
1507
1349
|
"""Return the word count of a given item handle."""
|
@@ -1512,15 +1354,6 @@ class GuiProjectTree(QTreeWidget):
|
|
1512
1354
|
"""Return the QTreeWidgetItem of a given item handle."""
|
1513
1355
|
return self._treeMap.get(tHandle, None) if tHandle else None
|
1514
1356
|
|
1515
|
-
def _toggleItemActive(self, tHandle: str) -> None:
|
1516
|
-
"""Toggle the active status of an item."""
|
1517
|
-
tItem = SHARED.project.tree[tHandle]
|
1518
|
-
if tItem is not None:
|
1519
|
-
tItem.setActive(not tItem.isActive)
|
1520
|
-
self.setTreeItemValues(tItem.itemHandle)
|
1521
|
-
self._alertTreeChange(tHandle, flush=False)
|
1522
|
-
return
|
1523
|
-
|
1524
1357
|
def _recursiveSetExpanded(self, trItem: QTreeWidgetItem, isExpanded: bool) -> None:
|
1525
1358
|
"""Recursive function to set expanded status starting from (and
|
1526
1359
|
not including) a given item.
|
@@ -1533,60 +1366,6 @@ class GuiProjectTree(QTreeWidget):
|
|
1533
1366
|
self._recursiveSetExpanded(chItem, isExpanded)
|
1534
1367
|
return
|
1535
1368
|
|
1536
|
-
def _changeItemStatus(self, tHandle: str, tStatus: str) -> None:
|
1537
|
-
"""Set a new status value of an item."""
|
1538
|
-
tItem = SHARED.project.tree[tHandle]
|
1539
|
-
if tItem is not None:
|
1540
|
-
tItem.setStatus(tStatus)
|
1541
|
-
self.setTreeItemValues(tItem.itemHandle)
|
1542
|
-
self._alertTreeChange(tHandle, flush=False)
|
1543
|
-
return
|
1544
|
-
|
1545
|
-
def _changeItemImport(self, tHandle: str, tImport: str) -> None:
|
1546
|
-
"""Set a new importance value of an item."""
|
1547
|
-
tItem = SHARED.project.tree[tHandle]
|
1548
|
-
if tItem is not None:
|
1549
|
-
tItem.setImport(tImport)
|
1550
|
-
self.setTreeItemValues(tItem.itemHandle)
|
1551
|
-
self._alertTreeChange(tHandle, flush=False)
|
1552
|
-
return
|
1553
|
-
|
1554
|
-
def _changeItemLayout(self, tHandle: str, itemLayout: nwItemLayout) -> None:
|
1555
|
-
"""Set a new item layout value of an item."""
|
1556
|
-
tItem = SHARED.project.tree[tHandle]
|
1557
|
-
if tItem is not None:
|
1558
|
-
if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed():
|
1559
|
-
tItem.setLayout(nwItemLayout.DOCUMENT)
|
1560
|
-
self.setTreeItemValues(tHandle)
|
1561
|
-
self._alertTreeChange(tHandle, flush=False)
|
1562
|
-
elif itemLayout == nwItemLayout.NOTE:
|
1563
|
-
tItem.setLayout(nwItemLayout.NOTE)
|
1564
|
-
self.setTreeItemValues(tHandle)
|
1565
|
-
self._alertTreeChange(tHandle, flush=False)
|
1566
|
-
return
|
1567
|
-
|
1568
|
-
def _covertFolderToFile(self, tHandle: str, itemLayout: nwItemLayout) -> None:
|
1569
|
-
"""Convert a folder to a note or document."""
|
1570
|
-
tItem = SHARED.project.tree[tHandle]
|
1571
|
-
if tItem is not None and tItem.isFolderType():
|
1572
|
-
msgYes = SHARED.question(self.tr(
|
1573
|
-
"Do you want to convert the folder to a {0}? "
|
1574
|
-
"This action cannot be reversed."
|
1575
|
-
).format(trConst(nwLabels.LAYOUT_NAME[itemLayout])))
|
1576
|
-
if msgYes and itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed():
|
1577
|
-
tItem.setType(nwItemType.FILE)
|
1578
|
-
tItem.setLayout(nwItemLayout.DOCUMENT)
|
1579
|
-
self.setTreeItemValues(tHandle)
|
1580
|
-
self._alertTreeChange(tHandle, flush=False)
|
1581
|
-
elif msgYes and itemLayout == nwItemLayout.NOTE:
|
1582
|
-
tItem.setType(nwItemType.FILE)
|
1583
|
-
tItem.setLayout(nwItemLayout.NOTE)
|
1584
|
-
self.setTreeItemValues(tHandle)
|
1585
|
-
self._alertTreeChange(tHandle, flush=False)
|
1586
|
-
else:
|
1587
|
-
logger.info("Folder conversion cancelled")
|
1588
|
-
return
|
1589
|
-
|
1590
1369
|
def _mergeDocuments(self, tHandle: str, newFile: bool) -> bool:
|
1591
1370
|
"""Merge an item's child documents into a single document."""
|
1592
1371
|
logger.info("Request to merge items under handle '%s'", tHandle)
|
@@ -1606,7 +1385,7 @@ class GuiProjectTree(QTreeWidget):
|
|
1606
1385
|
dlgMerge = GuiDocMerge(self.mainGui, tHandle, itemList)
|
1607
1386
|
dlgMerge.exec_()
|
1608
1387
|
|
1609
|
-
if dlgMerge.result() == QDialog.Accepted:
|
1388
|
+
if dlgMerge.result() == QDialog.DialogCode.Accepted:
|
1610
1389
|
|
1611
1390
|
mrgData = dlgMerge.getData()
|
1612
1391
|
mrgList = mrgData.get("finalItems", [])
|
@@ -1676,7 +1455,7 @@ class GuiProjectTree(QTreeWidget):
|
|
1676
1455
|
dlgSplit = GuiDocSplit(self.mainGui, tHandle)
|
1677
1456
|
dlgSplit.exec_()
|
1678
1457
|
|
1679
|
-
if dlgSplit.result() == QDialog.Accepted:
|
1458
|
+
if dlgSplit.result() == QDialog.DialogCode.Accepted:
|
1680
1459
|
|
1681
1460
|
splitData, splitText = dlgSplit.getData()
|
1682
1461
|
|
@@ -1781,10 +1560,10 @@ class GuiProjectTree(QTreeWidget):
|
|
1781
1560
|
newItem.setText(self.C_ACTIVE, "")
|
1782
1561
|
newItem.setText(self.C_STATUS, "")
|
1783
1562
|
|
1784
|
-
newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft)
|
1785
|
-
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
1786
|
-
newItem.setTextAlignment(self.C_ACTIVE, Qt.AlignLeft)
|
1787
|
-
newItem.setTextAlignment(self.C_STATUS, Qt.AlignLeft)
|
1563
|
+
newItem.setTextAlignment(self.C_NAME, Qt.AlignmentFlag.AlignLeft)
|
1564
|
+
newItem.setTextAlignment(self.C_COUNT, Qt.AlignmentFlag.AlignRight)
|
1565
|
+
newItem.setTextAlignment(self.C_ACTIVE, Qt.AlignmentFlag.AlignLeft)
|
1566
|
+
newItem.setTextAlignment(self.C_STATUS, Qt.AlignmentFlag.AlignLeft)
|
1788
1567
|
|
1789
1568
|
newItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
|
1790
1569
|
newItem.setData(self.C_DATA, self.D_WORDS, 0)
|
@@ -1852,16 +1631,322 @@ class GuiProjectTree(QTreeWidget):
|
|
1852
1631
|
|
1853
1632
|
return
|
1854
1633
|
|
1855
|
-
|
1856
|
-
|
1857
|
-
|
1858
|
-
|
1859
|
-
|
1860
|
-
|
1861
|
-
|
1862
|
-
|
1863
|
-
|
1864
|
-
|
1634
|
+
# END Class GuiProjectTree
|
1635
|
+
|
1636
|
+
|
1637
|
+
class _TreeContextMenu(QMenu):
|
1638
|
+
|
1639
|
+
def __init__(self, projTree: GuiProjectTree, nwItem: NWItem) -> None:
|
1640
|
+
super().__init__(parent=projTree)
|
1641
|
+
|
1642
|
+
self.projTree = projTree
|
1643
|
+
self.projView = projTree.projView
|
1644
|
+
|
1645
|
+
self._item = nwItem
|
1646
|
+
self._handle = nwItem.itemHandle
|
1647
|
+
self._items: list[str] = []
|
1648
|
+
|
1649
|
+
logger.debug("Ready: _TreeContextMenu")
|
1650
|
+
|
1865
1651
|
return
|
1866
1652
|
|
1867
|
-
#
|
1653
|
+
def __del__(self): # pragma: no cover
|
1654
|
+
logger.debug("Delete: _TreeContextMenu")
|
1655
|
+
return
|
1656
|
+
|
1657
|
+
##
|
1658
|
+
# Methods
|
1659
|
+
##
|
1660
|
+
|
1661
|
+
def buildTrashMenu(self) -> None:
|
1662
|
+
"""Build the special menu for the Trash folder."""
|
1663
|
+
action = self.addAction(self.tr("Empty Trash"))
|
1664
|
+
action.triggered.connect(self.projTree.emptyTrash)
|
1665
|
+
return
|
1666
|
+
|
1667
|
+
def buildSingleSelectMenu(self, hasChild: bool) -> None:
|
1668
|
+
"""Build the single-select menu."""
|
1669
|
+
isFile = self._item.isFileType()
|
1670
|
+
isFolder = self._item.isFolderType()
|
1671
|
+
isRoot = self._item.isRootType()
|
1672
|
+
|
1673
|
+
# Document Actions
|
1674
|
+
if isFile:
|
1675
|
+
self._docActions()
|
1676
|
+
self.addSeparator()
|
1677
|
+
|
1678
|
+
# Edit Item Settings
|
1679
|
+
aLabel = self.addAction(self.tr("Rename"))
|
1680
|
+
aLabel.triggered.connect(lambda: self.projTree.renameTreeItem(self._handle))
|
1681
|
+
if isFile:
|
1682
|
+
self._itemActive(False)
|
1683
|
+
self._itemStatusImport(False)
|
1684
|
+
|
1685
|
+
# Transform Item
|
1686
|
+
if isFile or isFolder:
|
1687
|
+
self._itemTransform(isFile, isFolder, hasChild)
|
1688
|
+
self.addSeparator()
|
1689
|
+
|
1690
|
+
# Process Item
|
1691
|
+
self._itemProcess(isFile, isFolder, isRoot, hasChild)
|
1692
|
+
|
1693
|
+
return
|
1694
|
+
|
1695
|
+
def buildMultiSelectMenu(self, items: list[str]) -> None:
|
1696
|
+
"""Build the multi-select menu."""
|
1697
|
+
self._items = items
|
1698
|
+
self._itemActive(True)
|
1699
|
+
self._itemStatusImport(True)
|
1700
|
+
self.addSeparator()
|
1701
|
+
self._moveToTrash(True)
|
1702
|
+
return
|
1703
|
+
|
1704
|
+
##
|
1705
|
+
# Menu Builders
|
1706
|
+
##
|
1707
|
+
|
1708
|
+
def _docActions(self) -> None:
|
1709
|
+
"""Add document actions."""
|
1710
|
+
action = self.addAction(self.tr("Open Document"))
|
1711
|
+
action.triggered.connect(
|
1712
|
+
lambda: self.projView.openDocumentRequest.emit(self._handle, nwDocMode.EDIT, "", True)
|
1713
|
+
)
|
1714
|
+
action = self.addAction(self.tr("View Document"))
|
1715
|
+
action.triggered.connect(
|
1716
|
+
lambda: self.projView.openDocumentRequest.emit(self._handle, nwDocMode.VIEW, "", False)
|
1717
|
+
)
|
1718
|
+
return
|
1719
|
+
|
1720
|
+
def _itemActive(self, multi: bool) -> None:
|
1721
|
+
"""Add Active/Inactive actions."""
|
1722
|
+
if multi:
|
1723
|
+
mSub = self.addMenu(self.tr("Set Active to ..."))
|
1724
|
+
aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.tr("Active"))
|
1725
|
+
aOne.triggered.connect(lambda: self._iterItemActive(True))
|
1726
|
+
aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.tr("Inactive"))
|
1727
|
+
aTwo.triggered.connect(lambda: self._iterItemActive(False))
|
1728
|
+
else:
|
1729
|
+
action = self.addAction(self.tr("Toggle Active"))
|
1730
|
+
action.triggered.connect(self._toggleItemActive)
|
1731
|
+
return
|
1732
|
+
|
1733
|
+
def _itemStatusImport(self, multi: bool) -> None:
|
1734
|
+
"""Add actions for changing status or importance."""
|
1735
|
+
if self._item.isNovelLike():
|
1736
|
+
menu = self.addMenu(self.tr("Set Status to ..."))
|
1737
|
+
current = self._item.itemStatus
|
1738
|
+
for n, (key, entry) in enumerate(SHARED.project.data.itemStatus.items()):
|
1739
|
+
name = entry["name"]
|
1740
|
+
if not multi and current == key:
|
1741
|
+
name += f" ({nwUnicode.U_CHECK})"
|
1742
|
+
action = menu.addAction(entry["icon"], name)
|
1743
|
+
if multi:
|
1744
|
+
action.triggered.connect(lambda n, key=key: self._iterSetItemStatus(key))
|
1745
|
+
else:
|
1746
|
+
action.triggered.connect(lambda n, key=key: self._changeItemStatus(key))
|
1747
|
+
menu.addSeparator()
|
1748
|
+
action = menu.addAction(self.tr("Manage Labels ..."))
|
1749
|
+
action.triggered.connect(
|
1750
|
+
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_STATUS)
|
1751
|
+
)
|
1752
|
+
else:
|
1753
|
+
menu = self.addMenu(self.tr("Set Importance to ..."))
|
1754
|
+
current = self._item.itemImport
|
1755
|
+
for n, (key, entry) in enumerate(SHARED.project.data.itemImport.items()):
|
1756
|
+
name = entry["name"]
|
1757
|
+
if not multi and current == key:
|
1758
|
+
name += f" ({nwUnicode.U_CHECK})"
|
1759
|
+
action = menu.addAction(entry["icon"], name)
|
1760
|
+
if multi:
|
1761
|
+
action.triggered.connect(lambda n, key=key: self._iterSetItemImport(key))
|
1762
|
+
else:
|
1763
|
+
action.triggered.connect(lambda n, key=key: self._changeItemImport(key))
|
1764
|
+
menu.addSeparator()
|
1765
|
+
action = menu.addAction(self.tr("Manage Labels ..."))
|
1766
|
+
action.triggered.connect(
|
1767
|
+
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_IMPORT)
|
1768
|
+
)
|
1769
|
+
return
|
1770
|
+
|
1771
|
+
def _itemTransform(self, isFile: bool, isFolder: bool, hasChild: bool) -> None:
|
1772
|
+
"""Add actions for the Transform menu."""
|
1773
|
+
menu = self.addMenu(self.tr("Transform"))
|
1774
|
+
|
1775
|
+
tree = self.projTree
|
1776
|
+
tHandle = self._handle
|
1777
|
+
|
1778
|
+
trDoc = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
|
1779
|
+
trNote = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
|
1780
|
+
loDoc = nwItemLayout.DOCUMENT
|
1781
|
+
loNote = nwItemLayout.NOTE
|
1782
|
+
isDocFile = isFile and self._item.isDocumentLayout()
|
1783
|
+
isNoteFile = isFile and self._item.isNoteLayout()
|
1784
|
+
|
1785
|
+
if isNoteFile and self._item.documentAllowed():
|
1786
|
+
action = menu.addAction(self.tr("Convert to {0}").format(trDoc))
|
1787
|
+
action.triggered.connect(lambda: self._changeItemLayout(loDoc))
|
1788
|
+
|
1789
|
+
if isDocFile:
|
1790
|
+
action = menu.addAction(self.tr("Convert to {0}").format(trNote))
|
1791
|
+
action.triggered.connect(lambda: self._changeItemLayout(loNote))
|
1792
|
+
|
1793
|
+
if isFolder and self._item.documentAllowed():
|
1794
|
+
action = menu.addAction(self.tr("Convert to {0}").format(trDoc))
|
1795
|
+
action.triggered.connect(lambda: self._covertFolderToFile(loDoc))
|
1796
|
+
|
1797
|
+
if isFolder:
|
1798
|
+
action = menu.addAction(self.tr("Convert to {0}").format(trNote))
|
1799
|
+
action.triggered.connect(lambda: self._covertFolderToFile(loNote))
|
1800
|
+
|
1801
|
+
if hasChild and isFile:
|
1802
|
+
action = menu.addAction(self.tr("Merge Child Items into Self"))
|
1803
|
+
action.triggered.connect(lambda: tree._mergeDocuments(tHandle, False))
|
1804
|
+
action = menu.addAction(self.tr("Merge Child Items into New"))
|
1805
|
+
action.triggered.connect(lambda: tree._mergeDocuments(tHandle, True))
|
1806
|
+
|
1807
|
+
if hasChild and isFolder:
|
1808
|
+
action = menu.addAction(self.tr("Merge Documents in Folder"))
|
1809
|
+
action.triggered.connect(lambda: tree._mergeDocuments(tHandle, True))
|
1810
|
+
|
1811
|
+
if isFile:
|
1812
|
+
action = menu.addAction(self.tr("Split Document by Headers"))
|
1813
|
+
action.triggered.connect(lambda: tree._splitDocument(tHandle))
|
1814
|
+
|
1815
|
+
return
|
1816
|
+
|
1817
|
+
def _itemProcess(self, isFile: bool, isFolder: bool, isRoot: bool, hasChild: bool) -> None:
|
1818
|
+
"""Add actions for item processing."""
|
1819
|
+
tree = self.projTree
|
1820
|
+
tHandle = self._handle
|
1821
|
+
if hasChild:
|
1822
|
+
action = self.addAction(self.tr("Expand All"))
|
1823
|
+
action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, True))
|
1824
|
+
action = self.addAction(self.tr("Collapse All"))
|
1825
|
+
action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, False))
|
1826
|
+
action = self.addAction(self.tr("Duplicate from Here"))
|
1827
|
+
action.triggered.connect(lambda: tree._duplicateFromHandle(tHandle))
|
1828
|
+
elif isFile:
|
1829
|
+
action = self.addAction(self.tr("Duplicate Document"))
|
1830
|
+
action.triggered.connect(lambda: tree._duplicateFromHandle(tHandle))
|
1831
|
+
|
1832
|
+
if self._item.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
|
1833
|
+
action = self.addAction(self.tr("Delete Permanently"))
|
1834
|
+
action.triggered.connect(lambda: tree.permDeleteItem(tHandle))
|
1835
|
+
else:
|
1836
|
+
action = self.addAction(self.tr("Move to Trash"))
|
1837
|
+
action.triggered.connect(lambda: tree.moveItemToTrash(tHandle))
|
1838
|
+
|
1839
|
+
return
|
1840
|
+
|
1841
|
+
def _moveToTrash(self, multi: bool) -> None:
|
1842
|
+
"""Add move to Trash action."""
|
1843
|
+
action = self.addAction(self.tr("Move to Trash"))
|
1844
|
+
if multi:
|
1845
|
+
action.triggered.connect(self._iterMoveToTrash)
|
1846
|
+
return
|
1847
|
+
|
1848
|
+
##
|
1849
|
+
# Private Slots
|
1850
|
+
##
|
1851
|
+
|
1852
|
+
@pyqtSlot()
|
1853
|
+
def _iterMoveToTrash(self) -> None:
|
1854
|
+
"""Iterate through files and move them to Trash."""
|
1855
|
+
if SHARED.question(self.tr("Move {0} items to Trash?").format(len(self._items))):
|
1856
|
+
for tHandle in self._items:
|
1857
|
+
tItem = SHARED.project.tree[tHandle]
|
1858
|
+
if tItem and tItem.isFileType():
|
1859
|
+
self.projTree.moveItemToTrash(tHandle, askFirst=False, flush=False)
|
1860
|
+
self.projTree.saveTreeOrder()
|
1861
|
+
return
|
1862
|
+
|
1863
|
+
@pyqtSlot()
|
1864
|
+
def _toggleItemActive(self) -> None:
|
1865
|
+
"""Toggle the active status of an item."""
|
1866
|
+
self._item.setActive(not self._item.isActive)
|
1867
|
+
self.projTree.setTreeItemValues(self._handle)
|
1868
|
+
self.projTree._alertTreeChange(self._handle, flush=False)
|
1869
|
+
return
|
1870
|
+
|
1871
|
+
##
|
1872
|
+
# Internal Functions
|
1873
|
+
##
|
1874
|
+
|
1875
|
+
def _iterItemActive(self, isActive: bool) -> None:
|
1876
|
+
"""Set the active status of multiple items."""
|
1877
|
+
for tHandle in self._items:
|
1878
|
+
tItem = SHARED.project.tree[tHandle]
|
1879
|
+
if tItem and tItem.isFileType():
|
1880
|
+
tItem.setActive(isActive)
|
1881
|
+
self.projTree.setTreeItemValues(tHandle)
|
1882
|
+
self.projTree._alertTreeChange(tHandle, flush=False)
|
1883
|
+
return
|
1884
|
+
|
1885
|
+
def _changeItemStatus(self, key: str) -> None:
|
1886
|
+
"""Set a new status value of an item."""
|
1887
|
+
self._item.setStatus(key)
|
1888
|
+
self.projTree.setTreeItemValues(self._handle)
|
1889
|
+
self.projTree._alertTreeChange(self._handle, flush=False)
|
1890
|
+
return
|
1891
|
+
|
1892
|
+
def _iterSetItemStatus(self, key: str) -> None:
|
1893
|
+
"""Change the status value for multiple items."""
|
1894
|
+
for tHandle in self._items:
|
1895
|
+
tItem = SHARED.project.tree[tHandle]
|
1896
|
+
if tItem and tItem.isNovelLike():
|
1897
|
+
tItem.setStatus(key)
|
1898
|
+
self.projTree.setTreeItemValues(tHandle)
|
1899
|
+
self.projTree._alertTreeChange(tHandle, flush=False)
|
1900
|
+
return
|
1901
|
+
|
1902
|
+
def _changeItemImport(self, key: str) -> None:
|
1903
|
+
"""Set a new importance value of an item."""
|
1904
|
+
self._item.setImport(key)
|
1905
|
+
self.projTree.setTreeItemValues(self._handle)
|
1906
|
+
self.projTree._alertTreeChange(self._handle, flush=False)
|
1907
|
+
return
|
1908
|
+
|
1909
|
+
def _iterSetItemImport(self, key: str) -> None:
|
1910
|
+
"""Change the status value for multiple items."""
|
1911
|
+
for tHandle in self._items:
|
1912
|
+
tItem = SHARED.project.tree[tHandle]
|
1913
|
+
if tItem and not tItem.isNovelLike():
|
1914
|
+
tItem.setImport(key)
|
1915
|
+
self.projTree.setTreeItemValues(tHandle)
|
1916
|
+
self.projTree._alertTreeChange(tHandle, flush=False)
|
1917
|
+
return
|
1918
|
+
|
1919
|
+
def _changeItemLayout(self, itemLayout: nwItemLayout) -> None:
|
1920
|
+
"""Set a new item layout value of an item."""
|
1921
|
+
if itemLayout == nwItemLayout.DOCUMENT and self._item.documentAllowed():
|
1922
|
+
self._item.setLayout(nwItemLayout.DOCUMENT)
|
1923
|
+
self.projTree.setTreeItemValues(self._handle)
|
1924
|
+
self.projTree._alertTreeChange(self._handle, flush=False)
|
1925
|
+
elif itemLayout == nwItemLayout.NOTE:
|
1926
|
+
self._item.setLayout(nwItemLayout.NOTE)
|
1927
|
+
self.projTree.setTreeItemValues(self._handle)
|
1928
|
+
self.projTree._alertTreeChange(self._handle, flush=False)
|
1929
|
+
return
|
1930
|
+
|
1931
|
+
def _covertFolderToFile(self, itemLayout: nwItemLayout) -> None:
|
1932
|
+
"""Convert a folder to a note or document."""
|
1933
|
+
if self._item.isFolderType():
|
1934
|
+
msgYes = SHARED.question(self.tr(
|
1935
|
+
"Do you want to convert the folder to a {0}? "
|
1936
|
+
"This action cannot be reversed."
|
1937
|
+
).format(trConst(nwLabels.LAYOUT_NAME[itemLayout])))
|
1938
|
+
if msgYes and itemLayout == nwItemLayout.DOCUMENT and self._item.documentAllowed():
|
1939
|
+
self._item.setType(nwItemType.FILE)
|
1940
|
+
self._item.setLayout(nwItemLayout.DOCUMENT)
|
1941
|
+
self.projTree.setTreeItemValues(self._handle)
|
1942
|
+
self.projTree._alertTreeChange(self._handle, flush=False)
|
1943
|
+
elif msgYes and itemLayout == nwItemLayout.NOTE:
|
1944
|
+
self._item.setType(nwItemType.FILE)
|
1945
|
+
self._item.setLayout(nwItemLayout.NOTE)
|
1946
|
+
self.projTree.setTreeItemValues(self._handle)
|
1947
|
+
self.projTree._alertTreeChange(self._handle, flush=False)
|
1948
|
+
else:
|
1949
|
+
logger.info("Folder conversion cancelled")
|
1950
|
+
return
|
1951
|
+
|
1952
|
+
# END Class _TreeContextMenu
|