Add progress bar for bulky tasks (#2478)
This commit is contained in:
@@ -160,6 +160,10 @@ class DocSplitter:
|
||||
|
||||
return
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""The length of the split job."""
|
||||
return len(self._rawData)
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -270,7 +274,9 @@ class DocDuplicator:
|
||||
after = True
|
||||
if items:
|
||||
hMap: dict[str, str | None] = {t: None for t in items}
|
||||
SHARED.initMainProgress(len(items))
|
||||
for tHandle in items:
|
||||
SHARED.incMainProgress()
|
||||
if oldItem := self._project.tree[tHandle]:
|
||||
pHandle = hMap.get(oldItem.itemParent or "") or oldItem.itemParent
|
||||
if newItem := self._project.tree.duplicate(tHandle, pHandle, after):
|
||||
@@ -282,6 +288,7 @@ class DocDuplicator:
|
||||
after = False
|
||||
else:
|
||||
break
|
||||
SHARED.clearMainProgress()
|
||||
return result
|
||||
|
||||
|
||||
@@ -320,10 +327,13 @@ class DocSearch:
|
||||
self._regEx = re.compile(self._buildPattern(search), self._opts)
|
||||
logger.debug("Searching with pattern '%s'", self._regEx.pattern)
|
||||
storage = project.storage
|
||||
SHARED.initMainProgress(len(project.tree))
|
||||
for item in project.tree:
|
||||
SHARED.incMainProgress()
|
||||
if item.isFileType():
|
||||
results, capped = self.searchText(storage.getDocumentText(item.itemHandle))
|
||||
yield item, results, capped
|
||||
SHARED.clearMainProgress()
|
||||
return
|
||||
|
||||
def searchText(self, text: str) -> tuple[list[tuple[int, int, str]], bool]:
|
||||
|
||||
@@ -147,14 +147,17 @@ class Index:
|
||||
def rebuild(self) -> None:
|
||||
"""Rebuild the entire index from scratch."""
|
||||
self.clear()
|
||||
SHARED.initMainProgress(len(self._project.tree))
|
||||
for nwItem in self._project.tree:
|
||||
if nwItem.isFileType():
|
||||
text = self._project.storage.getDocumentText(nwItem.itemHandle)
|
||||
self.scanText(nwItem.itemHandle, text, blockSignal=True)
|
||||
SHARED.incMainProgress()
|
||||
self._indexBroken = False
|
||||
SHARED.emitIndexAvailable(self._project)
|
||||
for tHandle in self._novelModels:
|
||||
self.refreshNovelModel(tHandle)
|
||||
SHARED.clearMainProgress()
|
||||
return
|
||||
|
||||
def deleteHandle(self, tHandle: str) -> None:
|
||||
|
||||
@@ -718,9 +718,15 @@ class GuiProjectTree(QTreeView):
|
||||
else:
|
||||
return False
|
||||
|
||||
SHARED.initMainProgress(len(items))
|
||||
self.setEnabled(False)
|
||||
for sHandle in items:
|
||||
SHARED.incMainProgress()
|
||||
docMerger.appendText(sHandle, True, mLabel)
|
||||
|
||||
self.setEnabled(True)
|
||||
SHARED.clearMainProgress()
|
||||
|
||||
if not docMerger.writeTargetDoc():
|
||||
SHARED.error(
|
||||
self.tr("Could not write document content."),
|
||||
@@ -764,7 +770,10 @@ class GuiProjectTree(QTreeView):
|
||||
docSplit.setParentItem(tItem.itemParent)
|
||||
|
||||
docSplit.splitDocument(headerList, text)
|
||||
SHARED.initMainProgress(len(docSplit))
|
||||
self.setEnabled(False)
|
||||
for writeOk in docSplit.writeDocuments(docHierarchy):
|
||||
SHARED.incMainProgress()
|
||||
if not writeOk:
|
||||
SHARED.error(
|
||||
self.tr("Could not write document content."),
|
||||
@@ -774,6 +783,9 @@ class GuiProjectTree(QTreeView):
|
||||
if data.get("moveToTrash", False):
|
||||
self.processDeleteRequest([tHandle], False)
|
||||
|
||||
self.setEnabled(True)
|
||||
SHARED.clearMainProgress()
|
||||
|
||||
return True
|
||||
|
||||
def duplicateFromHandle(self, tHandle: str) -> None:
|
||||
@@ -786,10 +798,12 @@ class GuiProjectTree(QTreeView):
|
||||
else:
|
||||
question = self.tr("Do you want to duplicate this item and all child items?")
|
||||
if SHARED.question(question):
|
||||
self.setEnabled(False)
|
||||
docDup = DocDuplicator(SHARED.project)
|
||||
dHandles = docDup.duplicate(itemTree)
|
||||
if len(dHandles) != len(itemTree):
|
||||
SHARED.warn(self.tr("Could not duplicate all items."))
|
||||
self.setEnabled(True)
|
||||
self.restoreExpandedState()
|
||||
return
|
||||
|
||||
@@ -912,17 +926,23 @@ class GuiProjectTree(QTreeView):
|
||||
if not SHARED.question(self.tr("Permanently delete selected item(s)?")):
|
||||
logger.info("Action cancelled by user")
|
||||
return
|
||||
|
||||
self.setEnabled(False)
|
||||
for index in indices:
|
||||
if node := model.node(index):
|
||||
for child in reversed(node.allChildren()):
|
||||
SHARED.project.removeItem(child.item.itemHandle)
|
||||
SHARED.project.removeItem(node.item.itemHandle)
|
||||
self.setEnabled(True)
|
||||
|
||||
elif trashNode := SHARED.project.tree.trash:
|
||||
if askFirst and not SHARED.question(self.tr("Move selected item(s) to Trash?")):
|
||||
logger.info("Action cancelled by user")
|
||||
return
|
||||
|
||||
self.setEnabled(False)
|
||||
model.multiMove(indices, model.indexFromNode(trashNode))
|
||||
self.setEnabled(True)
|
||||
|
||||
return
|
||||
|
||||
|
||||
+16
-9
@@ -45,6 +45,7 @@ from novelwriter.dialogs.preferences import GuiPreferences
|
||||
from novelwriter.dialogs.projectsettings import GuiProjectSettings
|
||||
from novelwriter.dialogs.wordlist import GuiWordList
|
||||
from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwFocus, nwItemType, nwView
|
||||
from novelwriter.extensions.progressbars import NProgressSimple
|
||||
from novelwriter.gui.doceditor import GuiDocEditor
|
||||
from novelwriter.gui.docviewer import GuiDocViewer
|
||||
from novelwriter.gui.docviewerpanel import GuiDocViewerPanel
|
||||
@@ -69,14 +70,10 @@ logger = logging.getLogger(__name__)
|
||||
class GuiMain(QMainWindow):
|
||||
"""Main GUI Window
|
||||
|
||||
The Main GUI window class. It is the entry point of the
|
||||
application, and holds all runtime objects aside from the main
|
||||
Config instance, which is created before the Main GUI.
|
||||
|
||||
The Main GUI is split up into GUI components, assembled in the init
|
||||
function. Also, the project instance and theme instance are created
|
||||
here. These should be passed around to all other objects who need
|
||||
them and new instances of them should generally not be created.
|
||||
The Main GUI window class is the entry point of the application. It
|
||||
is split up into GUI components, assembled in the init function.
|
||||
Tools and dialog windows are created on demand, but may be cached by
|
||||
the Qt library and reused unless explicitly freed after use.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -185,6 +182,10 @@ class GuiMain(QMainWindow):
|
||||
self.splitView.setVisible(False)
|
||||
self.docEditor.closeSearch()
|
||||
|
||||
# Progress Bar
|
||||
self.mainProgress = NProgressSimple(self)
|
||||
self.mainProgress.setFixedHeight(2)
|
||||
|
||||
# Assemble Main Window Elements
|
||||
self.mainBox = QHBoxLayout()
|
||||
self.mainBox.addWidget(self.sideBar)
|
||||
@@ -192,8 +193,14 @@ class GuiMain(QMainWindow):
|
||||
self.mainBox.setContentsMargins(0, 0, 0, 0)
|
||||
self.mainBox.setSpacing(0)
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.addLayout(self.mainBox)
|
||||
self.outerBox.addWidget(self.mainProgress)
|
||||
self.outerBox.setContentsMargins(0, 0, 0, 0)
|
||||
self.outerBox.setSpacing(0)
|
||||
|
||||
self.mainWidget = QWidget(self)
|
||||
self.mainWidget.setLayout(self.mainBox)
|
||||
self.mainWidget.setLayout(self.outerBox)
|
||||
|
||||
self.setMenuBar(self.mainMenu)
|
||||
self.setCentralWidget(self.mainWidget)
|
||||
|
||||
@@ -269,6 +269,26 @@ class SharedData(QObject):
|
||||
self._idleRefTime = currTime
|
||||
return
|
||||
|
||||
def initMainProgress(self, maximum: int, inclusive: bool = False) -> None:
|
||||
"""Start a session for the main progress bar."""
|
||||
if gui := self._gui:
|
||||
gui.mainProgress.setMaximum(maximum - (1 if inclusive else 0))
|
||||
gui.mainProgress.setValue(0)
|
||||
return
|
||||
|
||||
def incMainProgress(self) -> None:
|
||||
"""Increment the value for the main progress bar."""
|
||||
if gui := self._gui:
|
||||
gui.mainProgress.setValue(gui.mainProgress.value() + 1)
|
||||
QApplication.processEvents()
|
||||
return
|
||||
|
||||
def clearMainProgress(self, delay: float = 1.0) -> None:
|
||||
"""Clear the main progress bar."""
|
||||
if gui := self._gui:
|
||||
QTimer.singleShot(int(delay*1000), gui.mainProgress.reset)
|
||||
return
|
||||
|
||||
def newStatusMessage(self, message: str) -> None:
|
||||
"""Request a new status message. This is a callable function for
|
||||
core classes that cannot emit signals on their own.
|
||||
|
||||
@@ -33,6 +33,7 @@ class MockGuiMain(QWidget):
|
||||
self.mainStatus = MagicMock()
|
||||
self.docEditor = MagicMock()
|
||||
self.docViewer = MagicMock()
|
||||
self.mainProgress = MagicMock()
|
||||
self.projPath = ""
|
||||
return
|
||||
|
||||
|
||||
@@ -827,13 +827,13 @@ def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd)
|
||||
# Handle broken index on project open
|
||||
nwGUI.closeProject()
|
||||
idxPath: Path = projPath / "meta" / nwFiles.INDEX_FILE
|
||||
assert idxPath.read_text() != "{}"
|
||||
idxPath.write_text("{}")
|
||||
assert idxPath.read_text() == "{}"
|
||||
assert idxPath.read_text(encoding="utf-8") != "{}"
|
||||
idxPath.write_text("{}", encoding="utf-8")
|
||||
assert idxPath.read_text(encoding="utf-8") == "{}"
|
||||
|
||||
nwGUI.openProject(projPath)
|
||||
nwGUI.saveProject()
|
||||
assert idxPath.read_text() != "{}"
|
||||
assert idxPath.read_text(encoding="utf-8") != "{}"
|
||||
assert nwGUI.docEditor.docHandle == C.hSceneDoc
|
||||
assert nwGUI.docViewer.docHandle == C.hTitlePage
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ def testGuiTheme_SpecialColors(tstPaths):
|
||||
theme.iconCache = MagicMock()
|
||||
|
||||
testTheme: Path = tstPaths.cnfDir / "themes" / "test.conf"
|
||||
testTheme.write_text(
|
||||
testTheme.write_text((
|
||||
"[Main]\n"
|
||||
"name = Test\n"
|
||||
"mode = light\n"
|
||||
@@ -241,7 +241,7 @@ def testGuiTheme_SpecialColors(tstPaths):
|
||||
"[Palette]\n"
|
||||
"window = #000000\n"
|
||||
"text = #ffffff\n"
|
||||
)
|
||||
), encoding="utf-8")
|
||||
theme._scanThemes([testTheme])
|
||||
assert len(theme.colourThemes) == 1
|
||||
CONFIG.themeMode = nwTheme.LIGHT
|
||||
|
||||
Reference in New Issue
Block a user