Merge pull request #415 from vkbo/outtakes

Outtakes (Archive) Feature
This commit is contained in:
Veronica K. Berglyd Olsen
2020-08-16 18:29:29 +02:00
committed by GitHub
18 changed files with 184 additions and 26 deletions
+21
View File
@@ -43,6 +43,27 @@ currently selected item. This panel also includes the latest paragraph and chara
addition to the word count.
.. _a_ui_tree_dnd:
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.
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
:guilabel:`Outtakes` folder. Note files can be moved freely anywhere.
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.
.. _a_ui_edit:
Editing and Viewing Documents
+17 -1
View File
@@ -87,7 +87,7 @@ Deleted Documents
Deleted document files will be moved into a special :guilabel:`Trash` root folder. Files in the
trash folder can then be deleted permanently, either individually, or by emptying the trash from the
menu.
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.
@@ -96,6 +96,22 @@ A document file or a folder can be deleted from the :guilabel:`project` menu, or
:kbd:`Ctrl`:kbd:`Del`.
.. _a_proj_roots_out:
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.
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.
.. _a_proj_roots_orph:
Orphaned Documents
@@ -25,6 +25,7 @@ cls_timeline = calendar.svg
cls_object = key.svg
cls_entity = flag.svg
cls_custom = star.svg
cls_archive = delete.svg
cls_trash = trash.svg
proj_document = document-text.svg
proj_folder = folder.svg
@@ -25,6 +25,7 @@ cls_timeline = calendar.svg
cls_object = key.svg
cls_entity = flag.svg
cls_custom = star.svg
cls_archive = delete.svg
cls_trash = trash.svg
proj_document = document-text.svg
proj_folder = folder.svg
@@ -25,6 +25,7 @@ cls_timeline = calendar.svg
cls_object = key.svg
cls_entity = flag.svg
cls_custom = star.svg
cls_archive = delete.svg
cls_trash = trash.svg
proj_document = document-text.svg
proj_folder = folder.svg
@@ -25,6 +25,7 @@ cls_timeline = calendar.svg
cls_object = key.svg
cls_entity = flag.svg
cls_custom = star.svg
cls_archive = delete.svg
cls_trash = trash.svg
proj_document = document-text.svg
proj_folder = folder.svg
+3
View File
@@ -84,6 +84,7 @@ class nwLabels():
nwItemClass.OBJECT : "Objects",
nwItemClass.ENTITY : "Entity",
nwItemClass.CUSTOM : "Custom",
nwItemClass.ARCHIVE : "Outtakes",
nwItemClass.TRASH : "Trash",
}
CLASS_FLAG = {
@@ -96,6 +97,7 @@ class nwLabels():
nwItemClass.OBJECT : "O",
nwItemClass.ENTITY : "E",
nwItemClass.CUSTOM : "X",
nwItemClass.ARCHIVE : "U",
nwItemClass.TRASH : "R",
}
CLASS_ICON = {
@@ -108,6 +110,7 @@ class nwLabels():
nwItemClass.OBJECT : "cls_object",
nwItemClass.ENTITY : "cls_entity",
nwItemClass.CUSTOM : "cls_custom",
nwItemClass.ARCHIVE : "cls_archive",
nwItemClass.TRASH : "cls_trash",
}
LAYOUT_NAME = {
+2 -1
View File
@@ -48,7 +48,8 @@ class nwItemClass(Enum):
OBJECT = 6
ENTITY = 7
CUSTOM = 8
TRASH = 9
ARCHIVE = 9
TRASH = 10
# END Enum nwItemClass
+34
View File
@@ -34,6 +34,7 @@ from time import time
from nw.constants import (
nwFiles, nwKeyWords, nwItemType, nwItemClass, nwItemLayout, nwAlert
)
from nw.core.document import NWDoc
from nw.core.tools import countWords
logger = logging.getLogger(__name__)
@@ -106,6 +107,8 @@ class NWIndex():
def deleteHandle(self, tHandle):
"""Delete all entries of a given document handle.
"""
logger.debug("Removing item %s from the index" % tHandle)
delTags = []
for tTag in self.tagIndex:
if self.tagIndex[tTag][1] == tHandle:
@@ -121,6 +124,26 @@ class NWIndex():
return
def reIndexHandle(self, tHandle):
"""Put a file back into the index. This is used when files are
moved from the archive or trash folders back into the active
project.
"""
logger.debug("Re-indexing item %s" % tHandle)
tItem = self.theProject.projTree[tHandle]
if tItem is None:
return False
if tItem.itemType != nwItemType.FILE:
return False
theDoc = NWDoc(self.theProject, self.theParent)
theText = theDoc.openDocument(tHandle, showStatus=False)
if theText:
self.scanText(tHandle, theText)
return True
##
# Load and Save Index to/from File
##
@@ -244,13 +267,24 @@ class NWIndex():
files before we save them, unless we're rebuilding the index.
"""
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
if theRoot.itemClass == nwItemClass.ARCHIVE:
logger.error("Not indexing archived item %s" % tHandle)
return False
itemClass = theItem.itemClass
+19 -4
View File
@@ -67,6 +67,7 @@ class NWTree():
self._treeOrder = []
self._treeRoots = []
self._trashRoot = None
self._archRoot = None
self._theLength = 0
self._theIndex = 0
self._treeChanged = False
@@ -96,6 +97,9 @@ class NWTree():
if nwItem.itemType == nwItemType.ROOT:
logger.verbose("Entry %s is a root item" % str(tHandle))
self._treeRoots.append(tHandle)
if nwItem.itemClass == nwItemClass.ARCHIVE:
logger.verbose("Entry %s is the archive folder" % str(tHandle))
self._archRoot = tHandle
if nwItem.itemType == nwItemType.TRASH:
if self._trashRoot is None:
@@ -211,6 +215,14 @@ class NWTree():
return self._trashRoot
return None
def archiveRoot(self):
"""Returns the handle of the archive folder, or None if there
isn't one.
"""
if self._archRoot:
return self._archRoot
return None
def findRoot(self, theClass):
"""Find the root item for a given class.
Note: This returns the first item for class CUSTOM.
@@ -245,12 +257,10 @@ class NWTree():
if tItem is not None:
for i in range(200):
if tItem.parHandle is None:
return tHandle
return tItem
else:
tHandle = tItem.parHandle
tItem = self.__getitem__(tHandle)
if tItem is None:
return tHandle
tItem = self.__getitem__(tHandle)
return None
def getItemPath(self, tHandle):
@@ -391,7 +401,12 @@ class NWTree():
return False
self._treeOrder.remove(tHandle)
self._theLength = len(self._treeOrder)
if tHandle in self._treeRoots:
self._treeRoots.remove(tHandle)
self._setTreeChanged(True)
return True
def __contains__(self, tHandle):
+2 -1
View File
@@ -478,6 +478,7 @@ class GuiBuildNovel(QDialog):
noteRoot = noteFiles
noteRoot &= tItem.itemType == nwItemType.ROOT
noteRoot &= tItem.itemClass != nwItemClass.NOVEL
noteRoot &= tItem.itemClass != nwItemClass.ARCHIVE
try:
if noteRoot:
@@ -539,7 +540,6 @@ class GuiBuildNovel(QDialog):
* Items that appear in the TRASH folder or have parent set to
None (orphaned files).
"""
if theItem is None:
return False
@@ -551,6 +551,7 @@ class GuiBuildNovel(QDialog):
isNone |= theItem.itemClass == nwItemClass.NO_CLASS
isNone |= theItem.itemClass == nwItemClass.TRASH
isNone |= theItem.parHandle == self.theProject.projTree.trashRoot()
isNone |= theItem.parHandle == self.theProject.projTree.archiveRoot()
isNone |= theItem.parHandle is None
isNote = theItem.itemLayout == nwItemLayout.NOTE
isNovel = not isNone and not isNote
+4
View File
@@ -75,6 +75,9 @@ class GuiMainMenu(QMenuBar):
##
def setAvailableRoot(self):
"""Update the list of available root folders and set the ones
that are active.
"""
for itemClass in nwItemClass:
if itemClass == nwItemClass.NO_CLASS:
continue
@@ -245,6 +248,7 @@ class GuiMainMenu(QMenuBar):
self.rootItems[nwItemClass.OBJECT] = QAction("Object Root", self.rootMenu)
self.rootItems[nwItemClass.ENTITY] = QAction("Entity Root", self.rootMenu)
self.rootItems[nwItemClass.CUSTOM] = QAction("Custom Root", self.rootMenu)
self.rootItems[nwItemClass.ARCHIVE] = QAction("Outtakes Root", self.rootMenu)
nCount = 0
for itemClass in self.rootItems.keys():
nCount += 1 # This forces the lambdas to be unique
+34 -5
View File
@@ -201,7 +201,17 @@ class GuiProjectTree(QTreeWidget):
if pHandle == self.theProject.projTree.trashRoot():
self.makeAlert(
"Cannot add new files or folders to the trash folder.", nwAlert.ERROR
"Cannot add new files or folders to the %s folder." % (
nwLabels.CLASS_NAME[nwItemClass.TRASH]
), nwAlert.ERROR
)
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
@@ -459,6 +469,7 @@ class GuiProjectTree(QTreeWidget):
tIndex = self.indexOfTopLevelItem(trItemS)
if trItemS.childCount() == 0:
self.takeTopLevelItem(tIndex)
del self.theProject.projTree[tHandle]
self.theParent.mainMenu.setAvailableRoot()
self._setTreeChanged(True)
else:
@@ -669,17 +680,25 @@ class GuiProjectTree(QTreeWidget):
isNote = snItem.itemLayout == nwItemLayout.NOTE
onFile = dnItem.itemType == nwItemType.FILE
isRoot = snItem.itemType == nwItemType.ROOT
onFree = dnItem.itemClass == nwItemClass.ARCHIVE
onFree |= dnItem.itemClass == nwItemClass.TRASH
onFree &= snItem.itemType == nwItemType.FILE
isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem
if (isSame or isNone or isNote) and not (onFile and isOnTop) and not isRoot:
if (isSame or isNone or isNote or onFree) and not (onFile and isOnTop) and not isRoot:
logger.debug("Drag'n'drop of item %s accepted" % sHandle)
self.propagateCount(sHandle, 0)
QTreeWidget.dropEvent(self, theEvent)
# Handle orphaned files differently than tracked files
if isNone:
self._moveOrphanedItem(sHandle, dHandle)
self._cleanOrphanedRoot()
else:
self._updateItemParent(sHandle)
if not isSame:
# If the item does not have the same class as the target,
# and the target is not a free root folder, update its class
if not isSame and not onFree:
logger.debug("Item %s class has been changed from %s to %s" % (
sHandle,
snItem.itemClass.name,
@@ -687,7 +706,16 @@ class GuiProjectTree(QTreeWidget):
))
snItem.setClass(dnItem.itemClass)
self.setTreeItemValues(sHandle)
self.propagateCount(sHandle, wCount)
# The items dropped into archive or trash should be removed
# from the project index
if onFree:
self.theParent.theIndex.deleteHandle(sHandle)
else:
self.theParent.theIndex.reIndexHandle(sHandle)
else:
logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
@@ -928,14 +956,15 @@ class GuiProjectTreeMenu(QMenu):
inTrash = theItem.parHandle == trashHandle
isTrash = theItem.itemHandle == trashHandle
isFile = theItem.itemType == nwItemType.FILE
isArch = theItem.itemClass == nwItemClass.ARCHIVE
isOrph = isFile and theItem.parHandle is None
showOpen = isFile
showView = isFile
showEdit = not isTrash and not isOrph
showExport = isFile and not inTrash and not isOrph
showNewFile = not isTrash and not inTrash and not isOrph
showNewFolder = not isTrash 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
showDelete = not isTrash
showEmpty = isTrash
+1
View File
@@ -514,6 +514,7 @@ class GuiIcons:
"cls_object" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
"cls_entity" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
"cls_custom" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
"cls_archive" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
"cls_trash" : (QStyle.SP_DriveHDIcon, "drive-harddisk"),
"proj_document" : (QStyle.SP_FileIcon, "x-office-document"),
"proj_folder" : (QStyle.SP_DirIcon, "folder"),
+5
View File
@@ -29,6 +29,11 @@
"NOVEL",
"Chapter Two"
],
[
"content/8a5deb88c0e97.nwd",
"NOVEL",
"Old File"
],
[
"content/96b68994dfa3d.nwd",
"NOVEL",
+1
View File
@@ -10,6 +10,7 @@
content/636b6aa9b697b.nwd NOVEL Making a Scene
content/6a2d6d5f4f401.nwd NOVEL Chapter One
content/88706ddc78b1b.nwd NOVEL Chapter Two
content/8a5deb88c0e97.nwd NOVEL Old File
content/96b68994dfa3d.nwd NOVEL A Note on Structure
content/974e400180a99.nwd NOVEL Page
content/ae7339df26ded.nwd NOVEL We Found John!
+4
View File
@@ -0,0 +1,4 @@
%%~ 8a5deb88c0e97:6827118336ac1:NOVEL:SCENE:Old File
### Discarded Scene
If you have files you no longer want in your main project, you can move them to the “Outtakes” folder. This is equivalent to turning off the “Include when building project” switch, just that you also put the file away, although the switch can be ignored when building the project, this folder cannot.
+33 -14
View File
@@ -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 11:37:18">
<novelWriterXML appVersion="0.12.1" hexVersion="0x001201f0" fileVersion="1.2" timeStamp="2020-08-16 17:50:53">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
<author>Jay Doh</author>
<saveCount>684</saveCount>
<autoCount>125</autoCount>
<editTime>33351</editTime>
<saveCount>713</saveCount>
<autoCount>138</autoCount>
<editTime>35134</editTime>
</project>
<settings>
<doBackup>False</doBackup>
@@ -15,8 +15,8 @@
<autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited>
<lastViewed>636b6aa9b697b</lastViewed>
<lastWordCount>1022</lastWordCount>
<novelWordCount>646</novelWordCount>
<lastWordCount>1071</lastWordCount>
<novelWordCount>695</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="22">
<content count="24">
<item handle="7031beac91f75" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
@@ -119,7 +119,7 @@
<charCount>1811</charCount>
<wordCount>318</wordCount>
<paraCount>8</paraCount>
<cursorPos>1880</cursorPos>
<cursorPos>1364</cursorPos>
</item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>Another Scene</name>
@@ -137,13 +137,13 @@
<name>Interlude</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Finished</status>
<status>New</status>
<exported>True</exported>
<layout>UNNUMBERED</layout>
<charCount>633</charCount>
<wordCount>101</wordCount>
<paraCount>3</paraCount>
<cursorPos>1238</cursorPos>
<cursorPos>0</cursorPos>
</item>
<item handle="96b68994dfa3d" order="4" parent="e7ded148d6e4a">
<name>A Note on Structure</name>
@@ -262,7 +262,26 @@
<paraCount>1</paraCount>
<cursorPos>45</cursorPos>
</item>
<item handle="98acd8c76c93a" order="3" parent="None">
<item handle="6827118336ac1" order="3" parent="None">
<name>Outtakes</name>
<type>ROOT</type>
<class>ARCHIVE</class>
<status>None</status>
<expanded>True</expanded>
</item>
<item handle="8a5deb88c0e97" order="0" parent="6827118336ac1">
<name>Old File</name>
<type>FILE</type>
<class>NOVEL</class>
<status>1st Draft</status>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>315</charCount>
<wordCount>55</wordCount>
<paraCount>1</paraCount>
<cursorPos>322</cursorPos>
</item>
<item handle="98acd8c76c93a" order="4" parent="None">
<name>Trash</name>
<type>TRASH</type>
<class>TRASH</class>
@@ -276,9 +295,9 @@
<status>New</status>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>30</charCount>
<wordCount>6</wordCount>
<paraCount>1</paraCount>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>36</cursorPos>
</item>
</content>