# -*- coding: utf-8 -*-
"""novelWriter GUI Project Tree

 novelWriter – GUI project Tree
================================
 Class holding the project tree view

 File History:
 Created: 2018-09-29 [0.0.1] GuiProjectTree
 Created: 2020-06-04 [0.7.0] GuiProjectTreeMenu

 This file is a part of novelWriter
 Copyright 2018–2020, Veronica Berglyd Olsen

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

import nw
import logging

from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (
    qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMessageBox,
    QMenu, QAction
)

from nw.core import NWDoc
from nw.constants import (
    nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst
)

logger = logging.getLogger(__name__)

class GuiProjectTree(QTreeWidget):

    C_NAME   = 0
    C_COUNT  = 1
    C_EXPORT = 2
    C_FLAGS  = 3

    def __init__(self, theParent):
        QTreeWidget.__init__(self, theParent)

        logger.debug("Initialising GuiProjectTree ...")

        self.mainConf   = nw.CONFIG
        self.theParent  = theParent
        self.theTheme   = theParent.theTheme
        self.theProject = theParent.theProject
        self.theIndex   = theParent.theIndex

        # Tree Settings
        self.theMap      = None
        self.orphRoot    = None
        self.treeChanged = False

        self.ctxMenu = GuiProjectTreeMenu(self)
        self.clearTree()

        # Build GUI
        iPx = self.theTheme.baseIconSize
        self.setIconSize(QSize(iPx, iPx))
        self.setExpandsOnDoubleClick(True)
        self.setIndentation(iPx)
        self.setColumnCount(4)
        self.setHeaderLabels(["Label", "Words", "Inc", "Flags"])
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self._rightClickMenu)

        treeHeadItem = self.headerItem()
        treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
        treeHeadItem.setToolTip(self.C_NAME, "Item label")
        treeHeadItem.setToolTip(self.C_COUNT, "Word count")
        treeHeadItem.setToolTip(self.C_EXPORT, "Include in build")
        treeHeadItem.setToolTip(self.C_FLAGS, "Status, class, and layout flags")

        # Let the last column stretch, and set the minimum size to the
        # size of the icon as the default Qt font metrics approach fails
        # for some fonts like the Ubuntu font.
        treeHeader = self.header()
        treeHeader.setStretchLastSection(True)
        treeHeader.setMinimumSectionSize(iPx + 6)

        # Allow Move by Drag & Drop
        self.setDragEnabled(True)
        self.setDragDropMode(QAbstractItemView.InternalMove)

        # But don't allow drop on root level
        trRoot = self.invisibleRootItem()
        trRoot.setFlags(trRoot.flags() ^ Qt.ItemIsDropEnabled)

        # Set Multiple Selection by CTRL
        # Disabled for now, until the merge files option has been added
        # self.setSelectionMode(QAbstractItemView.ExtendedSelection)
        # self.setSelectionBehavior(QAbstractItemView.SelectRows)

        # Get user's column width preferences for NAME and COUNT
        treeColWidth = self.mainConf.getTreeColWidths()
        if len(treeColWidth) <= 4:
            for colN, colW in enumerate(treeColWidth):
                self.setColumnWidth(colN, colW)

        # The last column should just auto-scale
        self.resizeColumnToContents(self.C_FLAGS)

        # Set custom settings
        self.initTree()

        logger.debug("GuiProjectTree initialisation complete")

        # Internal Mapping
        self.makeAlert = self.theParent.makeAlert

        return

    def initTree(self):
        """Set or update tree widget settings.
        """
        # Scroll bars
        if self.mainConf.hideVScroll:
            self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        else:
            self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)

        if self.mainConf.hideHScroll:
            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        else:
            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)

        return

    ##
    #  Class Methods
    ##

    def clearTree(self):
        """Clear the GUI content and the related maps.
        """
        self.clear()
        self.theMap = {}
        self.orphRoot = None
        return

    def newTreeItem(self, itemType, itemClass):
        """Add new item to the tree, with a given itemType and
        itemClass, and attach it to the selected handle. Also make sure
        the item is added in a place it can be added, and that other
        meta data is set correctly to ensure a valid project tree.
        """
        pHandle = self.getSelectedHandle()
        nHandle = None

        if not self.theParent.hasProject:
            logger.error("No project open")
            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:
                itemClass = pItem.itemClass

        if itemClass is None:
            if itemType is not None:
                if itemType == nwItemType.FILE:
                    self.makeAlert(
                        "Please select a valid location in the tree to add a document.",
                        nwAlert.ERROR
                    )
                    return False
                elif itemType == nwItemType.FOLDER:
                    self.makeAlert(
                        "Please select a valid location in the tree to add a folder.",
                        nwAlert.ERROR
                    )
                    return False
            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))
        )

        if itemType == nwItemType.ROOT:
            tHandle = self.theProject.newRoot(nwLabels.CLASS_NAME[itemClass], itemClass)
            if tHandle is None:
                logger.error("No root item added")
                return False

        else:
            # If no parent has been selected, make the new file under
            # the root NOVEL item.
            if pHandle is None:
                pHandle = self.theProject.projTree.findRoot(nwItemClass.NOVEL)

            # If still nothing, give up
            if pHandle is None:
                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
            # the new file will be a sibling
            pItem = self.theProject.projTree[pHandle]
            if pItem.itemType == nwItemType.FILE:
                nHandle = pHandle
                pHandle = pItem.itemParent

            # If we again have no home, give up
            if pHandle is None:
                self.makeAlert(
                    "Did not find anywhere to add the file or folder!", nwAlert.ERROR
                )
                return False

            if self.theProject.projTree.isTrashRoot(pHandle):
                self.makeAlert(
                    "Cannot add new files or folders to the %s folder." % (
                        nwLabels.CLASS_NAME[nwItemClass.TRASH]
                    ), 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.MAX_DEPTH - 1:
                    # Folders cannot be deeper than MAX_DEPTH - 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

        # Add the new item to the tree
        if tHandle is not None:
            self.revealNewTreeItem(tHandle, nHandle)
            if self.mainConf.showGUI:
                self.theParent.editItem(tHandle)

        return True

    def revealNewTreeItem(self, tHandle, nHandle=None):
        """Reveal a newly added project item in the project tree.
        """
        nwItem = self.theProject.projTree[tHandle]
        trItem = self._addTreeItem(nwItem, nHandle)
        pHandle = nwItem.itemParent
        if pHandle is not None and pHandle in self.theMap:
            self.theMap[pHandle].setExpanded(True)
        self.clearSelection()
        trItem.setSelected(True)
        return True

    def moveTreeItem(self, nStep):
        """Move an item up or down in the tree, but only if the treeView
        has focus. This also applies when the menu is used.
        """
        if not self.theParent.hasProject:
            logger.error("No project open")
            return False

        hasFocus = qApp.focusWidget() == self or not self.mainConf.showGUI
        if hasFocus and self.theParent.hasProject:

            tHandle = self.getSelectedHandle()
            tItem = self._getTreeItem(tHandle)
            pItem = tItem.parent()
            if pItem is None:
                tIndex = self.indexOfTopLevelItem(tItem)
                nChild = self.topLevelItemCount()
                nIndex = tIndex + nStep
                if nIndex < 0 or nIndex >= nChild:
                    return False
                cItem = self.takeTopLevelItem(tIndex)
                self.insertTopLevelItem(nIndex, cItem)

            else:
                tIndex = pItem.indexOfChild(tItem)
                nChild = pItem.childCount()
                nIndex = tIndex + nStep
                if nIndex < 0 or nIndex >= nChild:
                    return False
                cItem = pItem.takeChild(tIndex)
                pItem.insertChild(nIndex, cItem)

            self.clearSelection()
            cItem.setSelected(True)
            self._setTreeChanged(True)

        else:
            return False

        return True

    def saveTreeOrder(self):
        """Build a list of the items in the project tree and send them
        to the project class. This syncs up the two versions of the
        project structure, and must be called before any code that
        depends on this order to be up to date.
        """
        theList = []
        for i in range(self.topLevelItemCount()):
            if self.topLevelItem(i) == self.orphRoot:
                continue
            theList = self._scanChildren(theList, self.topLevelItem(i), i)
        logger.debug("Saving project tree item order")
        self.theProject.setTreeOrder(theList)
        return True

    def flushTreeOrder(self):
        """Calls saveTreeOrder if there are unsaved changes, otherwise
        does nothing.
        """
        if self.treeChanged:
            logger.verbose("Flushing project tree to project class")
            self.saveTreeOrder()
            self._setTreeChanged(False)
        return

    def getTreeFromHandle(self, tHandle):
        """Recursively return all the children items starting from a
        given item handle.
        """
        theList = []
        theItem = self._getTreeItem(tHandle)
        if theItem is not None:
            theList = self._scanChildren(theList, theItem, 0)
        return theList

    def getColumnSizes(self):
        """Return the column widths for the tree columns.
        """
        retVals = [
            self.columnWidth(0),
            self.columnWidth(1),
            self.columnWidth(2),
        ]
        return retVals

    def emptyTrash(self):
        """Permanently delete all documents in the Trash folder. This
        function only asks for confirmation once, and calls the regular
        deleteItem function for each document in the Trash folder.
        """
        if not self.theParent.hasProject:
            logger.error("No project open")
            return False

        trashHandle = self.theProject.projTree.trashRoot()

        logger.debug("Emptying Trash folder")
        if trashHandle is None:
            self.makeAlert(
                "There is currently no Trash folder in this project.", nwAlert.INFO
            )
            return False

        theTrash = self.getTreeFromHandle(trashHandle)
        if trashHandle in theTrash:
            theTrash.remove(trashHandle)

        nTrash = len(theTrash)
        if nTrash == 0:
            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" if nTrash > 1 else ""
            )
        )
        if msgRes != QMessageBox.Yes:
            return False

        logger.verbose("Deleting %d files from Trash" % nTrash)
        for tHandle in self.getTreeFromHandle(trashHandle):
            if tHandle == trashHandle:
                continue
            self.deleteItem(tHandle, True)

        if nTrash > 0:
            self._setTreeChanged(True)

        return True

    def deleteItem(self, tHandle=None, alreadyAsked=False, askForTrash=False):
        """Delete an item from the project tree. As a first step, files are
        moved to the Trash folder. Permanent deletion is a second step. This
        second step also deletes the item from the project object as well as
        delete the files on disk. Folders are deleted if they're empty only,
        and the deletion is always permanent.
        """
        if not self.theParent.hasProject:
            logger.error("No project open")
            return False

        if tHandle is None:
            tHandle = self.getSelectedHandle()

        if tHandle is None:
            return False

        trItemS = self._getTreeItem(tHandle)
        nwItemS = self.theProject.projTree[tHandle]

        if nwItemS is None:
            return False

        wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole))
        if nwItemS.itemType == nwItemType.FILE:
            logger.debug("User requested file %s moved to trash" % tHandle)
            trItemP = trItemS.parent()
            trItemT = self._addTrashRoot()
            if trItemP is None or trItemT is None:
                logger.error("Could not delete item")
                return False

            pHandle = nwItemS.itemParent
            if self.theProject.projTree.isTrashRoot(pHandle):
                # If the file is in the trash folder already, as the
                # user if they want to permanently delete the file.
                doPermanent = False
                if not alreadyAsked:
                    msgBox = QMessageBox()
                    msgRes = msgBox.question(
                        self, "Delete File", "Permanently delete file '%s'?" % nwItemS.itemName
                    )
                    if msgRes == QMessageBox.Yes:
                        doPermanent = True
                else:
                    doPermanent = True

                if doPermanent:
                    logger.debug("Permanently deleting file with handle %s" % tHandle)

                    self.propagateCount(tHandle, 0)
                    tIndex = trItemP.indexOfChild(trItemS)
                    trItemC = trItemP.takeChild(tIndex)

                    if self.theParent.docEditor.theHandle == tHandle:
                        self.theParent.closeDocument()

                    theDoc = NWDoc(self.theProject, self.theParent)
                    theDoc.deleteDocument(tHandle)
                    del self.theProject.projTree[tHandle]
                    self.theIndex.deleteHandle(tHandle)

            else:
                # The file is not already in the trash folder, so we
                # move it there.
                doTrash = False
                if askForTrash:
                    msgBox = QMessageBox()
                    msgRes = msgBox.question(
                        self, "Delete File", "Move file '%s' to Trash?" % nwItemS.itemName
                    )
                    if msgRes == QMessageBox.Yes:
                        doTrash = True
                else:
                    doTrash = True

                if doTrash:
                    if pHandle is None:
                        logger.warning("File has no parent item")

                    self.propagateCount(tHandle, 0)
                    tIndex  = trItemP.indexOfChild(trItemS)
                    trItemC = trItemP.takeChild(tIndex)
                    trItemT.addChild(trItemC)
                    nwItemS.setParent(self.theProject.projTree.trashRoot())
                    self.propagateCount(tHandle, wCount)

                    self._setTreeChanged(True)
                    self.theIndex.deleteHandle(tHandle)

        elif nwItemS.itemType == nwItemType.FOLDER:
            logger.debug("User requested folder %s deleted" % tHandle)
            trItemP = trItemS.parent()
            if trItemP is None:
                logger.error("Could not delete folder")
                return False
            tIndex = trItemP.indexOfChild(trItemS)
            if trItemS.childCount() == 0:
                trItemP.takeChild(tIndex)
                del self.theProject.projTree[tHandle]
            else:
                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:
            logger.debug("User requested root folder %s deleted" % tHandle)
            tIndex = self.indexOfTopLevelItem(trItemS)
            if trItemS.childCount() == 0:
                self.takeTopLevelItem(tIndex)
                del self.theProject.projTree[tHandle]
                self.theParent.mainMenu.setAvailableRoot()
                self._setTreeChanged(True)
            else:
                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

    def setTreeItemValues(self, tHandle):
        """Set the name and flag values for a tree item.
        """
        trItem  = self._getTreeItem(tHandle)
        nwItem  = self.theProject.projTree[tHandle]
        tName   = nwItem.itemName
        tClass  = nwItem.itemClass
        tHandle = nwItem.itemHandle

        expIcon = QIcon()

        stClass = nwLabels.CLASS_FLAG[nwItem.itemClass]
        stLayout = ""
        if nwItem.itemType == nwItemType.FILE:
            stLayout = "."+nwLabels.LAYOUT_FLAG[nwItem.itemLayout]
            if nwItem.isExported:
                expIcon = self.theTheme.getIcon("check")
            else:
                expIcon = self.theTheme.getIcon("cross")
        tStatus = stClass+stLayout

        iStatus = nwItem.itemStatus
        if tClass == nwItemClass.NOVEL:
            iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid
            flagIcon = self.theParent.statusIcons[iStatus]
        else:
            iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid
            flagIcon = self.theParent.importIcons[iStatus]

        trItem.setText(self.C_NAME, tName)
        trItem.setIcon(self.C_EXPORT, expIcon)
        trItem.setText(self.C_FLAGS, tStatus)
        trItem.setIcon(self.C_FLAGS, flagIcon)

        return

    def propagateCount(self, tHandle, theCount, nDepth=0):
        """Recursive function setting the word count for a given item,
        and propagating that count upwards in the tree until reaching a
        root item. This function is more efficient than recalculating
        everything each time the word count is updated, but is also
        prone to diverging from the true values if the counts are not
        properly reported to the function.
        """
        tItem = self._getTreeItem(tHandle)
        if tItem is not None:
            tItem.setText(self.C_COUNT, f"{theCount:n}")
            tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount))
            pItem = tItem.parent()
            if pItem is not None:
                pCount = 0
                for i in range(pItem.childCount()):
                    pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
                    pHandle = pItem.data(self.C_NAME, Qt.UserRole)

                if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "":
                    self.propagateCount(pHandle, pCount, nDepth+1)

        return

    def projectWordCount(self):
        """Sum up the word counts for all root items and set the
        relevant values in the project and on the status bar. This call
        is a fast way of getting this number, and depends on the
        propagateCount function being called when it should to maintain
        the correct count. Orphan folder is not included in the total.
        """
        nWords = 0
        for n in range(self.topLevelItemCount()):
            tItem = self.topLevelItem(n)
            if tItem == self.orphRoot:
                continue
            nWords += int(tItem.data(self.C_COUNT, Qt.UserRole))

        self.theProject.setProjectWordCount(nWords)
        sWords = self.theProject.getSessionWordCount()
        self.theParent.statusBar.setStats(nWords, sWords)

        return

    def buildTree(self):
        """Build the entire project tree from scratch. This depends on
        the save project item iterator in the project class which will
        always make sure items with a parent have had their parent item
        sent first.
        """
        logger.debug("Building the project tree ...")
        self.clear()
        iCount = 0

        for nwItem in self.theProject.getProjectItems():
            iCount += 1
            self._addTreeItem(nwItem)

        logger.debug("%d items added to the project tree" % iCount)
        return True

    def getSelectedHandle(self):
        """Get the currently selected handle. If multiple items are
        selected, return the first.
        """
        selItem = self.selectedItems()

        if len(selItem) == 0:
            return None

        if isinstance(selItem[0], QTreeWidgetItem):
            return selItem[0].data(self.C_NAME, Qt.UserRole)

        return None

    def getSelectedHandles(self):
        """Return a list of all currently selected item handles.
        """
        selItems = self.selectedItems()
        selHandles = []
        for n in range(len(selItems)):
            if isinstance(selItems[n], QTreeWidgetItem):
                selHandles.append(selItems[n].data(self.C_NAME, Qt.UserRole))

        return selHandles

    def setSelectedHandle(self, tHandle, doScroll=False):
        """Set a specific handle as the selected item.
        """
        if tHandle in self.theMap:
            self.clearSelection()
            self.theMap[tHandle].setSelected(True)

            selItems = self.selectedIndexes()
            if selItems and doScroll:
                self.scrollTo(
                    selItems[0], QAbstractItemView.PositionAtCenter
                )
            return True

        return False

    ##
    #  Slots
    ##

    def _rightClickMenu(self, clickPos):
        """The user right clicked an element in the project tree, so we
        open a context menu in-place.
        """
        selItem = self.itemAt(clickPos)
        if isinstance(selItem, QTreeWidgetItem):
            tHandle = selItem.data(self.C_NAME, Qt.UserRole)
            tItem   = self.theProject.projTree[tHandle]
            self.setSelectedHandle(tHandle) # Just to be safe

            if self.ctxMenu.filterActions(tItem):
                # Only open menu if any actions remain after filter
                self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))

        return

    ##
    #  Events
    ##

    def mousePressEvent(self, theEvent):
        """Overload mousePressEvent to clear selection if clicking the
        mouse in a blank area of the tree view, and to load a document
        for viewing if the suer middle clicked.
        """
        QTreeWidget.mousePressEvent(self, theEvent)

        if theEvent.button() == Qt.LeftButton:
            selItem = self.indexAt(theEvent.pos())
            if not selItem.isValid():
                self.clearSelection()

        elif theEvent.button() == Qt.MiddleButton:
            selItem = self.itemAt(theEvent.pos())
            if not isinstance(selItem, QTreeWidgetItem):
                return

            tHandle = selItem.data(self.C_NAME, Qt.UserRole)
            tItem = self.theProject.projTree[tHandle]
            if tItem is None:
                return

            if tItem.itemType == nwItemType.FILE:
                self.theParent.viewDocument(tHandle)

        return

    def dropEvent(self, theEvent):
        """Overload the drop of dragged item event to check whether the
        drop is allowed or not. Disallowed drops are cancelled.
        """
        sHandle = self.getSelectedHandle()
        if sHandle is None:
            logger.error("No handle selected")
            return

        dIndex = self.indexAt(theEvent.pos())
        if not dIndex.isValid():
            logger.error("Invalid drop index")
            return

        sItem = self._getTreeItem(sHandle)
        dItem = self.itemFromIndex(dIndex)
        dHandle = dItem.data(self.C_NAME, Qt.UserRole)
        snItem = self.theProject.projTree[sHandle]
        dnItem = self.theProject.projTree[dHandle]
        if dnItem is None:
            self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
            return

        wCount = int(sItem.data(self.C_COUNT, Qt.UserRole))
        isSame = snItem.itemClass == dnItem.itemClass
        isNone = snItem.itemClass == nwItemClass.NO_CLASS
        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 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 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,
                    dnItem.itemClass.name
                ))
                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, for all other items, we rescan the
            # file to ensure the index is up to date.
            if onFree:
                self.theIndex.deleteHandle(sHandle)
            else:
                self.theIndex.reIndexHandle(sHandle)

        else:
            theEvent.ignore()
            logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
            self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)

        return

    ##
    #  Internal Functions
    ##

    def _getTreeItem(self, tHandle):
        """Returns the QTreeWidgetItem of a given item handle.
        """
        if tHandle in self.theMap.keys():
            return self.theMap[tHandle]
        return None

    def _scanChildren(self, theList, theItem, theIndex):
        """This is a recursive function returning all items in a tree
        starting at a given QTreeWidgetItem.
        """
        tHandle = theItem.data(self.C_NAME, Qt.UserRole)
        nwItem = self.theProject.projTree[tHandle]
        nwItem.setExpanded(theItem.isExpanded())
        nwItem.setOrder(theIndex)
        theList.append(tHandle)
        for i in range(theItem.childCount()):
            self._scanChildren(theList, theItem.child(i), i)
        return theList

    def _addTreeItem(self, nwItem, nHandle=None):
        """Create a QTreeWidgetItem from an NWItem and add it to the
        project tree.
        """
        tHandle = nwItem.itemHandle
        pHandle = nwItem.itemParent
        tClass  = nwItem.itemClass
        newItem = QTreeWidgetItem([""]*4)

        newItem.setText(self.C_NAME,   "")
        newItem.setText(self.C_COUNT,  "0")
        newItem.setText(self.C_EXPORT, "")
        newItem.setText(self.C_FLAGS,  "")

        newItem.setTextAlignment(self.C_NAME,   Qt.AlignLeft  | Qt.AlignVCenter)
        newItem.setTextAlignment(self.C_COUNT,  Qt.AlignRight | Qt.AlignVCenter)
        newItem.setTextAlignment(self.C_EXPORT, Qt.AlignLeft  | Qt.AlignVCenter)
        newItem.setTextAlignment(self.C_FLAGS,  Qt.AlignLeft  | Qt.AlignVCenter)

        newItem.setData(self.C_NAME, Qt.UserRole, tHandle)
        newItem.setData(self.C_COUNT, Qt.UserRole, 0)

        self.theMap[tHandle] = newItem
        if pHandle is None:
            if nwItem.itemType == nwItemType.ROOT:
                self.addTopLevelItem(newItem)
                self.theParent.mainMenu.setAvailableRoot()
            elif nwItem.itemType == nwItemType.TRASH:
                self.addTopLevelItem(newItem)
            else:
                self._addOrphanedRoot()
                self.orphRoot.addChild(newItem)
        else:
            byIndex = -1
            if nHandle is not None and nHandle in self.theMap:
                try:
                    byIndex = self.theMap[pHandle].indexOfChild(self.theMap[nHandle])
                except Exception:
                    logger.error("Failed to get index of item with handle %s" % nHandle)
            if byIndex >= 0:
                self.theMap[pHandle].insertChild(byIndex+1, newItem)
            else:
                self.theMap[pHandle].addChild(newItem)
            self.propagateCount(tHandle, nwItem.wordCount)

        self.setTreeItemValues(tHandle)
        newItem.setExpanded(nwItem.isExpanded)

        if nwItem.itemType == nwItemType.ROOT:
            newItem.setIcon(self.C_NAME, self.theTheme.getIcon(nwLabels.CLASS_ICON[tClass]))
        elif nwItem.itemType == nwItemType.FOLDER:
            newItem.setIcon(self.C_NAME, self.theTheme.getIcon("proj_folder"))
        elif nwItem.itemType == nwItemType.FILE:
            newItem.setIcon(self.C_NAME, self.theTheme.getIcon("proj_document"))
        elif nwItem.itemType == nwItemType.TRASH:
            newItem.setIcon(self.C_NAME, self.theTheme.getIcon(nwLabels.CLASS_ICON[tClass]))

        self._setTreeChanged(True)

        return newItem

    def _addTrashRoot(self):
        """Adds the trash root folder if it doesn't already exist in the
        project tree.
        """
        trashHandle = self.theProject.trashFolder()
        if trashHandle is None:
            return None

        trItem = self._getTreeItem(trashHandle)
        if trItem is None:
            trItem = self._addTreeItem(
                self.theProject.projTree[trashHandle]
            )
            trItem.setExpanded(True)
            self._setTreeChanged(True)

        return trItem

    def _addOrphanedRoot(self):
        """Add the special Orphaned Files root item to hold non-root
        items with no parent set.
        """
        if self.orphRoot is None:
            newItem = QTreeWidgetItem([""]*4)
            newItem.setText(self.C_NAME,   "Orphaned Files")
            newItem.setText(self.C_COUNT,  "")
            newItem.setText(self.C_EXPORT, "")
            newItem.setText(self.C_FLAGS,  "")
            self.addTopLevelItem(newItem)
            self.orphRoot = newItem
            newItem.setExpanded(True)
            newItem.setData(self.C_NAME, Qt.UserRole, "")
            newItem.setData(self.C_COUNT, Qt.UserRole, 0)
            newItem.setIcon(self.C_NAME, self.theTheme.getIcon("proj_orphan"))

        return

    def _cleanOrphanedRoot(self):
        """Remove the special Orphaned Files root folder if it is empty.
        """
        if self.orphRoot is not None:
            if self.orphRoot.childCount() == 0:
                self.takeTopLevelItem(self.indexOfTopLevelItem(self.orphRoot))
                self.orphRoot = None
        return

    def _updateItemParent(self, tHandle):
        """Update the parent handle of an item so that the information
        in the project is consistent with the treeView.
        """
        trItemS = self._getTreeItem(tHandle)
        nwItemS = self.theProject.projTree[tHandle]
        trItemP = trItemS.parent()
        if trItemP is None:
            logger.error("Failed to find new parent item of %s" % tHandle)
            return False

        pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
        nwItemS.setParent(pHandle)
        self.setTreeItemValues(tHandle)
        self._setTreeChanged(True)

        logger.debug("The parent of item %s has been changed to %s" % (tHandle, pHandle))

        return True

    def _moveOrphanedItem(self, tHandle, dHandle):
        """Move an Orphaned Item to a new dHandle parent item. This
        function will set all the missing meta data based on the meta
        data of the destination item.
        """
        trItemS = self._getTreeItem(tHandle)
        nwItemS = self.theProject.projTree[tHandle]
        nwItemD = self.theProject.projTree[dHandle]
        trItemP = trItemS.parent()
        nwItemS.setClass(nwItemD.itemClass)
        if trItemP is None:
            logger.error("Failed to find new parent item of %s" % tHandle)
            return False

        pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
        nwItemS.setParent(pHandle)
        self.setTreeItemValues(tHandle)
        self._setTreeChanged(True)

        return True

    def _setTreeChanged(self, theState):
        """Set the tree change flag, and propagate to the project.
        """
        self.treeChanged = theState
        if theState:
            self.theProject.setProjectChanged(True)
        return

# END Class GuiProjectTree

class GuiProjectTreeMenu(QMenu):

    def __init__(self, theTree):
        QMenu.__init__(self, theTree)

        self.theTree = theTree
        self.theItem = None

        self.openItem = QAction("Open Document", self)
        self.openItem.triggered.connect(self._doOpenItem)
        self.addAction(self.openItem)

        self.viewItem = QAction("View Document", self)
        self.viewItem.triggered.connect(self._doViewItem)
        self.addAction(self.viewItem)

        self.editItem = QAction("Edit Project Item", self)
        self.editItem.triggered.connect(self._doEditItem)
        self.addAction(self.editItem)

        self.toggleExp = QAction("Toggle Included Flag", self)
        self.toggleExp.triggered.connect(self._doToggleExported)
        self.addAction(self.toggleExp)

        self.newFile = QAction("New File", self)
        self.newFile.triggered.connect(self._doMakeFile)
        self.addAction(self.newFile)

        self.newFolder = QAction("New Folder", self)
        self.newFolder.triggered.connect(self._doMakeFolder)
        self.addAction(self.newFolder)

        self.deleteItem = QAction("Delete Item", self)
        self.deleteItem.triggered.connect(self._doDeleteItem)
        self.addAction(self.deleteItem)

        self.emptyTrash = QAction("Empty Trash", self)
        self.emptyTrash.triggered.connect(self._doEmptyTrash)
        self.addAction(self.emptyTrash)

        return

    def filterActions(self, theItem):
        """Update item settings from the nwItem.
        """
        self.theItem = theItem
        theRoot = self.theTree.theProject.projTree.getRootItem(theItem.itemHandle)

        if theItem is None or theRoot is None:
            logger.error("Failed to extract information to build tree context menu")
            return False

        trashHandle = self.theTree.theProject.projTree.trashRoot()

        inTrash = theItem.itemParent == trashHandle and trashHandle is not None
        isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
        isFile  = theItem.itemType == nwItemType.FILE
        isArch  = theRoot.itemClass == nwItemClass.ARCHIVE
        isOrph  = isFile and theItem.itemParent is None

        showOpen      = isFile
        showView      = isFile
        showEdit      = not isTrash and not isOrph
        showExport    = isFile
        showNewFile   = not (isTrash or inTrash or isOrph or isArch)
        showNewFolder = not (isTrash or inTrash or isOrph)
        showDelete    = not isTrash
        showEmpty     = isTrash

        self.openItem.setVisible(showOpen)
        self.viewItem.setVisible(showView)
        self.editItem.setVisible(showEdit)
        self.toggleExp.setVisible(showExport)
        self.newFile.setVisible(showNewFile)
        self.newFolder.setVisible(showNewFolder)
        self.deleteItem.setVisible(showDelete)
        self.emptyTrash.setVisible(showEmpty)

        return True

    ##
    #  Slots
    ##

    def _doOpenItem(self):
        """Forward the open document call to the main GUI window.
        """
        if self.theItem is not None:
            self.theTree.theParent.openDocument(self.theItem.itemHandle, doScroll=False)
        return

    def _doViewItem(self):
        """Forward the view document call to the main GUI window.
        """
        if self.theItem is not None:
            self.theTree.theParent.viewDocument(self.theItem.itemHandle)
        return

    def _doEditItem(self):
        """Forward the edit item call to the main GUI window.
        """
        if self.theItem is not None:
            self.theTree.theParent.editItem()
        return

    def _doMakeFile(self):
        """Forward the new file call to the project tree.
        """
        if self.theItem is not None:
            self.theTree.newTreeItem(nwItemType.FILE, None)
        return

    def _doMakeFolder(self):
        """Forward the new folder call to the project tree.
        """
        if self.theItem is not None:
            self.theTree.newTreeItem(nwItemType.FOLDER, None)
        return

    def _doToggleExported(self):
        """Flip the isExported flag of the current item.
        """
        if self.theItem is not None:
            self.theItem.setExported(not self.theItem.isExported)
            self.theTree.setTreeItemValues(self.theItem.itemHandle)
        return

    def _doDeleteItem(self):
        """Forward the delete item call to the project tree.
        """
        if self.theItem is not None:
            self.theTree.deleteItem(askForTrash=True)
        return

    def _doEmptyTrash(self):
        """Forward the delete item call to the project tree.
        """
        self.theTree.emptyTrash()
        return

# END Class GuiProjectTreeMenu
