@@ -48,9 +48,10 @@ addition to the word count.
|
||||
Project Tree Drag and Drop
|
||||
--------------------------
|
||||
|
||||
The project tree allows dragging and drop to a certain extent, but it does not allow bulk actions.
|
||||
This is deliberate to avoid accidentally messing up your project. The project tree has no undo
|
||||
function.
|
||||
The project tree allows dragging and drop to a certain extent. This feature is primarily intended
|
||||
for rearranging the order of your files within each root folder, and has only limited support for
|
||||
moving files elsewhere in the project tree. In general, bulk actions are not allowed. This is
|
||||
deliberate to avoid accidentally messing up your project. The project tree has no undo function.
|
||||
|
||||
Document files and their folders can be rearranged freely within their root folders. Novel files
|
||||
cannot be moved out of the :guilabel:`Novel` folder, except to :guilabel:`Trash` and the
|
||||
@@ -59,9 +60,9 @@ cannot be moved out of the :guilabel:`Novel` folder, except to :guilabel:`Trash`
|
||||
Folders cannot be moved at all outside their root tree. Neither can a folder containing files be
|
||||
deleted. You must first delete the files.
|
||||
|
||||
Root folders in the project tree cannot be dragged and dropped. However, if you want to reorder
|
||||
them, you can move them up or down with respect to eachother from the :guilabel:`Tools` menu, or by
|
||||
pressing :kbd:`Ctrl`:kbd:`Shift` and the :kbd:`Up` or :kbd:`Down` key.
|
||||
Root folders in the project tree cannot be dragged and dropped at all. However, if you want to
|
||||
reorder them, you can move them up or down with respect to eachother from the :guilabel:`Tools`
|
||||
menu, or by pressing :kbd:`Ctrl`:kbd:`Shift` and the :kbd:`Up` or :kbd:`Down` key.
|
||||
|
||||
|
||||
.. _a_ui_edit:
|
||||
|
||||
@@ -90,10 +90,8 @@ trash folder can then be deleted permanently, either individually, or by emptyin
|
||||
menu. Files in this folder are removed from the project index and cannot be referenced.
|
||||
|
||||
Folders and root folders can only be deleted when they are empty. Recursive deletion is not
|
||||
supported.
|
||||
|
||||
A document file or a folder can be deleted from the :guilabel:`project` menu, or by pressing
|
||||
:kbd:`Ctrl`:kbd:`Del`.
|
||||
supported. A document file or a folder can be deleted from the :guilabel:`project` menu, or by
|
||||
pressing :kbd:`Ctrl`:kbd:`Del`.
|
||||
|
||||
|
||||
.. _a_proj_roots_out:
|
||||
@@ -103,13 +101,13 @@ Archived Documents (Outtakes)
|
||||
|
||||
If you don't want to delete a file, or put it in the :guilabel:`Trash` folder where it may be
|
||||
deleted, but still want it out of your main project tree, you can create an :guilabel:`Outtakes`
|
||||
root folder from the :guilabel:`project` menu. This folder is similar to the :guilabel:`Trash`
|
||||
folder in that it doesn't allow subfolders and creating new files.
|
||||
root folder from the :guilabel:`Project` menu. You are not allowed to move folders to this root
|
||||
folder, only files. If you need folders in it to organise your files, you can of course create new
|
||||
ones there.
|
||||
|
||||
You can drag any file to this folder and preserve its settings. The file will always be excluded
|
||||
from the :guilabel:`Build Novel Project` builds, even with the setting to ignore the excluded files
|
||||
option. The file is also removed from the project index, so the tags and references defined in it
|
||||
will not show up anywhere else.
|
||||
from the :guilabel:`Build Novel Project` builds. The file is also removed from the project index, so
|
||||
the tags and references defined in it will not show up anywhere else.
|
||||
|
||||
|
||||
.. _a_proj_roots_orph:
|
||||
|
||||
@@ -33,6 +33,8 @@ class nwConst():
|
||||
fStampFmt = "%Y-%m-%d %H.%M.%S" # FileName safe format
|
||||
dStampFmt = "%Y-%m-%d" # Date only format
|
||||
|
||||
maxDepth = 30 # Maximum folder depth of a project
|
||||
|
||||
# END Class nwConst
|
||||
|
||||
class nwRegEx():
|
||||
|
||||
+2
-2
@@ -33,7 +33,7 @@ from os import path, rename, unlink
|
||||
from nw.core.item import NWItem
|
||||
from nw.constants import nwAlert
|
||||
from nw.common import isHandle
|
||||
from nw.constants import nwItemLayout, nwItemClass
|
||||
from nw.constants import nwItemLayout, nwItemClass, nwConst
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -213,7 +213,7 @@ class NWDoc():
|
||||
|
||||
# Scan for handles
|
||||
thePath = []
|
||||
for n in range(200):
|
||||
for n in range(nwConst.maxDepth + 5):
|
||||
if len(theMeta) < 14:
|
||||
break
|
||||
if theMeta[13] == ":":
|
||||
|
||||
+19
-16
@@ -176,7 +176,7 @@ class NWIndex():
|
||||
if "textCounts" in theData.keys():
|
||||
self.textCounts = theData["textCounts"]
|
||||
|
||||
nowTime = time()
|
||||
nowTime = round(time())
|
||||
self.timeNovel = nowTime
|
||||
self.timeNote = nowTime
|
||||
self.timeIndex = nowTime
|
||||
@@ -268,21 +268,28 @@ class NWIndex():
|
||||
"""
|
||||
theItem = self.theProject.projTree[tHandle]
|
||||
theRoot = self.theProject.projTree.getRootItem(tHandle)
|
||||
|
||||
if theItem is None:
|
||||
logger.error("Not indexing unknown item %s" % tHandle)
|
||||
return False
|
||||
if theItem.itemType != nwItemType.FILE:
|
||||
logger.error("Not indexing non-file item %s" % tHandle)
|
||||
return False
|
||||
if theItem.parHandle == self.theProject.projTree.trashRoot():
|
||||
logger.error("Not indexing trash item %s" % tHandle)
|
||||
return False
|
||||
if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
|
||||
logger.error("Not indexing no-layout item %s" % tHandle)
|
||||
return False
|
||||
if theRoot is None:
|
||||
logger.error("Not indexing homeless item %s" % tHandle)
|
||||
return False
|
||||
|
||||
# Run word counter for the whole text
|
||||
cC, wC, pC = countWords(theText)
|
||||
self.textCounts[tHandle] = [cC, wC, pC]
|
||||
|
||||
# If the file is archived or trashed, we don't index the file itself
|
||||
if theItem.parHandle == self.theProject.projTree.trashRoot():
|
||||
logger.error("Not indexing trash item %s" % tHandle)
|
||||
return False
|
||||
if theRoot.itemClass == nwItemClass.ARCHIVE:
|
||||
logger.error("Not indexing archived item %s" % tHandle)
|
||||
return False
|
||||
@@ -297,7 +304,7 @@ class NWIndex():
|
||||
self.refIndex[tHandle] = {}
|
||||
self.refIndex[tHandle]["T000000"] = {
|
||||
"tags" : [],
|
||||
"updated" : time(),
|
||||
"updated" : round(time()),
|
||||
}
|
||||
if itemLayout == nwItemLayout.NOTE:
|
||||
self.noteIndex[tHandle] = {}
|
||||
@@ -351,12 +358,8 @@ class NWIndex():
|
||||
lastText = "\n".join(theLines[nTitle-1:nLine-1])
|
||||
self._indexWordCounts(tHandle, isNovel, lastText, nTitle)
|
||||
|
||||
# Run word counter for whole text
|
||||
cC, wC, pC = countWords(theText)
|
||||
self.textCounts[tHandle] = [cC, wC, pC]
|
||||
|
||||
# Update timestamps for index changes
|
||||
nowTime = time()
|
||||
nowTime = round(time())
|
||||
self.timeIndex = nowTime
|
||||
if isNovel:
|
||||
self.timeNovel = nowTime
|
||||
@@ -391,7 +394,7 @@ class NWIndex():
|
||||
sTitle = "T%06d" % nLine
|
||||
self.refIndex[tHandle][sTitle] = {
|
||||
"tags" : [],
|
||||
"updated" : time(),
|
||||
"updated" : round(time()),
|
||||
}
|
||||
theData = {
|
||||
"level" : hDepth,
|
||||
@@ -401,7 +404,7 @@ class NWIndex():
|
||||
"cCount" : 0,
|
||||
"wCount" : 0,
|
||||
"pCount" : 0,
|
||||
"updated" : time(),
|
||||
"updated" : round(time()),
|
||||
}
|
||||
|
||||
if hText != "":
|
||||
@@ -425,14 +428,14 @@ class NWIndex():
|
||||
self.novelIndex[tHandle][sTitle]["cCount"] = cC
|
||||
self.novelIndex[tHandle][sTitle]["wCount"] = wC
|
||||
self.novelIndex[tHandle][sTitle]["pCount"] = pC
|
||||
self.novelIndex[tHandle][sTitle]["updated"] = time()
|
||||
self.novelIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
else:
|
||||
if tHandle in self.noteIndex:
|
||||
if sTitle in self.noteIndex[tHandle]:
|
||||
self.noteIndex[tHandle][sTitle]["cCount"] = cC
|
||||
self.noteIndex[tHandle][sTitle]["wCount"] = wC
|
||||
self.noteIndex[tHandle][sTitle]["pCount"] = pC
|
||||
self.noteIndex[tHandle][sTitle]["updated"] = time()
|
||||
self.noteIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
return
|
||||
|
||||
def _indexSynopsis(self, tHandle, isNovel, theText, nTitle):
|
||||
@@ -443,12 +446,12 @@ class NWIndex():
|
||||
if tHandle in self.novelIndex:
|
||||
if sTitle in self.novelIndex[tHandle]:
|
||||
self.novelIndex[tHandle][sTitle]["synopsis"] = theText
|
||||
self.novelIndex[tHandle][sTitle]["updated"] = time()
|
||||
self.novelIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
else:
|
||||
if tHandle in self.noteIndex:
|
||||
if sTitle in self.noteIndex[tHandle]:
|
||||
self.noteIndex[tHandle][sTitle]["synopsis"] = theText
|
||||
self.noteIndex[tHandle][sTitle]["updated"] = time()
|
||||
self.noteIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
return
|
||||
|
||||
def _indexNoteRef(self, tHandle, aLine, nLine, nTitle):
|
||||
|
||||
+5
-5
@@ -35,7 +35,7 @@ from time import time
|
||||
|
||||
from nw.core.item import NWItem
|
||||
from nw.common import checkString
|
||||
from nw.constants import nwFiles, nwItemType, nwItemClass, nwItemLayout
|
||||
from nw.constants import nwFiles, nwItemType, nwItemClass, nwItemLayout, nwConst
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -251,11 +251,11 @@ class NWTree():
|
||||
def getRootItem(self, tHandle):
|
||||
"""Iterate upwards in the tree until we find the item with
|
||||
parent None, the root item. We do this with a for loop with a
|
||||
maximum depth of 200 to make infinite loops impossible.
|
||||
maximum depth to make infinite loops impossible.
|
||||
"""
|
||||
tItem = self.__getitem__(tHandle)
|
||||
if tItem is not None:
|
||||
for i in range(200):
|
||||
for i in range(nwConst.maxDepth + 1):
|
||||
if tItem.parHandle is None:
|
||||
return tItem
|
||||
else:
|
||||
@@ -266,14 +266,14 @@ class NWTree():
|
||||
def getItemPath(self, tHandle):
|
||||
"""Iterate upwards in the tree until we find the item with
|
||||
parent None, the root item, and return the list of handles.
|
||||
We do this with a for loop with a maximum depth of 200 to make
|
||||
We do this with a for loop with a maximum depth to make
|
||||
infinite loops impossible.
|
||||
"""
|
||||
tTree = []
|
||||
tItem = self.__getitem__(tHandle)
|
||||
if tItem is not None:
|
||||
tTree.append(tHandle)
|
||||
for i in range(200):
|
||||
for i in range(nwConst.maxDepth + 1):
|
||||
if tItem.parHandle is None:
|
||||
return tTree
|
||||
else:
|
||||
|
||||
+43
-21
@@ -38,7 +38,7 @@ from PyQt5.QtWidgets import (
|
||||
|
||||
from nw.core import NWDoc
|
||||
from nw.constants import (
|
||||
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -59,6 +59,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
self.theProject = theParent.theProject
|
||||
self.theIndex = theParent.theIndex
|
||||
|
||||
# Tree Settings
|
||||
self.theMap = None
|
||||
@@ -145,6 +146,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
if not self.theParent.hasProject:
|
||||
return False
|
||||
|
||||
# The item needs to be assigned an item class, so one must be
|
||||
# provided, or it must be possible to extract it from the parent
|
||||
# item of the new item.
|
||||
if itemClass is None and pHandle is not None:
|
||||
pItem = self.theProject.projTree[pHandle]
|
||||
if pItem is not None:
|
||||
@@ -167,6 +171,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.makeAlert("Failed to add new item.", nwAlert.BUG)
|
||||
return False
|
||||
|
||||
# Everything is fine, we have what we need, so we proceed
|
||||
logger.verbose("Adding new item of type %s and class %s to handle %s" % (
|
||||
itemType.name, itemClass.name, str(pHandle))
|
||||
)
|
||||
@@ -182,7 +187,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
# If still nothing, give up
|
||||
if pHandle is None:
|
||||
logger.error("Did not find anywhere to add the item!")
|
||||
self.makeAlert(
|
||||
"Did not find anywhere to add the file or folder!", nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
# Now check if the selected item is a file, in which case
|
||||
@@ -207,19 +214,23 @@ class GuiProjectTree(QTreeWidget):
|
||||
)
|
||||
return False
|
||||
|
||||
if pItem.itemClass == nwItemClass.ARCHIVE:
|
||||
self.makeAlert(
|
||||
"Cannot add new files or folders to the %s folder." % (
|
||||
nwLabels.CLASS_NAME[nwItemClass.ARCHIVE]
|
||||
), nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
parTree = self.theProject.projTree.getItemPath(pHandle)
|
||||
|
||||
# If we're still here, add the file or folder
|
||||
if itemType == nwItemType.FILE:
|
||||
tHandle = self.theProject.newFile("New File", itemClass, pHandle)
|
||||
|
||||
elif itemType == nwItemType.FOLDER:
|
||||
if len(parTree) >= nwConst.maxDepth - 1:
|
||||
# Folders cannot be deeper than maxDepth - 1, leaving room
|
||||
# for one more level of files.
|
||||
self.makeAlert((
|
||||
"Cannot add new folder to this item. "
|
||||
"Maximum folder depth has been reached."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
tHandle = self.theProject.newFolder("New Folder", itemClass, pHandle)
|
||||
|
||||
else:
|
||||
logger.error("Failed to add new item")
|
||||
return False
|
||||
@@ -332,7 +343,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
logger.debug("Emptying Trash folder")
|
||||
if trashHandle is None:
|
||||
self.makeAlert("There is no Trash folder.", nwAlert.INFO)
|
||||
self.makeAlert(
|
||||
"There is currently no Trash folder in this project.", nwAlert.INFO
|
||||
)
|
||||
return False
|
||||
|
||||
theTrash = self.getTreeFromHandle(trashHandle)
|
||||
@@ -341,13 +354,13 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
nTrash = len(theTrash)
|
||||
if nTrash == 0:
|
||||
self.makeAlert("The Trash folder is empty.", nwAlert.INFO)
|
||||
self.makeAlert("The Trash folder is already empty.", nwAlert.INFO)
|
||||
return False
|
||||
|
||||
msgBox = QMessageBox()
|
||||
msgRes = msgBox.question(
|
||||
self, "Empty Trash", "Permanently delete %d file%s from Trash?" % (
|
||||
nTrash, "s"*int(nTrash > 1)
|
||||
nTrash, "s" if nTrash > 1 else ""
|
||||
)
|
||||
)
|
||||
if msgRes != QMessageBox.Yes:
|
||||
@@ -420,7 +433,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
theDoc = NWDoc(self.theProject, self.theParent)
|
||||
theDoc.deleteDocument(tHandle)
|
||||
del self.theProject.projTree[tHandle]
|
||||
self.theParent.theIndex.deleteHandle(tHandle)
|
||||
self.theIndex.deleteHandle(tHandle)
|
||||
|
||||
else:
|
||||
# The file is not already in the trash folder, so we
|
||||
@@ -448,7 +461,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.propagateCount(tHandle, wCount)
|
||||
|
||||
self._setTreeChanged(True)
|
||||
self.theParent.theIndex.deleteHandle(tHandle)
|
||||
self.theIndex.deleteHandle(tHandle)
|
||||
|
||||
elif nwItemS.itemType == nwItemType.FOLDER:
|
||||
logger.debug("User requested folder %s deleted" % tHandle)
|
||||
@@ -461,7 +474,11 @@ class GuiProjectTree(QTreeWidget):
|
||||
trItemP.takeChild(tIndex)
|
||||
del self.theProject.projTree[tHandle]
|
||||
else:
|
||||
self.makeAlert("Cannot delete folder. It is not empty.", nwAlert.ERROR)
|
||||
self.makeAlert((
|
||||
"Cannot delete folder. It is not empty. "
|
||||
"Recursive deletion is not supported. "
|
||||
"Please delete the content first."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
elif nwItemS.itemType == nwItemType.ROOT:
|
||||
@@ -473,7 +490,11 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.theParent.mainMenu.setAvailableRoot()
|
||||
self._setTreeChanged(True)
|
||||
else:
|
||||
self.makeAlert("Cannot delete root folder. It is not empty.", nwAlert.ERROR)
|
||||
self.makeAlert((
|
||||
"Cannot delete root folder. It is not empty. "
|
||||
"Recursive deletion is not supported. "
|
||||
"Please delete the content first."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -532,7 +553,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
pCount += int(pItem.child(i).text(self.C_COUNT))
|
||||
pHandle = pItem.data(self.C_NAME, Qt.UserRole)
|
||||
|
||||
if not nDepth > 200 and pHandle != "":
|
||||
if not nDepth > nwConst.maxDepth + 1 and pHandle != "":
|
||||
self.propagateCount(pHandle, pCount, nDepth+1)
|
||||
|
||||
return
|
||||
@@ -710,11 +731,12 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.propagateCount(sHandle, wCount)
|
||||
|
||||
# The items dropped into archive or trash should be removed
|
||||
# from the project index
|
||||
# from the project index, for all other items, we rescan the
|
||||
# file to ensure the index is up to date.
|
||||
if onFree:
|
||||
self.theParent.theIndex.deleteHandle(sHandle)
|
||||
self.theIndex.deleteHandle(sHandle)
|
||||
else:
|
||||
self.theParent.theIndex.reIndexHandle(sHandle)
|
||||
self.theIndex.reIndexHandle(sHandle)
|
||||
|
||||
else:
|
||||
logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
|
||||
@@ -964,7 +986,7 @@ class GuiProjectTreeMenu(QMenu):
|
||||
showEdit = not isTrash and not isOrph
|
||||
showExport = isFile and not inTrash and not isOrph
|
||||
showNewFile = not isTrash and not inTrash and not isOrph and not isArch
|
||||
showNewFolder = not isTrash and not inTrash and not isOrph and not isArch
|
||||
showNewFolder = not isTrash and not inTrash and not isOrph
|
||||
showDelete = not isTrash
|
||||
showEmpty = isTrash
|
||||
|
||||
|
||||
+18
-11
@@ -1,13 +1,13 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.12.1" hexVersion="0x001201f0" fileVersion="1.2" timeStamp="2020-08-16 17:50:53">
|
||||
<novelWriterXML appVersion="0.12.1" hexVersion="0x001201f0" fileVersion="1.2" timeStamp="2020-08-16 21:12:32">
|
||||
<project>
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jane Smith</author>
|
||||
<author>Jay Doh</author>
|
||||
<saveCount>713</saveCount>
|
||||
<autoCount>138</autoCount>
|
||||
<editTime>35134</editTime>
|
||||
<saveCount>723</saveCount>
|
||||
<autoCount>140</autoCount>
|
||||
<editTime>35485</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>False</doBackup>
|
||||
@@ -15,8 +15,8 @@
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>636b6aa9b697b</lastEdited>
|
||||
<lastViewed>636b6aa9b697b</lastViewed>
|
||||
<lastWordCount>1071</lastWordCount>
|
||||
<novelWordCount>695</novelWordCount>
|
||||
<lastWordCount>1077</lastWordCount>
|
||||
<novelWordCount>701</novelWordCount>
|
||||
<notesWordCount>376</notesWordCount>
|
||||
<autoReplace>
|
||||
<entry key="A">B</entry>
|
||||
@@ -46,7 +46,7 @@
|
||||
<entry blue="175" green="0" red="117">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="24">
|
||||
<content count="25">
|
||||
<item handle="7031beac91f75" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
@@ -269,7 +269,14 @@
|
||||
<status>None</status>
|
||||
<expanded>True</expanded>
|
||||
</item>
|
||||
<item handle="8a5deb88c0e97" order="0" parent="6827118336ac1">
|
||||
<item handle="ae9bf3c3ea159" order="0" parent="6827118336ac1">
|
||||
<name>Scenes</name>
|
||||
<type>FOLDER</type>
|
||||
<class>ARCHIVE</class>
|
||||
<status>None</status>
|
||||
<expanded>True</expanded>
|
||||
</item>
|
||||
<item handle="8a5deb88c0e97" order="0" parent="ae9bf3c3ea159">
|
||||
<name>Old File</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -295,9 +302,9 @@
|
||||
<status>New</status>
|
||||
<exported>True</exported>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<charCount>30</charCount>
|
||||
<wordCount>6</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>36</cursorPos>
|
||||
</item>
|
||||
</content>
|
||||
|
||||
Reference in New Issue
Block a user