novelWriter 2.3.1__py3-none-any.whl → 2.4__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.3.1.dist-info → novelWriter-2.4.dist-info}/METADATA +5 -6
- {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/RECORD +114 -107
- novelwriter/__init__.py +17 -10
- novelwriter/assets/i18n/nw_de_DE.qm +0 -0
- novelwriter/assets/i18n/nw_en_US.qm +0 -0
- novelwriter/assets/i18n/nw_es_419.qm +0 -0
- novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
- novelwriter/assets/i18n/nw_it_IT.qm +0 -0
- novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
- novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
- novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
- novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
- novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
- novelwriter/assets/icons/none.svg +4 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
- novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
- novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
- novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
- novelwriter/assets/icons/typicons_dark/typ_unfold-hidden.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_unfold-visible.svg +4 -0
- novelwriter/assets/icons/typicons_light/icons.conf +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
- novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
- novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
- novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
- novelwriter/assets/icons/typicons_light/typ_unfold-hidden.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_unfold-visible.svg +4 -0
- 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 +25 -23
- novelwriter/common.py +12 -4
- novelwriter/config.py +47 -16
- novelwriter/constants.py +5 -6
- novelwriter/core/buildsettings.py +64 -44
- novelwriter/core/coretools.py +97 -13
- novelwriter/core/docbuild.py +74 -7
- novelwriter/core/document.py +24 -3
- novelwriter/core/index.py +31 -112
- novelwriter/core/project.py +10 -15
- novelwriter/core/projectxml.py +1 -1
- novelwriter/core/sessions.py +2 -2
- novelwriter/core/spellcheck.py +3 -3
- novelwriter/core/status.py +6 -5
- novelwriter/core/storage.py +8 -2
- novelwriter/core/tohtml.py +22 -25
- novelwriter/core/tokenizer.py +417 -233
- novelwriter/core/tomd.py +17 -8
- novelwriter/core/toodt.py +386 -351
- novelwriter/core/tree.py +8 -8
- novelwriter/dialogs/about.py +9 -11
- novelwriter/dialogs/docmerge.py +17 -14
- novelwriter/dialogs/docsplit.py +20 -19
- novelwriter/dialogs/editlabel.py +5 -4
- novelwriter/dialogs/preferences.py +32 -40
- novelwriter/dialogs/projectsettings.py +31 -28
- novelwriter/dialogs/quotes.py +10 -9
- novelwriter/dialogs/wordlist.py +17 -14
- novelwriter/enum.py +17 -14
- novelwriter/error.py +14 -12
- novelwriter/extensions/circularprogress.py +12 -8
- novelwriter/extensions/configlayout.py +1 -3
- novelwriter/extensions/modified.py +51 -2
- novelwriter/extensions/pagedsidebar.py +16 -14
- novelwriter/extensions/simpleprogress.py +3 -1
- novelwriter/extensions/statusled.py +3 -1
- novelwriter/extensions/switch.py +10 -9
- novelwriter/extensions/switchbox.py +14 -13
- novelwriter/extensions/versioninfo.py +1 -1
- novelwriter/gui/doceditor.py +433 -496
- novelwriter/gui/dochighlight.py +54 -33
- novelwriter/gui/docviewer.py +162 -175
- novelwriter/gui/docviewerpanel.py +20 -37
- novelwriter/gui/editordocument.py +15 -4
- novelwriter/gui/itemdetails.py +51 -54
- novelwriter/gui/mainmenu.py +37 -16
- novelwriter/gui/noveltree.py +31 -37
- novelwriter/gui/outline.py +120 -98
- novelwriter/gui/projtree.py +61 -67
- novelwriter/gui/search.py +362 -0
- novelwriter/gui/sidebar.py +36 -45
- novelwriter/gui/statusbar.py +14 -14
- novelwriter/gui/theme.py +107 -32
- novelwriter/guimain.py +209 -202
- novelwriter/shared.py +31 -6
- novelwriter/text/counting.py +138 -0
- novelwriter/tools/dictionaries.py +15 -14
- novelwriter/tools/lipsum.py +20 -17
- novelwriter/tools/manusbuild.py +43 -35
- novelwriter/tools/manuscript.py +381 -104
- novelwriter/tools/manussettings.py +262 -125
- novelwriter/tools/noveldetails.py +20 -18
- novelwriter/tools/welcome.py +52 -49
- novelwriter/tools/writingstats.py +61 -55
- novelwriter/types.py +90 -0
- novelwriter/core/__init__.py +0 -3
- novelwriter/dialogs/__init__.py +0 -3
- novelwriter/extensions/__init__.py +0 -3
- novelwriter/gui/__init__.py +0 -3
- novelwriter/tools/__init__.py +0 -3
- {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/WHEEL +0 -0
- {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/top_level.txt +0 -0
novelwriter/gui/projtree.py
CHANGED
@@ -32,26 +32,29 @@ from enum import Enum
|
|
32
32
|
from time import time
|
33
33
|
from typing import TYPE_CHECKING
|
34
34
|
|
35
|
+
from PyQt5.QtCore import QPoint, QTimer, Qt, pyqtSignal, pyqtSlot
|
35
36
|
from PyQt5.QtGui import (
|
36
37
|
QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon, QMouseEvent, QPalette
|
37
38
|
)
|
38
|
-
from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot
|
39
39
|
from PyQt5.QtWidgets import (
|
40
40
|
QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView,
|
41
|
-
QLabel, QMenu, QShortcut, QSizePolicy,
|
42
|
-
|
41
|
+
QLabel, QMenu, QShortcut, QSizePolicy, QTreeWidget, QTreeWidgetItem,
|
42
|
+
QVBoxLayout, QWidget
|
43
43
|
)
|
44
44
|
|
45
45
|
from novelwriter import CONFIG, SHARED
|
46
|
-
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout
|
47
46
|
from novelwriter.common import minmax
|
48
47
|
from novelwriter.constants import nwHeaders, nwUnicode, trConst, nwLabels
|
49
|
-
from novelwriter.core.item import NWItem
|
50
48
|
from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
|
49
|
+
from novelwriter.core.item import NWItem
|
51
50
|
from novelwriter.dialogs.docmerge import GuiDocMerge
|
52
51
|
from novelwriter.dialogs.docsplit import GuiDocSplit
|
53
52
|
from novelwriter.dialogs.editlabel import GuiEditLabel
|
54
53
|
from novelwriter.dialogs.projectsettings import GuiProjectSettings
|
54
|
+
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout
|
55
|
+
from novelwriter.extensions.modified import NIconToolButton
|
56
|
+
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
|
57
|
+
from novelwriter.types import QtAlignLeft, QtAlignRight, QtMouseLeft, QtMouseMiddle, QtUserRole
|
55
58
|
|
56
59
|
if TYPE_CHECKING: # pragma: no cover
|
57
60
|
from novelwriter.guimain import GuiMain
|
@@ -142,7 +145,6 @@ class GuiProjectView(QWidget):
|
|
142
145
|
self.requestDeleteItem = self.projTree.requestDeleteItem
|
143
146
|
self.getSelectedHandle = self.projTree.getSelectedHandle
|
144
147
|
self.changedSince = self.projTree.changedSince
|
145
|
-
self.createNewNote = self.projTree.createNewNote
|
146
148
|
|
147
149
|
return
|
148
150
|
|
@@ -240,6 +242,12 @@ class GuiProjectView(QWidget):
|
|
240
242
|
self.projBar.buildQuickLinksMenu()
|
241
243
|
return
|
242
244
|
|
245
|
+
@pyqtSlot(str, nwItemClass)
|
246
|
+
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
|
247
|
+
"""Process new not request."""
|
248
|
+
self.projTree.createNewNote(tag, itemClass)
|
249
|
+
return
|
250
|
+
|
243
251
|
# END Class GuiProjectView
|
244
252
|
|
245
253
|
|
@@ -256,36 +264,33 @@ class GuiProjectToolBar(QWidget):
|
|
256
264
|
self.projTree = projView.projTree
|
257
265
|
self.mainGui = projView.mainGui
|
258
266
|
|
259
|
-
|
267
|
+
iSz = SHARED.theme.baseIconSize
|
260
268
|
mPx = CONFIG.pxInt(2)
|
261
269
|
|
262
270
|
self.setContentsMargins(0, 0, 0, 0)
|
263
271
|
self.setAutoFillBackground(True)
|
264
272
|
|
265
273
|
# Widget Label
|
266
|
-
self.viewLabel = QLabel(
|
274
|
+
self.viewLabel = QLabel(self.tr("Project Content"), self)
|
275
|
+
self.viewLabel.setFont(SHARED.theme.guiFontB)
|
267
276
|
self.viewLabel.setContentsMargins(0, 0, 0, 0)
|
268
277
|
self.viewLabel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
269
278
|
|
270
279
|
# Quick Links
|
271
280
|
self.mQuick = QMenu(self)
|
272
281
|
|
273
|
-
self.tbQuick =
|
282
|
+
self.tbQuick = NIconToolButton(self, iSz)
|
274
283
|
self.tbQuick.setToolTip("%s [Ctrl+L]" % self.tr("Quick Links"))
|
275
284
|
self.tbQuick.setShortcut("Ctrl+L")
|
276
|
-
self.tbQuick.setIconSize(QSize(iPx, iPx))
|
277
285
|
self.tbQuick.setMenu(self.mQuick)
|
278
|
-
self.tbQuick.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
279
286
|
|
280
287
|
# Move Buttons
|
281
|
-
self.tbMoveU =
|
288
|
+
self.tbMoveU = NIconToolButton(self, iSz)
|
282
289
|
self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up"))
|
283
|
-
self.tbMoveU.setIconSize(QSize(iPx, iPx))
|
284
290
|
self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1))
|
285
291
|
|
286
|
-
self.tbMoveD =
|
292
|
+
self.tbMoveD = NIconToolButton(self, iSz)
|
287
293
|
self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down"))
|
288
|
-
self.tbMoveD.setIconSize(QSize(iPx, iPx))
|
289
294
|
self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1))
|
290
295
|
|
291
296
|
# Add Item Menu
|
@@ -324,12 +329,10 @@ class GuiProjectToolBar(QWidget):
|
|
324
329
|
self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"]))
|
325
330
|
self._buildRootMenu()
|
326
331
|
|
327
|
-
self.tbAdd =
|
332
|
+
self.tbAdd = NIconToolButton(self, iSz)
|
328
333
|
self.tbAdd.setToolTip("%s [Ctrl+N]" % self.tr("Add Item"))
|
329
334
|
self.tbAdd.setShortcut("Ctrl+N")
|
330
|
-
self.tbAdd.setIconSize(QSize(iPx, iPx))
|
331
335
|
self.tbAdd.setMenu(self.mAdd)
|
332
|
-
self.tbAdd.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
333
336
|
|
334
337
|
# More Options Menu
|
335
338
|
self.mMore = QMenu(self)
|
@@ -343,11 +346,9 @@ class GuiProjectToolBar(QWidget):
|
|
343
346
|
self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
|
344
347
|
self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash())
|
345
348
|
|
346
|
-
self.tbMore =
|
349
|
+
self.tbMore = NIconToolButton(self, iSz)
|
347
350
|
self.tbMore.setToolTip(self.tr("More Options"))
|
348
|
-
self.tbMore.setIconSize(QSize(iPx, iPx))
|
349
351
|
self.tbMore.setMenu(self.mMore)
|
350
|
-
self.tbMore.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
351
352
|
|
352
353
|
# Assemble
|
353
354
|
self.outerBox = QHBoxLayout()
|
@@ -377,29 +378,24 @@ class GuiProjectToolBar(QWidget):
|
|
377
378
|
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
|
378
379
|
self.setPalette(qPalette)
|
379
380
|
|
380
|
-
|
381
|
-
buttonStyle
|
382
|
-
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
|
383
|
-
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
|
384
|
-
).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
|
385
|
-
buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
|
386
|
-
|
387
|
-
self.tbQuick.setStyleSheet(buttonStyleMenu)
|
381
|
+
buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
|
382
|
+
self.tbQuick.setStyleSheet(buttonStyle)
|
388
383
|
self.tbMoveU.setStyleSheet(buttonStyle)
|
389
384
|
self.tbMoveD.setStyleSheet(buttonStyle)
|
390
|
-
self.tbAdd.setStyleSheet(
|
391
|
-
self.tbMore.setStyleSheet(
|
385
|
+
self.tbAdd.setStyleSheet(buttonStyle)
|
386
|
+
self.tbMore.setStyleSheet(buttonStyle)
|
387
|
+
|
388
|
+
self.tbQuick.setThemeIcon("bookmark")
|
389
|
+
self.tbMoveU.setThemeIcon("up")
|
390
|
+
self.tbMoveD.setThemeIcon("down")
|
391
|
+
self.tbAdd.setThemeIcon("add")
|
392
|
+
self.tbMore.setThemeIcon("menu")
|
392
393
|
|
393
|
-
self.tbQuick.setIcon(SHARED.theme.getIcon("bookmark"))
|
394
|
-
self.tbMoveU.setIcon(SHARED.theme.getIcon("up"))
|
395
|
-
self.tbMoveD.setIcon(SHARED.theme.getIcon("down"))
|
396
394
|
self.aAddEmpty.setIcon(SHARED.theme.getIcon("proj_document"))
|
397
395
|
self.aAddChap.setIcon(SHARED.theme.getIcon("proj_chapter"))
|
398
396
|
self.aAddScene.setIcon(SHARED.theme.getIcon("proj_scene"))
|
399
397
|
self.aAddNote.setIcon(SHARED.theme.getIcon("proj_note"))
|
400
398
|
self.aAddFolder.setIcon(SHARED.theme.getIcon("proj_folder"))
|
401
|
-
self.tbAdd.setIcon(SHARED.theme.getIcon("add"))
|
402
|
-
self.tbMore.setIcon(SHARED.theme.getIcon("menu"))
|
403
399
|
|
404
400
|
self.buildQuickLinksMenu()
|
405
401
|
self._buildRootMenu()
|
@@ -457,7 +453,7 @@ class GuiProjectToolBar(QWidget):
|
|
457
453
|
|
458
454
|
def _buildRootMenu(self) -> None:
|
459
455
|
"""Build the rood folder menu."""
|
460
|
-
def addClass(itemClass):
|
456
|
+
def addClass(itemClass: nwItemClass) -> None:
|
461
457
|
aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass]))
|
462
458
|
aNew.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass]))
|
463
459
|
aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass))
|
@@ -491,8 +487,8 @@ class GuiProjectTree(QTreeWidget):
|
|
491
487
|
C_ACTIVE = 2
|
492
488
|
C_STATUS = 3
|
493
489
|
|
494
|
-
D_HANDLE =
|
495
|
-
D_WORDS =
|
490
|
+
D_HANDLE = QtUserRole
|
491
|
+
D_WORDS = QtUserRole + 1
|
496
492
|
|
497
493
|
itemRefreshed = pyqtSignal(str, NWItem, QIcon)
|
498
494
|
|
@@ -522,10 +518,10 @@ class GuiProjectTree(QTreeWidget):
|
|
522
518
|
self.customContextMenuRequested.connect(self._openContextMenu)
|
523
519
|
|
524
520
|
# Tree Settings
|
525
|
-
iPx = SHARED.theme.
|
521
|
+
iPx = SHARED.theme.baseIconHeight
|
526
522
|
cMg = CONFIG.pxInt(6)
|
527
523
|
|
528
|
-
self.setIconSize(
|
524
|
+
self.setIconSize(SHARED.theme.baseIconSize)
|
529
525
|
self.setFrameStyle(QFrame.Shape.NoFrame)
|
530
526
|
self.setUniformRowHeights(True)
|
531
527
|
self.setAllColumnsShowFocus(True)
|
@@ -569,9 +565,9 @@ class GuiProjectTree(QTreeWidget):
|
|
569
565
|
self.itemSelectionChanged.connect(self._treeSelectionChange)
|
570
566
|
|
571
567
|
# Auto Scroll
|
572
|
-
self._scrollMargin = SHARED.theme.
|
568
|
+
self._scrollMargin = SHARED.theme.baseIconHeight
|
573
569
|
self._scrollDirection = 0
|
574
|
-
self._scrollTimer = QTimer()
|
570
|
+
self._scrollTimer = QTimer(self)
|
575
571
|
self._scrollTimer.timeout.connect(self._doAutoScroll)
|
576
572
|
self._scrollTimer.setInterval(250)
|
577
573
|
|
@@ -606,18 +602,18 @@ class GuiProjectTree(QTreeWidget):
|
|
606
602
|
self._timeChanged = 0.0
|
607
603
|
return
|
608
604
|
|
609
|
-
def createNewNote(self, tag: str, itemClass: nwItemClass
|
605
|
+
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
|
610
606
|
"""Create a new note. This function is used by the document
|
611
607
|
editor to create note files for unknown tags.
|
612
608
|
"""
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
609
|
+
if itemClass != nwItemClass.NO_CLASS:
|
610
|
+
if not (rHandle := SHARED.project.tree.findRoot(itemClass)):
|
611
|
+
self.newTreeItem(nwItemType.ROOT, itemClass)
|
612
|
+
rHandle = SHARED.project.tree.findRoot(itemClass)
|
613
|
+
if rHandle and (tHandle := SHARED.project.newFile(tag, rHandle)):
|
617
614
|
SHARED.project.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n")
|
618
615
|
self.revealNewTreeItem(tHandle, wordCount=True)
|
619
|
-
|
620
|
-
return False
|
616
|
+
return
|
621
617
|
|
622
618
|
def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
|
623
619
|
hLevel: int = 1, isNote: bool = False, copyDoc: str | None = None) -> bool:
|
@@ -1234,7 +1230,7 @@ class GuiProjectTree(QTreeWidget):
|
|
1234
1230
|
else:
|
1235
1231
|
ctxMenu.buildSingleSelectMenu(hasChild)
|
1236
1232
|
|
1237
|
-
ctxMenu.
|
1233
|
+
ctxMenu.exec(self.viewport().mapToGlobal(clickPos))
|
1238
1234
|
ctxMenu.deleteLater()
|
1239
1235
|
|
1240
1236
|
return True
|
@@ -1260,11 +1256,11 @@ class GuiProjectTree(QTreeWidget):
|
|
1260
1256
|
for viewing if the user middle-clicked.
|
1261
1257
|
"""
|
1262
1258
|
super().mousePressEvent(event)
|
1263
|
-
if event.button() ==
|
1259
|
+
if event.button() == QtMouseLeft:
|
1264
1260
|
selItem = self.indexAt(event.pos())
|
1265
1261
|
if not selItem.isValid():
|
1266
1262
|
self.clearSelection()
|
1267
|
-
elif event.button() ==
|
1263
|
+
elif event.button() == QtMouseMiddle:
|
1268
1264
|
selItem = self.itemAt(event.pos())
|
1269
1265
|
if selItem:
|
1270
1266
|
tHandle = selItem.data(self.C_DATA, self.D_HANDLE)
|
@@ -1272,7 +1268,7 @@ class GuiProjectTree(QTreeWidget):
|
|
1272
1268
|
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
|
1273
1269
|
return
|
1274
1270
|
|
1275
|
-
def startDrag(self, dropAction: Qt.
|
1271
|
+
def startDrag(self, dropAction: Qt.DropAction) -> None:
|
1276
1272
|
"""Capture the drag and drop handling to pop alerts."""
|
1277
1273
|
super().startDrag(dropAction)
|
1278
1274
|
if self._popAlert:
|
@@ -1414,7 +1410,7 @@ class GuiProjectTree(QTreeWidget):
|
|
1414
1410
|
itemList.remove(tHandle)
|
1415
1411
|
|
1416
1412
|
dlgMerge = GuiDocMerge(self.mainGui, tHandle, itemList)
|
1417
|
-
dlgMerge.
|
1413
|
+
dlgMerge.exec()
|
1418
1414
|
|
1419
1415
|
if dlgMerge.result() == QDialog.DialogCode.Accepted:
|
1420
1416
|
|
@@ -1484,7 +1480,7 @@ class GuiProjectTree(QTreeWidget):
|
|
1484
1480
|
return False
|
1485
1481
|
|
1486
1482
|
dlgSplit = GuiDocSplit(self.mainGui, tHandle)
|
1487
|
-
dlgSplit.
|
1483
|
+
dlgSplit.exec()
|
1488
1484
|
|
1489
1485
|
if dlgSplit.result() == QDialog.DialogCode.Accepted:
|
1490
1486
|
|
@@ -1591,10 +1587,10 @@ class GuiProjectTree(QTreeWidget):
|
|
1591
1587
|
newItem.setText(self.C_ACTIVE, "")
|
1592
1588
|
newItem.setText(self.C_STATUS, "")
|
1593
1589
|
|
1594
|
-
newItem.setTextAlignment(self.C_NAME,
|
1595
|
-
newItem.setTextAlignment(self.C_COUNT,
|
1596
|
-
newItem.setTextAlignment(self.C_ACTIVE,
|
1597
|
-
newItem.setTextAlignment(self.C_STATUS,
|
1590
|
+
newItem.setTextAlignment(self.C_NAME, QtAlignLeft)
|
1591
|
+
newItem.setTextAlignment(self.C_COUNT, QtAlignRight)
|
1592
|
+
newItem.setTextAlignment(self.C_ACTIVE, QtAlignLeft)
|
1593
|
+
newItem.setTextAlignment(self.C_STATUS, QtAlignLeft)
|
1598
1594
|
|
1599
1595
|
newItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
|
1600
1596
|
newItem.setData(self.C_DATA, self.D_WORDS, 0)
|
@@ -1834,7 +1830,7 @@ class _TreeContextMenu(QMenu):
|
|
1834
1830
|
|
1835
1831
|
def _itemHeader(self) -> None:
|
1836
1832
|
"""Check if there is a header that can be used for rename."""
|
1837
|
-
if hItem := SHARED.project.index.
|
1833
|
+
if hItem := SHARED.project.index.getItemHeading(self._handle, "T0001"):
|
1838
1834
|
action = self.addAction(self.tr("Rename to Heading"))
|
1839
1835
|
action.triggered.connect(
|
1840
1836
|
lambda: self.projTree.renameTreeItem(self._handle, hItem.title)
|
@@ -1933,7 +1929,7 @@ class _TreeContextMenu(QMenu):
|
|
1933
1929
|
action.triggered.connect(lambda: tree._mergeDocuments(tHandle, True))
|
1934
1930
|
|
1935
1931
|
if isFile:
|
1936
|
-
action = menu.addAction(self.tr("Split Document by
|
1932
|
+
action = menu.addAction(self.tr("Split Document by Headings"))
|
1937
1933
|
action.triggered.connect(lambda: tree._splitDocument(tHandle))
|
1938
1934
|
|
1939
1935
|
return
|
@@ -1947,11 +1943,9 @@ class _TreeContextMenu(QMenu):
|
|
1947
1943
|
action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, True))
|
1948
1944
|
action = self.addAction(self.tr("Collapse All"))
|
1949
1945
|
action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, False))
|
1950
|
-
|
1951
|
-
|
1952
|
-
|
1953
|
-
action = self.addAction(self.tr("Duplicate Document"))
|
1954
|
-
action.triggered.connect(lambda: tree._duplicateFromHandle(tHandle))
|
1946
|
+
|
1947
|
+
action = self.addAction(self.tr("Duplicate"))
|
1948
|
+
action.triggered.connect(lambda: tree._duplicateFromHandle(tHandle))
|
1955
1949
|
|
1956
1950
|
if self._item.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
|
1957
1951
|
action = self.addAction(self.tr("Delete Permanently"))
|
@@ -0,0 +1,362 @@
|
|
1
|
+
"""
|
2
|
+
novelWriter – GUI Project Search
|
3
|
+
================================
|
4
|
+
|
5
|
+
File History:
|
6
|
+
Created: 2024-03-21 [2.4b1] GuiProjectSearch
|
7
|
+
|
8
|
+
This file is a part of novelWriter
|
9
|
+
Copyright 2018–2024, Veronica Berglyd Olsen
|
10
|
+
|
11
|
+
This program is free software: you can redistribute it and/or modify
|
12
|
+
it under the terms of the GNU General Public License as published by
|
13
|
+
the Free Software Foundation, either version 3 of the License, or
|
14
|
+
(at your option) any later version.
|
15
|
+
|
16
|
+
This program is distributed in the hope that it will be useful, but
|
17
|
+
WITHOUT ANY WARRANTY; without even the implied warranty of
|
18
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
19
|
+
General Public License for more details.
|
20
|
+
|
21
|
+
You should have received a copy of the GNU General Public License
|
22
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
23
|
+
"""
|
24
|
+
from __future__ import annotations
|
25
|
+
|
26
|
+
import logging
|
27
|
+
|
28
|
+
from time import time
|
29
|
+
|
30
|
+
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
|
31
|
+
from PyQt5.QtGui import QCursor, QKeyEvent
|
32
|
+
from PyQt5.QtWidgets import (
|
33
|
+
QApplication, QFrame, QHBoxLayout, QHeaderView, QLabel, QLineEdit,
|
34
|
+
QToolBar, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
|
35
|
+
)
|
36
|
+
|
37
|
+
from novelwriter import CONFIG, SHARED
|
38
|
+
from novelwriter.common import checkInt, cssCol
|
39
|
+
from novelwriter.core.coretools import DocSearch
|
40
|
+
from novelwriter.core.item import NWItem
|
41
|
+
from novelwriter.types import QtAlignMiddle, QtAlignRight, QtUserRole
|
42
|
+
|
43
|
+
logger = logging.getLogger(__name__)
|
44
|
+
|
45
|
+
|
46
|
+
class GuiProjectSearch(QWidget):
|
47
|
+
|
48
|
+
C_NAME = 0
|
49
|
+
C_RESULT = 0
|
50
|
+
C_COUNT = 1
|
51
|
+
|
52
|
+
D_HANDLE = QtUserRole
|
53
|
+
D_RESULT = QtUserRole + 1
|
54
|
+
|
55
|
+
selectedItemChanged = pyqtSignal(str)
|
56
|
+
openDocumentSelectRequest = pyqtSignal(str, int, int, bool)
|
57
|
+
|
58
|
+
def __init__(self, parent: QWidget) -> None:
|
59
|
+
super().__init__(parent=parent)
|
60
|
+
|
61
|
+
logger.debug("Create: GuiProjectSearch")
|
62
|
+
|
63
|
+
iPx = SHARED.theme.baseIconHeight
|
64
|
+
iSz = SHARED.theme.baseIconSize
|
65
|
+
mPx = CONFIG.pxInt(2)
|
66
|
+
tPx = CONFIG.pxInt(4)
|
67
|
+
|
68
|
+
self._time = time()
|
69
|
+
self._search = DocSearch()
|
70
|
+
self._blocked = False
|
71
|
+
self._map: dict[str, tuple[int, float]] = {}
|
72
|
+
|
73
|
+
# Header
|
74
|
+
self.viewLabel = QLabel(self.tr("Project Search"), self)
|
75
|
+
self.viewLabel.setFont(SHARED.theme.guiFontB)
|
76
|
+
self.viewLabel.setContentsMargins(mPx, tPx, 0, mPx)
|
77
|
+
|
78
|
+
# Options
|
79
|
+
self.searchOpt = QToolBar(self)
|
80
|
+
self.searchOpt.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
|
81
|
+
self.searchOpt.setIconSize(iSz)
|
82
|
+
self.searchOpt.setContentsMargins(0, 0, 0, 0)
|
83
|
+
|
84
|
+
self.toggleCase = self.searchOpt.addAction(self.tr("Case Sensitive"))
|
85
|
+
self.toggleCase.setCheckable(True)
|
86
|
+
self.toggleCase.setChecked(CONFIG.searchProjCase)
|
87
|
+
self.toggleCase.toggled.connect(self._toggleCase)
|
88
|
+
|
89
|
+
self.toggleWord = self.searchOpt.addAction(self.tr("Whole Words Only"))
|
90
|
+
self.toggleWord.setCheckable(True)
|
91
|
+
self.toggleWord.setChecked(CONFIG.searchProjWord)
|
92
|
+
self.toggleWord.toggled.connect(self._toggleWord)
|
93
|
+
|
94
|
+
self.toggleRegEx = self.searchOpt.addAction(self.tr("RegEx Mode"))
|
95
|
+
self.toggleRegEx.setCheckable(True)
|
96
|
+
self.toggleRegEx.setChecked(CONFIG.searchProjRegEx)
|
97
|
+
self.toggleRegEx.toggled.connect(self._toggleRegEx)
|
98
|
+
|
99
|
+
# Search Box
|
100
|
+
self.searchText = QLineEdit(self)
|
101
|
+
self.searchText.setPlaceholderText(self.tr("Search for"))
|
102
|
+
self.searchText.setClearButtonEnabled(True)
|
103
|
+
|
104
|
+
self.searchAction = self.searchText.addAction(
|
105
|
+
SHARED.theme.getIcon("search"), QLineEdit.ActionPosition.TrailingPosition
|
106
|
+
)
|
107
|
+
self.searchAction.triggered.connect(self._processSearch)
|
108
|
+
|
109
|
+
# Search Result
|
110
|
+
self.searchResult = QTreeWidget(self)
|
111
|
+
self.searchResult.setHeaderHidden(True)
|
112
|
+
self.searchResult.setColumnCount(2)
|
113
|
+
self.searchResult.setIconSize(iSz)
|
114
|
+
self.searchResult.setIndentation(iPx)
|
115
|
+
self.searchResult.setFrameStyle(QFrame.Shape.NoFrame)
|
116
|
+
self.searchResult.setUniformRowHeights(True)
|
117
|
+
self.searchResult.setAllColumnsShowFocus(True)
|
118
|
+
self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked)
|
119
|
+
self.searchResult.itemSelectionChanged.connect(self._searchResultSelected)
|
120
|
+
|
121
|
+
treeHeader = self.searchResult.header()
|
122
|
+
treeHeader.setStretchLastSection(False)
|
123
|
+
treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.Stretch)
|
124
|
+
treeHeader.setSectionResizeMode(self.C_COUNT, QHeaderView.ResizeMode.ResizeToContents)
|
125
|
+
|
126
|
+
# Assemble
|
127
|
+
self.headerBox = QHBoxLayout()
|
128
|
+
self.headerBox.addWidget(self.viewLabel, 1)
|
129
|
+
self.headerBox.addWidget(self.searchOpt, 0, QtAlignMiddle)
|
130
|
+
self.headerBox.setContentsMargins(0, 0, 0, 0)
|
131
|
+
self.headerBox.setSpacing(0)
|
132
|
+
|
133
|
+
self.headerWidget = QWidget(self)
|
134
|
+
self.headerWidget.setLayout(self.headerBox)
|
135
|
+
self.headerWidget.setContentsMargins(0, 0, 0, 0)
|
136
|
+
|
137
|
+
self.outerBox = QVBoxLayout()
|
138
|
+
self.outerBox.addWidget(self.headerWidget, 0)
|
139
|
+
self.outerBox.addWidget(self.searchText, 0)
|
140
|
+
self.outerBox.addWidget(self.searchResult, 1)
|
141
|
+
self.outerBox.setContentsMargins(0, 0, 0, 0)
|
142
|
+
self.outerBox.setSpacing(mPx)
|
143
|
+
|
144
|
+
self.setLayout(self.outerBox)
|
145
|
+
self.updateTheme()
|
146
|
+
|
147
|
+
logger.debug("Ready: GuiProjectSearch")
|
148
|
+
|
149
|
+
return
|
150
|
+
|
151
|
+
##
|
152
|
+
# Methods
|
153
|
+
##
|
154
|
+
|
155
|
+
def updateTheme(self) -> None:
|
156
|
+
"""Update theme elements."""
|
157
|
+
bPx = CONFIG.pxInt(1)
|
158
|
+
mPx = CONFIG.pxInt(2)
|
159
|
+
|
160
|
+
qPalette = self.palette()
|
161
|
+
colBase = cssCol(qPalette.base().color())
|
162
|
+
colFocus = cssCol(qPalette.highlight().color())
|
163
|
+
|
164
|
+
self.headerWidget.setStyleSheet(f"QWidget {{background: {colBase};}}")
|
165
|
+
self.headerWidget.setAutoFillBackground(True)
|
166
|
+
|
167
|
+
self.setStyleSheet(
|
168
|
+
"QToolBar {padding: 0; background: none;} "
|
169
|
+
f"QLineEdit {{border: {bPx}px solid {colBase}; padding: {mPx}px;}} "
|
170
|
+
f"QLineEdit:focus {{border: {bPx}px solid {colFocus};}} "
|
171
|
+
)
|
172
|
+
|
173
|
+
self.searchAction.setIcon(SHARED.theme.getIcon("search"))
|
174
|
+
self.toggleCase.setIcon(SHARED.theme.getIcon("search_case"))
|
175
|
+
self.toggleWord.setIcon(SHARED.theme.getIcon("search_word"))
|
176
|
+
self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex"))
|
177
|
+
|
178
|
+
return
|
179
|
+
|
180
|
+
def processReturn(self) -> None:
|
181
|
+
"""Process a return keypress forwarded from the main GUI."""
|
182
|
+
if self.searchText.hasFocus():
|
183
|
+
self._processSearch()
|
184
|
+
elif (
|
185
|
+
self.searchResult.hasFocus()
|
186
|
+
and (items := self.searchResult.selectedItems())
|
187
|
+
and (data := items[0].data(0, self.D_RESULT))
|
188
|
+
and len(data) == 3
|
189
|
+
):
|
190
|
+
self.openDocumentSelectRequest.emit(
|
191
|
+
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False
|
192
|
+
)
|
193
|
+
return
|
194
|
+
|
195
|
+
def beginSearch(self, text: str = "") -> None:
|
196
|
+
"""Focus the search box and select its text, if any."""
|
197
|
+
self.searchText.setFocus()
|
198
|
+
self.searchText.selectAll()
|
199
|
+
if text:
|
200
|
+
self.searchText.setText(text.partition("\n")[0])
|
201
|
+
self.searchText.selectAll()
|
202
|
+
return
|
203
|
+
|
204
|
+
def closeProjectTasks(self) -> None:
|
205
|
+
"""Run close project tasks."""
|
206
|
+
self._map = {}
|
207
|
+
self.searchText.clear()
|
208
|
+
self.searchResult.clear()
|
209
|
+
return
|
210
|
+
|
211
|
+
##
|
212
|
+
# Events
|
213
|
+
##
|
214
|
+
|
215
|
+
def keyPressEvent(self, event: QKeyEvent) -> None:
|
216
|
+
"""Process key press events. This handles up and down arrow key
|
217
|
+
presses to jump between search text box and result tree.
|
218
|
+
"""
|
219
|
+
if (
|
220
|
+
event.key() == Qt.Key.Key_Down
|
221
|
+
and self.searchText.hasFocus()
|
222
|
+
and (first := self.searchResult.topLevelItem(0))
|
223
|
+
):
|
224
|
+
first.setSelected(True)
|
225
|
+
self.searchResult.setFocus()
|
226
|
+
elif (
|
227
|
+
event.key() == Qt.Key.Key_Up
|
228
|
+
and self.searchResult.hasFocus()
|
229
|
+
and (first := self.searchResult.topLevelItem(0))
|
230
|
+
and first.isSelected()
|
231
|
+
):
|
232
|
+
first.setSelected(False)
|
233
|
+
self.searchText.setFocus()
|
234
|
+
else:
|
235
|
+
super().keyPressEvent(event)
|
236
|
+
return
|
237
|
+
|
238
|
+
##
|
239
|
+
# Public Slots
|
240
|
+
##
|
241
|
+
|
242
|
+
@pyqtSlot(str, float)
|
243
|
+
def textChanged(self, tHandle: str, timeStamp: float) -> None:
|
244
|
+
"""Update search result for a specific document."""
|
245
|
+
if (entry := self._map.get(tHandle)) and timeStamp > entry[1]:
|
246
|
+
start = time()
|
247
|
+
results, capped = self._search.searchText(SHARED.mainGui.docEditor.getText())
|
248
|
+
self._displayResultSet(SHARED.project.tree[tHandle], results, capped)
|
249
|
+
logger.debug("Updated search for '%s' in %.3f ms", tHandle, 1000*(time() - start))
|
250
|
+
return
|
251
|
+
|
252
|
+
##
|
253
|
+
# Private Slots
|
254
|
+
##
|
255
|
+
|
256
|
+
@pyqtSlot()
|
257
|
+
def _processSearch(self) -> None:
|
258
|
+
"""Perform a search."""
|
259
|
+
if not self._blocked:
|
260
|
+
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
|
261
|
+
start = time()
|
262
|
+
SHARED.mainGui.saveDocument()
|
263
|
+
self._blocked = True
|
264
|
+
self._map = {}
|
265
|
+
self.searchResult.clear()
|
266
|
+
if text := self.searchText.text():
|
267
|
+
self._search.setUserRegEx(self.toggleRegEx.isChecked())
|
268
|
+
self._search.setCaseSensitive(self.toggleCase.isChecked())
|
269
|
+
self._search.setWholeWords(self.toggleWord.isChecked())
|
270
|
+
for item, results, capped in self._search.iterSearch(SHARED.project, text):
|
271
|
+
self._displayResultSet(item, results, capped)
|
272
|
+
logger.debug("Search took %.3f ms", 1000*(time() - start))
|
273
|
+
self._time = time()
|
274
|
+
QApplication.restoreOverrideCursor()
|
275
|
+
self._blocked = False
|
276
|
+
return
|
277
|
+
|
278
|
+
@pyqtSlot()
|
279
|
+
def _searchResultSelected(self) -> None:
|
280
|
+
"""Process search result selection."""
|
281
|
+
if items := self.searchResult.selectedItems():
|
282
|
+
if (data := items[0].data(0, self.D_RESULT)) and len(data) == 3:
|
283
|
+
self.selectedItemChanged.emit(str(data[0]))
|
284
|
+
elif data := items[0].data(0, self.D_HANDLE):
|
285
|
+
self.selectedItemChanged.emit(str(data))
|
286
|
+
return
|
287
|
+
|
288
|
+
@pyqtSlot("QTreeWidgetItem*", int)
|
289
|
+
def _searchResultDoubleClicked(self, item: QTreeWidgetItem, column: int) -> None:
|
290
|
+
"""Process search result double click."""
|
291
|
+
if (data := item.data(0, self.D_RESULT)) and len(data) == 3:
|
292
|
+
self.openDocumentSelectRequest.emit(
|
293
|
+
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), True
|
294
|
+
)
|
295
|
+
return
|
296
|
+
|
297
|
+
@pyqtSlot(bool)
|
298
|
+
def _toggleCase(self, state: bool) -> None:
|
299
|
+
"""Enable/disable case sensitive mode."""
|
300
|
+
CONFIG.searchProjCase = state
|
301
|
+
return
|
302
|
+
|
303
|
+
@pyqtSlot(bool)
|
304
|
+
def _toggleWord(self, state: bool) -> None:
|
305
|
+
"""Enable/disable whole word search mode."""
|
306
|
+
CONFIG.searchProjWord = state
|
307
|
+
return
|
308
|
+
|
309
|
+
@pyqtSlot(bool)
|
310
|
+
def _toggleRegEx(self, state: bool) -> None:
|
311
|
+
"""Enable/disable regular expression search mode."""
|
312
|
+
CONFIG.searchProjRegEx = state
|
313
|
+
return
|
314
|
+
|
315
|
+
##
|
316
|
+
# Internal Functions
|
317
|
+
##
|
318
|
+
|
319
|
+
def _displayResultSet(
|
320
|
+
self, nwItem: NWItem | None, results: list[tuple[int, int, str]], capped: bool
|
321
|
+
) -> None:
|
322
|
+
"""Populate the result tree."""
|
323
|
+
if results and nwItem:
|
324
|
+
tHandle = nwItem.itemHandle
|
325
|
+
docIcon = SHARED.theme.getItemIcon(
|
326
|
+
nwItem.itemType, nwItem.itemClass,
|
327
|
+
nwItem.itemLayout, nwItem.mainHeading
|
328
|
+
)
|
329
|
+
ext = "+" if capped else ""
|
330
|
+
|
331
|
+
tItem = QTreeWidgetItem()
|
332
|
+
tItem.setText(self.C_NAME, nwItem.itemName)
|
333
|
+
tItem.setIcon(self.C_NAME, docIcon)
|
334
|
+
tItem.setData(self.C_NAME, self.D_HANDLE, tHandle)
|
335
|
+
tItem.setText(self.C_COUNT, f"({len(results):n}{ext})")
|
336
|
+
tItem.setTextAlignment(self.C_COUNT, QtAlignRight)
|
337
|
+
tItem.setForeground(self.C_COUNT, self.palette().highlight())
|
338
|
+
|
339
|
+
index = self._map.get(tHandle, (self.searchResult.topLevelItemCount(), 0.0))[0]
|
340
|
+
self.searchResult.takeTopLevelItem(index)
|
341
|
+
self.searchResult.insertTopLevelItem(index, tItem)
|
342
|
+
self._map[tHandle] = (index, time())
|
343
|
+
|
344
|
+
rItems = []
|
345
|
+
for start, length, context in results:
|
346
|
+
rItem = QTreeWidgetItem()
|
347
|
+
rItem.setText(0, context)
|
348
|
+
rItem.setData(0, self.D_RESULT, (tHandle, start, length))
|
349
|
+
rItems.append(rItem)
|
350
|
+
|
351
|
+
tItem.addChildren(rItems)
|
352
|
+
tItem.setExpanded(True)
|
353
|
+
|
354
|
+
parent = self.searchResult.indexFromItem(tItem)
|
355
|
+
for i in range(tItem.childCount()):
|
356
|
+
self.searchResult.setFirstColumnSpanned(i, parent, True)
|
357
|
+
|
358
|
+
QApplication.processEvents()
|
359
|
+
|
360
|
+
return
|
361
|
+
|
362
|
+
# END Class GuiProjectSearch
|