novelWriter 2.1.1__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.1.1.dist-info → novelWriter-2.2rc1.dist-info}/METADATA +3 -3
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/RECORD +105 -76
- novelwriter/__init__.py +6 -24
- novelwriter/assets/i18n/project_de_DE.json +10 -0
- novelwriter/assets/i18n/project_en_GB.json +11 -0
- novelwriter/assets/i18n/project_en_US.json +10 -0
- novelwriter/assets/i18n/project_ja_JP.json +11 -1
- novelwriter/assets/i18n/project_nb_NO.json +10 -0
- novelwriter/assets/i18n/project_nn_NO.json +10 -0
- novelwriter/assets/icons/novelwriter.ico +0 -0
- novelwriter/assets/icons/novelwriter.svg +8 -183
- novelwriter/assets/icons/typicons_dark/icons.conf +17 -2
- 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/nw_tb-bold.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +8 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +8 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +5 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +5 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +5 -0
- novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg +4 -0
- novelwriter/assets/icons/typicons_light/icons.conf +17 -2
- 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/nw_tb-bold.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +8 -0
- novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +8 -0
- novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +5 -0
- novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +5 -0
- novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +5 -0
- novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg +4 -0
- novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
- novelwriter/assets/icons/x-novelwriter-project.svg +7 -206
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/syntax/default_dark.conf +1 -0
- novelwriter/assets/syntax/default_light.conf +1 -0
- novelwriter/assets/syntax/grey_dark.conf +1 -0
- novelwriter/assets/syntax/grey_light.conf +1 -0
- novelwriter/assets/syntax/light_owl.conf +1 -0
- novelwriter/assets/syntax/night_owl.conf +1 -0
- novelwriter/assets/syntax/solarized_dark.conf +1 -0
- novelwriter/assets/syntax/solarized_light.conf +1 -0
- novelwriter/assets/syntax/tomorrow.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
- novelwriter/assets/text/credits_en.htm +7 -0
- novelwriter/assets/text/release_notes.htm +7 -37
- novelwriter/common.py +22 -1
- novelwriter/config.py +27 -42
- novelwriter/constants.py +45 -7
- novelwriter/core/buildsettings.py +40 -24
- novelwriter/core/coretools.py +8 -1
- novelwriter/core/docbuild.py +2 -6
- novelwriter/core/index.py +264 -175
- novelwriter/core/options.py +8 -3
- novelwriter/core/project.py +2 -2
- novelwriter/core/projectdata.py +3 -3
- novelwriter/core/tohtml.py +60 -59
- novelwriter/core/tokenizer.py +110 -70
- novelwriter/core/tomd.py +51 -38
- novelwriter/core/toodt.py +184 -147
- novelwriter/dialogs/preferences.py +75 -106
- novelwriter/dialogs/projsettings.py +101 -110
- novelwriter/dialogs/updates.py +25 -14
- novelwriter/enum.py +28 -3
- novelwriter/extensions/novelselector.py +1 -1
- novelwriter/gui/doceditor.py +1345 -1235
- novelwriter/gui/dochighlight.py +98 -62
- novelwriter/gui/docviewer.py +151 -340
- novelwriter/gui/docviewerpanel.py +457 -0
- novelwriter/gui/editordocument.py +126 -0
- novelwriter/gui/mainmenu.py +350 -300
- novelwriter/gui/noveltree.py +101 -125
- novelwriter/gui/outline.py +154 -171
- novelwriter/gui/projtree.py +480 -380
- novelwriter/gui/sidebar.py +106 -75
- novelwriter/gui/statusbar.py +1 -1
- novelwriter/gui/theme.py +114 -75
- novelwriter/guimain.py +353 -254
- novelwriter/shared.py +36 -3
- novelwriter/tools/dictionaries.py +268 -0
- novelwriter/tools/manusbuild.py +17 -6
- novelwriter/tools/manuscript.py +11 -3
- novelwriter/tools/manussettings.py +0 -14
- novelwriter/tools/projwizard.py +16 -2
- novelwriter/tools/writingstats.py +1 -1
- novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_th-menu.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_th-menu.svg +0 -4
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/WHEEL +0 -0
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.1.1.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,11 +137,9 @@ 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
|
142
|
+
self.createNewNote = self.projTree.createNewNote
|
150
143
|
|
151
144
|
return
|
152
145
|
|
@@ -188,7 +181,7 @@ class GuiProjectView(QWidget):
|
|
188
181
|
self.projTree.buildTree()
|
189
182
|
return
|
190
183
|
|
191
|
-
def
|
184
|
+
def setTreeFocus(self) -> None:
|
192
185
|
"""Forward the set focus call to the tree widget."""
|
193
186
|
self.projTree.setFocus()
|
194
187
|
return
|
@@ -197,17 +190,32 @@ class GuiProjectView(QWidget):
|
|
197
190
|
"""Check if the project tree has focus."""
|
198
191
|
return self.projTree.hasFocus()
|
199
192
|
|
200
|
-
|
193
|
+
##
|
194
|
+
# Public Slots
|
195
|
+
##
|
196
|
+
|
197
|
+
@pyqtSlot(str, str)
|
198
|
+
def renameTreeItem(self, tHandle: str | None = None, name: str = "") -> None:
|
201
199
|
"""External request to rename an item or the currently selected
|
202
200
|
item. This is triggered by the global menu or keyboard shortcut.
|
203
201
|
"""
|
204
202
|
if tHandle is None:
|
205
203
|
tHandle = self.projTree.getSelectedHandle()
|
206
|
-
|
204
|
+
if tHandle:
|
205
|
+
self.projTree.renameTreeItem(tHandle, name=name)
|
206
|
+
return
|
207
207
|
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
211
219
|
|
212
220
|
@pyqtSlot(str, int, int, int)
|
213
221
|
def updateCounts(self, tHandle: str, cCount: int, wCount: int, pCount: int) -> None:
|
@@ -255,7 +263,7 @@ class GuiProjectToolBar(QWidget):
|
|
255
263
|
self.tbQuick.setShortcut("Ctrl+L")
|
256
264
|
self.tbQuick.setIconSize(QSize(iPx, iPx))
|
257
265
|
self.tbQuick.setMenu(self.mQuick)
|
258
|
-
self.tbQuick.setPopupMode(QToolButton.InstantPopup)
|
266
|
+
self.tbQuick.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
259
267
|
|
260
268
|
# Move Buttons
|
261
269
|
self.tbMoveU = QToolButton(self)
|
@@ -304,7 +312,7 @@ class GuiProjectToolBar(QWidget):
|
|
304
312
|
self.tbAdd.setShortcut("Ctrl+N")
|
305
313
|
self.tbAdd.setIconSize(QSize(iPx, iPx))
|
306
314
|
self.tbAdd.setMenu(self.mAdd)
|
307
|
-
self.tbAdd.setPopupMode(QToolButton.InstantPopup)
|
315
|
+
self.tbAdd.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
308
316
|
|
309
317
|
# More Options Menu
|
310
318
|
self.mMore = QMenu(self)
|
@@ -315,9 +323,6 @@ class GuiProjectToolBar(QWidget):
|
|
315
323
|
self.aCollapse = self.mMore.addAction(self.tr("Collapse All"))
|
316
324
|
self.aCollapse.triggered.connect(lambda: self.projTree.setExpandedFromHandle(None, False))
|
317
325
|
|
318
|
-
self.aMoreUndo = self.mMore.addAction(self.tr("Undo Move"))
|
319
|
-
self.aMoreUndo.triggered.connect(lambda: self.projTree.undoLastMove())
|
320
|
-
|
321
326
|
self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
|
322
327
|
self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash())
|
323
328
|
|
@@ -325,7 +330,7 @@ class GuiProjectToolBar(QWidget):
|
|
325
330
|
self.tbMore.setToolTip(self.tr("More Options"))
|
326
331
|
self.tbMore.setIconSize(QSize(iPx, iPx))
|
327
332
|
self.tbMore.setMenu(self.mMore)
|
328
|
-
self.tbMore.setPopupMode(QToolButton.InstantPopup)
|
333
|
+
self.tbMore.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
329
334
|
|
330
335
|
# Assemble
|
331
336
|
self.outerBox = QHBoxLayout()
|
@@ -352,7 +357,7 @@ class GuiProjectToolBar(QWidget):
|
|
352
357
|
def updateTheme(self) -> None:
|
353
358
|
"""Update theme elements."""
|
354
359
|
qPalette = self.palette()
|
355
|
-
qPalette.setBrush(QPalette.Window, qPalette.base())
|
360
|
+
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
|
356
361
|
self.setPalette(qPalette)
|
357
362
|
|
358
363
|
fadeCol = qPalette.text().color()
|
@@ -360,12 +365,13 @@ class GuiProjectToolBar(QWidget):
|
|
360
365
|
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
|
361
366
|
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
|
362
367
|
).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
|
368
|
+
buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
|
363
369
|
|
364
|
-
self.tbQuick.setStyleSheet(
|
370
|
+
self.tbQuick.setStyleSheet(buttonStyleMenu)
|
365
371
|
self.tbMoveU.setStyleSheet(buttonStyle)
|
366
372
|
self.tbMoveD.setStyleSheet(buttonStyle)
|
367
|
-
self.tbAdd.setStyleSheet(
|
368
|
-
self.tbMore.setStyleSheet(
|
373
|
+
self.tbAdd.setStyleSheet(buttonStyleMenu)
|
374
|
+
self.tbMore.setStyleSheet(buttonStyleMenu)
|
369
375
|
|
370
376
|
self.tbQuick.setIcon(SHARED.theme.getIcon("bookmark"))
|
371
377
|
self.tbMoveU.setIcon(SHARED.theme.getIcon("up"))
|
@@ -429,7 +435,7 @@ class GuiProjectToolBar(QWidget):
|
|
429
435
|
return
|
430
436
|
|
431
437
|
##
|
432
|
-
# Slots
|
438
|
+
# Private Slots
|
433
439
|
##
|
434
440
|
|
435
441
|
@pyqtSlot(str)
|
@@ -469,14 +475,14 @@ class GuiProjectTree(QTreeWidget):
|
|
469
475
|
|
470
476
|
# Internal Variables
|
471
477
|
self._treeMap = {}
|
472
|
-
self._lastMove = {}
|
473
478
|
self._timeChanged = 0.0
|
479
|
+
self._popAlert = None
|
474
480
|
|
475
481
|
# Build GUI
|
476
482
|
# =========
|
477
483
|
|
478
484
|
# Context Menu
|
479
|
-
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
485
|
+
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
480
486
|
self.customContextMenuRequested.connect(self._openContextMenu)
|
481
487
|
|
482
488
|
# Tree Settings
|
@@ -484,7 +490,7 @@ class GuiProjectTree(QTreeWidget):
|
|
484
490
|
cMg = CONFIG.pxInt(6)
|
485
491
|
|
486
492
|
self.setIconSize(QSize(iPx, iPx))
|
487
|
-
self.setFrameStyle(QFrame.NoFrame)
|
493
|
+
self.setFrameStyle(QFrame.Shape.NoFrame)
|
488
494
|
self.setUniformRowHeights(True)
|
489
495
|
self.setAllColumnsShowFocus(True)
|
490
496
|
self.setExpandsOnDoubleClick(False)
|
@@ -497,19 +503,18 @@ class GuiProjectTree(QTreeWidget):
|
|
497
503
|
treeHeader = self.header()
|
498
504
|
treeHeader.setStretchLastSection(False)
|
499
505
|
treeHeader.setMinimumSectionSize(iPx + cMg)
|
500
|
-
treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.Stretch)
|
501
|
-
treeHeader.setSectionResizeMode(self.C_COUNT, QHeaderView.ResizeToContents)
|
502
|
-
treeHeader.setSectionResizeMode(self.C_ACTIVE, QHeaderView.Fixed)
|
503
|
-
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)
|
504
510
|
treeHeader.resizeSection(self.C_ACTIVE, iPx + cMg)
|
505
511
|
treeHeader.resizeSection(self.C_STATUS, iPx + cMg)
|
506
512
|
|
507
513
|
# Allow Move by Drag & Drop
|
508
514
|
self.setDragEnabled(True)
|
509
|
-
self.setDragDropMode(QAbstractItemView.InternalMove)
|
510
|
-
self.setDropIndicatorShown(True)
|
515
|
+
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
511
516
|
|
512
|
-
# Disable built-in
|
517
|
+
# Disable built-in auto scroll as it isn't working in some Qt
|
513
518
|
# releases (see #1561) and instead use our own implementation
|
514
519
|
self.setAutoScroll(False)
|
515
520
|
|
@@ -517,21 +522,21 @@ class GuiProjectTree(QTreeWidget):
|
|
517
522
|
# Due to a bug, this stops working somewhere between Qt 5.15.3
|
518
523
|
# and 5.15.8, so this is also blocked in dropEvent (see #1569)
|
519
524
|
trRoot = self.invisibleRootItem()
|
520
|
-
trRoot.setFlags(trRoot.flags() ^ Qt.ItemIsDropEnabled)
|
525
|
+
trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled)
|
521
526
|
|
522
527
|
# Cached values
|
523
528
|
self._lblActive = self.tr("Active")
|
524
529
|
self._lblInactive = self.tr("Inactive")
|
525
530
|
|
526
531
|
# Set selection options
|
527
|
-
self.setSelectionMode(QAbstractItemView.
|
528
|
-
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
532
|
+
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
533
|
+
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
529
534
|
|
530
535
|
# Connect signals
|
531
536
|
self.itemDoubleClicked.connect(self._treeDoubleClick)
|
532
537
|
self.itemSelectionChanged.connect(self._treeSelectionChange)
|
533
538
|
|
534
|
-
#
|
539
|
+
# Auto Scroll
|
535
540
|
self._scrollMargin = SHARED.theme.baseIconSize
|
536
541
|
self._scrollDirection = 0
|
537
542
|
self._scrollTimer = QTimer()
|
@@ -549,13 +554,13 @@ class GuiProjectTree(QTreeWidget):
|
|
549
554
|
"""Set or update tree widget settings."""
|
550
555
|
# Scroll bars
|
551
556
|
if CONFIG.hideVScroll:
|
552
|
-
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
557
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
553
558
|
else:
|
554
|
-
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
559
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
555
560
|
if CONFIG.hideHScroll:
|
556
|
-
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
561
|
+
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
557
562
|
else:
|
558
|
-
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
563
|
+
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
559
564
|
return
|
560
565
|
|
561
566
|
##
|
@@ -566,10 +571,22 @@ class GuiProjectTree(QTreeWidget):
|
|
566
571
|
"""Clear the GUI content and the related map."""
|
567
572
|
self.clear()
|
568
573
|
self._treeMap = {}
|
569
|
-
self._lastMove = {}
|
570
574
|
self._timeChanged = 0.0
|
571
575
|
return
|
572
576
|
|
577
|
+
def createNewNote(self, tag: str, itemClass: nwItemClass | None) -> bool:
|
578
|
+
"""Create a new note. This function is used by the document
|
579
|
+
editor to create note files for unknown tags.
|
580
|
+
"""
|
581
|
+
rHandle = SHARED.project.tree.findRoot(itemClass)
|
582
|
+
if rHandle:
|
583
|
+
tHandle = SHARED.project.newFile(tag, rHandle)
|
584
|
+
if tHandle:
|
585
|
+
SHARED.project.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n")
|
586
|
+
self.revealNewTreeItem(tHandle, wordCount=True)
|
587
|
+
return True
|
588
|
+
return False
|
589
|
+
|
573
590
|
def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
|
574
591
|
hLevel: int = 1, isNote: bool = False) -> bool:
|
575
592
|
"""Add new item to the tree, with a given itemType (and
|
@@ -724,7 +741,6 @@ class GuiProjectTree(QTreeWidget):
|
|
724
741
|
|
725
742
|
cItem = pItem.takeChild(tIndex)
|
726
743
|
pItem.insertChild(nIndex, cItem)
|
727
|
-
self._recordLastMove(cItem, pItem, tIndex)
|
728
744
|
|
729
745
|
self._alertTreeChange(tHandle, flush=True)
|
730
746
|
self.setCurrentItem(tItem)
|
@@ -753,19 +769,16 @@ class GuiProjectTree(QTreeWidget):
|
|
753
769
|
self.setCurrentItem(tItem.child(0))
|
754
770
|
return
|
755
771
|
|
756
|
-
def renameTreeItem(self, tHandle: str) ->
|
772
|
+
def renameTreeItem(self, tHandle: str, name: str = "") -> None:
|
757
773
|
"""Open a dialog to edit the label of an item."""
|
758
774
|
tItem = SHARED.project.tree[tHandle]
|
759
|
-
if tItem
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
self._alertTreeChange(tHandle, flush=False)
|
767
|
-
|
768
|
-
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
|
769
782
|
|
770
783
|
def saveTreeOrder(self) -> None:
|
771
784
|
"""Build a list of the items in the project tree and send them
|
@@ -828,6 +841,7 @@ class GuiProjectTree(QTreeWidget):
|
|
828
841
|
|
829
842
|
return status
|
830
843
|
|
844
|
+
@pyqtSlot()
|
831
845
|
def emptyTrash(self) -> bool:
|
832
846
|
"""Permanently delete all documents in the Trash folder. This
|
833
847
|
function only asks for confirmation once, and calls the regular
|
@@ -906,15 +920,13 @@ class GuiProjectTree(QTreeWidget):
|
|
906
920
|
logger.info("Action cancelled by user")
|
907
921
|
return False
|
908
922
|
|
909
|
-
wCount = self._getItemWordCount(tHandle)
|
910
923
|
self.propagateCount(tHandle, 0)
|
911
924
|
|
912
925
|
tIndex = trItemP.indexOfChild(trItemS)
|
913
926
|
trItemC = trItemP.takeChild(tIndex)
|
914
927
|
trItemT.addChild(trItemC)
|
915
928
|
|
916
|
-
self._postItemMove(tHandle
|
917
|
-
self._recordLastMove(trItemS, trItemP, tIndex)
|
929
|
+
self._postItemMove(tHandle)
|
918
930
|
self._alertTreeChange(tHandle, flush=flush)
|
919
931
|
|
920
932
|
logger.debug("Moved item '%s' to Trash", tHandle)
|
@@ -1075,46 +1087,6 @@ class GuiProjectTree(QTreeWidget):
|
|
1075
1087
|
logger.info("%d item(s) added to the project tree", count)
|
1076
1088
|
return
|
1077
1089
|
|
1078
|
-
def undoLastMove(self) -> bool:
|
1079
|
-
"""Attempt to undo the last action."""
|
1080
|
-
srcItem = self._lastMove.get("item", None)
|
1081
|
-
dstItem = self._lastMove.get("parent", None)
|
1082
|
-
dstIndex = self._lastMove.get("index", None)
|
1083
|
-
|
1084
|
-
srcOK = isinstance(srcItem, QTreeWidgetItem)
|
1085
|
-
dstOk = isinstance(dstItem, QTreeWidgetItem)
|
1086
|
-
if not srcOK or not dstOk or dstIndex is None:
|
1087
|
-
logger.debug("No tree move to undo")
|
1088
|
-
return False
|
1089
|
-
|
1090
|
-
if srcItem not in self._treeMap.values():
|
1091
|
-
logger.warning("Source item no longer exists")
|
1092
|
-
return False
|
1093
|
-
|
1094
|
-
if dstItem not in self._treeMap.values():
|
1095
|
-
logger.warning("Previous parent item no longer exists")
|
1096
|
-
return False
|
1097
|
-
|
1098
|
-
dstIndex = min(max(0, dstIndex), dstItem.childCount())
|
1099
|
-
sHandle = srcItem.data(self.C_DATA, self.D_HANDLE)
|
1100
|
-
dHandle = dstItem.data(self.C_DATA, self.D_HANDLE)
|
1101
|
-
logger.debug("Moving item '%s' back to '%s', index %d", sHandle, dHandle, dstIndex)
|
1102
|
-
|
1103
|
-
wCount = self._getItemWordCount(sHandle)
|
1104
|
-
self.propagateCount(sHandle, 0)
|
1105
|
-
parItem = srcItem.parent()
|
1106
|
-
srcIndex = parItem.indexOfChild(srcItem)
|
1107
|
-
movItem = parItem.takeChild(srcIndex)
|
1108
|
-
dstItem.insertChild(dstIndex, movItem)
|
1109
|
-
|
1110
|
-
self._postItemMove(sHandle, wCount)
|
1111
|
-
self._alertTreeChange(sHandle, flush=True)
|
1112
|
-
|
1113
|
-
self.setCurrentItem(movItem)
|
1114
|
-
self._lastMove = {}
|
1115
|
-
|
1116
|
-
return True
|
1117
|
-
|
1118
1090
|
def getSelectedHandle(self) -> str | None:
|
1119
1091
|
"""Get the currently selected handle. If multiple items are
|
1120
1092
|
selected, return the first.
|
@@ -1135,7 +1107,7 @@ class GuiProjectTree(QTreeWidget):
|
|
1135
1107
|
|
1136
1108
|
selIndex = self.selectedIndexes()
|
1137
1109
|
if selIndex and doScroll:
|
1138
|
-
self.scrollTo(selIndex[0], QAbstractItemView.PositionAtCenter)
|
1110
|
+
self.scrollTo(selIndex[0], QAbstractItemView.ScrollHint.PositionAtCenter)
|
1139
1111
|
|
1140
1112
|
return True
|
1141
1113
|
|
@@ -1170,6 +1142,15 @@ class GuiProjectTree(QTreeWidget):
|
|
1170
1142
|
tHandle = self.getSelectedHandle()
|
1171
1143
|
if tHandle is not None:
|
1172
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
|
+
|
1173
1154
|
return
|
1174
1155
|
|
1175
1156
|
@pyqtSlot("QTreeWidgetItem*", int)
|
@@ -1200,160 +1181,29 @@ class GuiProjectTree(QTreeWidget):
|
|
1200
1181
|
tItem = None
|
1201
1182
|
tHandle = None
|
1202
1183
|
hasChild = False
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1184
|
+
sItem = self.itemAt(clickPos)
|
1185
|
+
sItems = self.selectedItems()
|
1186
|
+
if isinstance(sItem, QTreeWidgetItem):
|
1187
|
+
tHandle = sItem.data(self.C_DATA, self.D_HANDLE)
|
1206
1188
|
tItem = SHARED.project.tree[tHandle]
|
1207
|
-
hasChild =
|
1189
|
+
hasChild = sItem.childCount() > 0
|
1208
1190
|
|
1209
1191
|
if tItem is None or tHandle is None:
|
1210
1192
|
logger.debug("No item found")
|
1211
1193
|
return False
|
1212
1194
|
|
1213
|
-
ctxMenu =
|
1214
|
-
|
1215
|
-
# Trash Folder
|
1216
|
-
# ============
|
1217
|
-
|
1195
|
+
ctxMenu = _TreeContextMenu(self, tItem)
|
1218
1196
|
trashHandle = SHARED.project.tree.trashRoot
|
1219
|
-
if
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
ctxMenu.
|
1224
|
-
return True
|
1225
|
-
|
1226
|
-
# Document Actions
|
1227
|
-
# ================
|
1228
|
-
|
1229
|
-
isRoot = tItem.isRootType()
|
1230
|
-
isFolder = tItem.isFolderType()
|
1231
|
-
isFile = tItem.isFileType()
|
1232
|
-
|
1233
|
-
if isFile:
|
1234
|
-
aOpenDoc = ctxMenu.addAction(self.tr("Open Document"))
|
1235
|
-
aOpenDoc.triggered.connect(
|
1236
|
-
lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
|
1237
|
-
)
|
1238
|
-
aViewDoc = ctxMenu.addAction(self.tr("View Document"))
|
1239
|
-
aViewDoc.triggered.connect(
|
1240
|
-
lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
|
1241
|
-
)
|
1242
|
-
ctxMenu.addSeparator()
|
1243
|
-
|
1244
|
-
# Edit Item Settings
|
1245
|
-
# ==================
|
1246
|
-
|
1247
|
-
aLabel = ctxMenu.addAction(self.tr("Rename"))
|
1248
|
-
aLabel.triggered.connect(lambda: self.renameTreeItem(tHandle))
|
1249
|
-
|
1250
|
-
if isFile:
|
1251
|
-
aActive = ctxMenu.addAction(self.tr("Toggle Active"))
|
1252
|
-
aActive.triggered.connect(lambda: self._toggleItemActive(tHandle))
|
1253
|
-
|
1254
|
-
checkMark = f" ({nwUnicode.U_CHECK})"
|
1255
|
-
if tItem.isNovelLike():
|
1256
|
-
mStatus = ctxMenu.addMenu(self.tr("Set Status to ..."))
|
1257
|
-
for n, (key, entry) in enumerate(SHARED.project.data.itemStatus.items()):
|
1258
|
-
entryName = entry["name"] + (checkMark if tItem.itemStatus == key else "")
|
1259
|
-
aStatus = mStatus.addAction(entry["icon"], entryName)
|
1260
|
-
aStatus.triggered.connect(
|
1261
|
-
lambda n, key=key: self._changeItemStatus(tHandle, key)
|
1262
|
-
)
|
1263
|
-
mStatus.addSeparator()
|
1264
|
-
aManage1 = mStatus.addAction("Manage Labels ...")
|
1265
|
-
aManage1.triggered.connect(
|
1266
|
-
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_STATUS)
|
1267
|
-
)
|
1268
|
-
else:
|
1269
|
-
mImport = ctxMenu.addMenu(self.tr("Set Importance to ..."))
|
1270
|
-
for n, (key, entry) in enumerate(SHARED.project.data.itemImport.items()):
|
1271
|
-
entryName = entry["name"] + (checkMark if tItem.itemImport == key else "")
|
1272
|
-
aImport = mImport.addAction(entry["icon"], entryName)
|
1273
|
-
aImport.triggered.connect(
|
1274
|
-
lambda n, key=key: self._changeItemImport(tHandle, key)
|
1275
|
-
)
|
1276
|
-
mImport.addSeparator()
|
1277
|
-
aManage2 = mImport.addAction("Manage Labels ...")
|
1278
|
-
aManage2.triggered.connect(
|
1279
|
-
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_IMPORT)
|
1280
|
-
)
|
1281
|
-
|
1282
|
-
# Transform Item
|
1283
|
-
# ==============
|
1284
|
-
|
1285
|
-
if not isRoot:
|
1286
|
-
mTrans = ctxMenu.addMenu(self.tr("Transform"))
|
1287
|
-
|
1288
|
-
trDoc = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
|
1289
|
-
trNote = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
|
1290
|
-
|
1291
|
-
isDocFile = isFile and tItem.isDocumentLayout()
|
1292
|
-
isNoteFile = isFile and tItem.isNoteLayout()
|
1293
|
-
|
1294
|
-
if isNoteFile and tItem.documentAllowed():
|
1295
|
-
aConvert1 = mTrans.addAction(self.tr("Convert to {0}").format(trDoc))
|
1296
|
-
aConvert1.triggered.connect(
|
1297
|
-
lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT)
|
1298
|
-
)
|
1299
|
-
|
1300
|
-
if isDocFile:
|
1301
|
-
aConvert2 = mTrans.addAction(self.tr("Convert to {0}").format(trNote))
|
1302
|
-
aConvert2.triggered.connect(
|
1303
|
-
lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE)
|
1304
|
-
)
|
1305
|
-
|
1306
|
-
if isFolder and tItem.documentAllowed():
|
1307
|
-
aConvert3 = mTrans.addAction(self.tr("Convert to {0}").format(trDoc))
|
1308
|
-
aConvert3.triggered.connect(
|
1309
|
-
lambda: self._covertFolderToFile(tHandle, nwItemLayout.DOCUMENT)
|
1310
|
-
)
|
1311
|
-
|
1312
|
-
if isFolder:
|
1313
|
-
aConvert4 = mTrans.addAction(self.tr("Convert to {0}").format(trNote))
|
1314
|
-
aConvert4.triggered.connect(
|
1315
|
-
lambda: self._covertFolderToFile(tHandle, nwItemLayout.NOTE)
|
1316
|
-
)
|
1317
|
-
|
1318
|
-
if hasChild and isFile:
|
1319
|
-
aMerge1 = mTrans.addAction(self.tr("Merge Child Items into Self"))
|
1320
|
-
aMerge1.triggered.connect(lambda: self._mergeDocuments(tHandle, False))
|
1321
|
-
aMerge2 = mTrans.addAction(self.tr("Merge Child Items into New"))
|
1322
|
-
aMerge2.triggered.connect(lambda: self._mergeDocuments(tHandle, True))
|
1323
|
-
|
1324
|
-
if hasChild and isFolder:
|
1325
|
-
aMerge3 = mTrans.addAction(self.tr("Merge Documents in Folder"))
|
1326
|
-
aMerge3.triggered.connect(lambda: self._mergeDocuments(tHandle, True))
|
1327
|
-
|
1328
|
-
if isFile:
|
1329
|
-
aSplit1 = mTrans.addAction(self.tr("Split Document by Headers"))
|
1330
|
-
aSplit1.triggered.connect(lambda: self._splitDocument(tHandle))
|
1331
|
-
|
1332
|
-
# Expand/Collapse/Delete/Duplicate
|
1333
|
-
# ================================
|
1334
|
-
|
1335
|
-
ctxMenu.addSeparator()
|
1336
|
-
|
1337
|
-
if hasChild:
|
1338
|
-
aExpand = ctxMenu.addAction(self.tr("Expand All"))
|
1339
|
-
aExpand.triggered.connect(lambda: self.setExpandedFromHandle(tHandle, True))
|
1340
|
-
aCollapse = ctxMenu.addAction(self.tr("Collapse All"))
|
1341
|
-
aCollapse.triggered.connect(lambda: self.setExpandedFromHandle(tHandle, False))
|
1342
|
-
aDuplicate = ctxMenu.addAction(self.tr("Duplicate from Here"))
|
1343
|
-
aDuplicate.triggered.connect(lambda: self._duplicateFromHandle(tHandle))
|
1344
|
-
elif isFile:
|
1345
|
-
aDuplicate = ctxMenu.addAction(self.tr("Duplicate Document"))
|
1346
|
-
aDuplicate.triggered.connect(lambda: self._duplicateFromHandle(tHandle))
|
1347
|
-
|
1348
|
-
if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
|
1349
|
-
aDelete = ctxMenu.addAction(self.tr("Delete Permanently"))
|
1350
|
-
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)
|
1351
1202
|
else:
|
1352
|
-
|
1353
|
-
aMoveTrash.triggered.connect(lambda: self.moveItemToTrash(tHandle))
|
1203
|
+
ctxMenu.buildSingleSelectMenu(hasChild)
|
1354
1204
|
|
1355
|
-
# Show Context Menu
|
1356
1205
|
ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
|
1206
|
+
ctxMenu.deleteLater()
|
1357
1207
|
|
1358
1208
|
return True
|
1359
1209
|
|
@@ -1378,29 +1228,45 @@ class GuiProjectTree(QTreeWidget):
|
|
1378
1228
|
for viewing if the user middle-clicked.
|
1379
1229
|
"""
|
1380
1230
|
super().mousePressEvent(event)
|
1381
|
-
|
1382
|
-
if event.button() == Qt.LeftButton:
|
1231
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
1383
1232
|
selItem = self.indexAt(event.pos())
|
1384
1233
|
if not selItem.isValid():
|
1385
1234
|
self.clearSelection()
|
1386
|
-
|
1387
|
-
elif event.button() == Qt.MiddleButton:
|
1235
|
+
elif event.button() == Qt.MouseButton.MiddleButton:
|
1388
1236
|
selItem = self.itemAt(event.pos())
|
1389
|
-
if
|
1390
|
-
|
1391
|
-
|
1392
|
-
|
1393
|
-
|
1394
|
-
if tItem is None:
|
1395
|
-
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
|
1396
1242
|
|
1397
|
-
|
1398
|
-
|
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
|
1399
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()
|
1400
1266
|
return
|
1401
1267
|
|
1402
1268
|
def dragMoveEvent(self, event: QDragMoveEvent) -> None:
|
1403
|
-
"""Capture the drag move event to enable edge
|
1269
|
+
"""Capture the drag move event to enable edge auto scroll."""
|
1404
1270
|
y = event.pos().y()
|
1405
1271
|
if y < self._scrollMargin:
|
1406
1272
|
if not self._scrollTimer.isActive():
|
@@ -1414,38 +1280,33 @@ class GuiProjectTree(QTreeWidget):
|
|
1414
1280
|
return
|
1415
1281
|
|
1416
1282
|
def dropEvent(self, event: QDropEvent) -> None:
|
1417
|
-
"""Overload the drop item event to ensure
|
1418
|
-
|
1283
|
+
"""Overload the drop item event to ensure the drag and drop
|
1284
|
+
action is allowed, and update relevant data.
|
1419
1285
|
"""
|
1420
|
-
|
1421
|
-
|
1422
|
-
|
1423
|
-
|
1424
|
-
event.ignore()
|
1425
|
-
return
|
1426
|
-
|
1427
|
-
if not self.indexAt(event.pos()).isValid():
|
1428
|
-
# Needed due to a bug somewhere around Qt 5.15.8 that
|
1429
|
-
# 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:
|
1430
1290
|
logger.error("Invalid drop location")
|
1431
1291
|
event.ignore()
|
1432
1292
|
return
|
1433
1293
|
|
1434
|
-
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
1438
|
-
|
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)
|
1439
1301
|
|
1440
|
-
|
1441
|
-
self.propagateCount(sHandle, 0)
|
1302
|
+
super().dropEvent(event)
|
1442
1303
|
|
1443
|
-
|
1444
|
-
|
1445
|
-
|
1446
|
-
|
1304
|
+
for mHandle, (sItem, isExpanded) in mItems.items():
|
1305
|
+
self._postItemMove(mHandle)
|
1306
|
+
sItem.setExpanded(isExpanded)
|
1307
|
+
self._alertTreeChange(mHandle, flush=False)
|
1447
1308
|
|
1448
|
-
|
1309
|
+
self.saveTreeOrder()
|
1449
1310
|
|
1450
1311
|
return
|
1451
1312
|
|
@@ -1453,17 +1314,16 @@ class GuiProjectTree(QTreeWidget):
|
|
1453
1314
|
# Internal Functions
|
1454
1315
|
##
|
1455
1316
|
|
1456
|
-
def _postItemMove(self, tHandle: str
|
1317
|
+
def _postItemMove(self, tHandle: str) -> None:
|
1457
1318
|
"""Run various maintenance tasks for a moved item."""
|
1458
1319
|
trItemS = self._getTreeItem(tHandle)
|
1459
1320
|
nwItemS = SHARED.project.tree[tHandle]
|
1460
1321
|
trItemP = trItemS.parent() if trItemS else None
|
1461
1322
|
if trItemP is None or nwItemS is None:
|
1462
1323
|
logger.error("Failed to find new parent item of '%s'", tHandle)
|
1463
|
-
return
|
1324
|
+
return
|
1464
1325
|
|
1465
|
-
# Update item parent handle in the project
|
1466
|
-
# is updated accordingly, and update word count
|
1326
|
+
# Update item parent handle in the project
|
1467
1327
|
pHandle = trItemP.data(self.C_DATA, self.D_HANDLE)
|
1468
1328
|
nwItemS.setParent(pHandle)
|
1469
1329
|
trItemP.setExpanded(True)
|
@@ -1474,19 +1334,16 @@ class GuiProjectTree(QTreeWidget):
|
|
1474
1334
|
for mHandle in mHandles:
|
1475
1335
|
logger.debug("Updating item '%s'", mHandle)
|
1476
1336
|
SHARED.project.tree.updateItemData(mHandle)
|
1477
|
-
|
1478
|
-
# Update the index
|
1479
1337
|
if nwItemS.isInactiveClass():
|
1480
1338
|
SHARED.project.index.deleteHandle(mHandle)
|
1481
1339
|
else:
|
1482
1340
|
SHARED.project.index.reIndexHandle(mHandle)
|
1483
|
-
|
1484
1341
|
self.setTreeItemValues(mHandle)
|
1485
1342
|
|
1486
|
-
#
|
1487
|
-
self.propagateCount(tHandle,
|
1343
|
+
# Update word count
|
1344
|
+
self.propagateCount(tHandle, nwItemS.wordCount, countChildren=True)
|
1488
1345
|
|
1489
|
-
return
|
1346
|
+
return
|
1490
1347
|
|
1491
1348
|
def _getItemWordCount(self, tHandle: str) -> int:
|
1492
1349
|
"""Return the word count of a given item handle."""
|
@@ -1497,15 +1354,6 @@ class GuiProjectTree(QTreeWidget):
|
|
1497
1354
|
"""Return the QTreeWidgetItem of a given item handle."""
|
1498
1355
|
return self._treeMap.get(tHandle, None) if tHandle else None
|
1499
1356
|
|
1500
|
-
def _toggleItemActive(self, tHandle: str) -> None:
|
1501
|
-
"""Toggle the active status of an item."""
|
1502
|
-
tItem = SHARED.project.tree[tHandle]
|
1503
|
-
if tItem is not None:
|
1504
|
-
tItem.setActive(not tItem.isActive)
|
1505
|
-
self.setTreeItemValues(tItem.itemHandle)
|
1506
|
-
self._alertTreeChange(tHandle, flush=False)
|
1507
|
-
return
|
1508
|
-
|
1509
1357
|
def _recursiveSetExpanded(self, trItem: QTreeWidgetItem, isExpanded: bool) -> None:
|
1510
1358
|
"""Recursive function to set expanded status starting from (and
|
1511
1359
|
not including) a given item.
|
@@ -1518,60 +1366,6 @@ class GuiProjectTree(QTreeWidget):
|
|
1518
1366
|
self._recursiveSetExpanded(chItem, isExpanded)
|
1519
1367
|
return
|
1520
1368
|
|
1521
|
-
def _changeItemStatus(self, tHandle: str, tStatus: str) -> None:
|
1522
|
-
"""Set a new status value of an item."""
|
1523
|
-
tItem = SHARED.project.tree[tHandle]
|
1524
|
-
if tItem is not None:
|
1525
|
-
tItem.setStatus(tStatus)
|
1526
|
-
self.setTreeItemValues(tItem.itemHandle)
|
1527
|
-
self._alertTreeChange(tHandle, flush=False)
|
1528
|
-
return
|
1529
|
-
|
1530
|
-
def _changeItemImport(self, tHandle: str, tImport: str) -> None:
|
1531
|
-
"""Set a new importance value of an item."""
|
1532
|
-
tItem = SHARED.project.tree[tHandle]
|
1533
|
-
if tItem is not None:
|
1534
|
-
tItem.setImport(tImport)
|
1535
|
-
self.setTreeItemValues(tItem.itemHandle)
|
1536
|
-
self._alertTreeChange(tHandle, flush=False)
|
1537
|
-
return
|
1538
|
-
|
1539
|
-
def _changeItemLayout(self, tHandle: str, itemLayout: nwItemLayout) -> None:
|
1540
|
-
"""Set a new item layout value of an item."""
|
1541
|
-
tItem = SHARED.project.tree[tHandle]
|
1542
|
-
if tItem is not None:
|
1543
|
-
if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed():
|
1544
|
-
tItem.setLayout(nwItemLayout.DOCUMENT)
|
1545
|
-
self.setTreeItemValues(tHandle)
|
1546
|
-
self._alertTreeChange(tHandle, flush=False)
|
1547
|
-
elif itemLayout == nwItemLayout.NOTE:
|
1548
|
-
tItem.setLayout(nwItemLayout.NOTE)
|
1549
|
-
self.setTreeItemValues(tHandle)
|
1550
|
-
self._alertTreeChange(tHandle, flush=False)
|
1551
|
-
return
|
1552
|
-
|
1553
|
-
def _covertFolderToFile(self, tHandle: str, itemLayout: nwItemLayout) -> None:
|
1554
|
-
"""Convert a folder to a note or document."""
|
1555
|
-
tItem = SHARED.project.tree[tHandle]
|
1556
|
-
if tItem is not None and tItem.isFolderType():
|
1557
|
-
msgYes = SHARED.question(self.tr(
|
1558
|
-
"Do you want to convert the folder to a {0}? "
|
1559
|
-
"This action cannot be reversed."
|
1560
|
-
).format(trConst(nwLabels.LAYOUT_NAME[itemLayout])))
|
1561
|
-
if msgYes and itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed():
|
1562
|
-
tItem.setType(nwItemType.FILE)
|
1563
|
-
tItem.setLayout(nwItemLayout.DOCUMENT)
|
1564
|
-
self.setTreeItemValues(tHandle)
|
1565
|
-
self._alertTreeChange(tHandle, flush=False)
|
1566
|
-
elif msgYes and itemLayout == nwItemLayout.NOTE:
|
1567
|
-
tItem.setType(nwItemType.FILE)
|
1568
|
-
tItem.setLayout(nwItemLayout.NOTE)
|
1569
|
-
self.setTreeItemValues(tHandle)
|
1570
|
-
self._alertTreeChange(tHandle, flush=False)
|
1571
|
-
else:
|
1572
|
-
logger.info("Folder conversion cancelled")
|
1573
|
-
return
|
1574
|
-
|
1575
1369
|
def _mergeDocuments(self, tHandle: str, newFile: bool) -> bool:
|
1576
1370
|
"""Merge an item's child documents into a single document."""
|
1577
1371
|
logger.info("Request to merge items under handle '%s'", tHandle)
|
@@ -1591,7 +1385,7 @@ class GuiProjectTree(QTreeWidget):
|
|
1591
1385
|
dlgMerge = GuiDocMerge(self.mainGui, tHandle, itemList)
|
1592
1386
|
dlgMerge.exec_()
|
1593
1387
|
|
1594
|
-
if dlgMerge.result() == QDialog.Accepted:
|
1388
|
+
if dlgMerge.result() == QDialog.DialogCode.Accepted:
|
1595
1389
|
|
1596
1390
|
mrgData = dlgMerge.getData()
|
1597
1391
|
mrgList = mrgData.get("finalItems", [])
|
@@ -1661,7 +1455,7 @@ class GuiProjectTree(QTreeWidget):
|
|
1661
1455
|
dlgSplit = GuiDocSplit(self.mainGui, tHandle)
|
1662
1456
|
dlgSplit.exec_()
|
1663
1457
|
|
1664
|
-
if dlgSplit.result() == QDialog.Accepted:
|
1458
|
+
if dlgSplit.result() == QDialog.DialogCode.Accepted:
|
1665
1459
|
|
1666
1460
|
splitData, splitText = dlgSplit.getData()
|
1667
1461
|
|
@@ -1766,10 +1560,10 @@ class GuiProjectTree(QTreeWidget):
|
|
1766
1560
|
newItem.setText(self.C_ACTIVE, "")
|
1767
1561
|
newItem.setText(self.C_STATUS, "")
|
1768
1562
|
|
1769
|
-
newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft)
|
1770
|
-
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
1771
|
-
newItem.setTextAlignment(self.C_ACTIVE, Qt.AlignLeft)
|
1772
|
-
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)
|
1773
1567
|
|
1774
1568
|
newItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
|
1775
1569
|
newItem.setData(self.C_DATA, self.D_WORDS, 0)
|
@@ -1837,16 +1631,322 @@ class GuiProjectTree(QTreeWidget):
|
|
1837
1631
|
|
1838
1632
|
return
|
1839
1633
|
|
1840
|
-
|
1841
|
-
|
1842
|
-
|
1843
|
-
|
1844
|
-
|
1845
|
-
|
1846
|
-
|
1847
|
-
|
1848
|
-
|
1849
|
-
|
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
|
+
|
1850
1651
|
return
|
1851
1652
|
|
1852
|
-
#
|
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
|