#
# Tis file is part of TreeTime, a tree editor and data analyser
#
# Copyright (C) GPLv3, 2015, Jacob Kanev
# Tree folding icons by Alexhuszagh/BreezeStyleSheets, slightly modified
#
# 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA

# -*- coding:utf-8 -*-

#!/usr/bin/python3

import sys

from .tree import *
from .mainwindow import *
import base64
import datetime
import time
import netifaces
import requests
import os.path
import pathlib
import platform
import sys
import os
from PyQt6 import QtCore, QtGui, QtWidgets
from threading import Timer, Thread
from PyQt6.QtGui import QPalette, QColor, QIcon, QClipboard, QGuiApplication
from PyQt6.QtWidgets import QAbstractItemView
from werkzeug.wrappers import Request as WzRequest, Response as WzResponse
from werkzeug.serving import make_server


# Use only for debugging purposes (to cause an error on purpose, if you feel there might be loops), can cause segfaults
# sys.setrecursionlimit(50)

def errorMessage(message):
    """ Helper function for displaying error message box
    :param message: The text to display in the box
    """
    print(message)
    msgBox = QtWidgets.QMessageBox()
    msgBox.setText(message)
    msgBox.setWindowTitle("TreeTime Error")
    msgBox.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok)
    result = msgBox.exec()


class QNode(QtWidgets.QTreeWidgetItem):
    """
    The GUI counterpart of a node. Displays the contents of a node.
    """
    
    def __init__(self, sourceNode, fieldOrder):
        
        # initialise display
        self.sourceNode = sourceNode
        displayStrings = [sourceNode.name]
        for d in fieldOrder:
            if d in self.sourceNode.fields:
                displayStrings += [self.sourceNode.fields[d].getString()]
            else:
                displayStrings += [""]
                
        super().__init__(displayStrings)
        
        # build reverse field order dictionary
        self.notifyFieldOrderChange(fieldOrder)

        # recurse
        for c in sourceNode.children:
            child = QNode(c, fieldOrder)
            super().addChild(child)
        
        # register callbacks
        self.registerCallbacks()

    def parent(self):
        """
        Returns the parent of a node
        """
        return super().parent() or self.treeWidget().invisibleRootItem() or None

    def registerCallbacks(self):
        """
        Registers QNode-specific callbacks with the source node
        """
        self.sourceNode.registerNameChangeCallback(self.notifyNameChange)
        self.sourceNode.registerFieldChangeCallback(self.notifyFieldChange)
        self.sourceNode.registerDeletionCallback(self.notifyDeletion)
        self.sourceNode.registerSelectionCallback(lambda x: self.notifySelection(x))
        self.sourceNode.registerMoveCallback(self.notifyMove)
        self.sourceNode.registerViewNode(self)
        self.sourceNode.registerFieldNameChangeCallback(self.notifyFieldNameChange)
        self.sourceNode.registerFieldOrderChangeCallback(self.notifyFieldOrderChange)

    def notifyNameChange(self, newName):
        self.setText(0, newName)

    def notifyFieldChange(self, fieldName, fieldContent):
        if fieldName in self.fieldOrder:
            self.setText(self.fieldOrder[fieldName], fieldContent)

    def notifyDeletion(self):
        # unlink node
        self.sourceNode = None
        
        # unlink from parent
        if self.parent() is not None:
            self.parent().removeChild(self)
        else:
            print("I have no parent, so I'll stay.")

    def notifyMove(self):

        # unlink from parent
        self.parent().removeChild(self)
        newParent = self.sourceNode.parent.viewNode
        newParent.addChild(self)

    def notifySelection(self, select):
        self.setSelected(select)

    def notifyFieldNameChange(self, n, newName):
        """ Updates the name of a field in the reverse-field-order dict and in the main window
        :param n: Field index
        :param newName: New name of field with index n, of False if field is being removed
        :return: void
        """
        [oldName] = [k for k, i in self.fieldOrder.items() if i==(n+1)] or [False]
        if oldName:
            self.fieldOrder.pop(oldName)

        # update field order if new field exists
        if newName and newName in self.fieldOrder:
            self.fieldOrder[newName] = n

        # field has been removed
        else:
            # renumber field if field has been removed (newName = False)
            self.fieldOrder = {name:index for name, index in
                               zip([f for f,n in self.fieldOrder.items()],
                                   range(1,len(self.fieldOrder)+1))}

            # update all subsequent fields by taking them one number down
            for k in range(n+1, len(self.fieldOrder)+1):
                self.setText(k, self.text(k+1))




    def notifyFieldOrderChange(self, fieldOrder):
        """ Updates the reverse-field-order dict
        :param n: Field index
        :param newName: New name of field with index n
        :return: void
        """
        self.fieldOrder = {}

        # set to -1 to hide hidden fields
        for k in self.fieldOrder.keys():
            self.fieldOrder[k] = -1

        # re-enumerate, leaving hidden fields hidden
        for i, f in enumerate(fieldOrder):
            self.fieldOrder[f] = i+1     # plus one because column 0 is the node name


class QMetaNode(QtWidgets.QTreeWidgetItem):
    """
    The GUI object for displaying definitions of trees, fields, data items.
    Each node has a pointer to its source (central tree field, default data item field, etc) and changes them directly,
    then calls appropriate functions in the forest to update all other nodes/items.
    """

    def __init__(self, source, forest, name='', parentName='', grandParentName='', nodeType=''):

        # initialise display
        self.source = source
        self.forest = forest
        self.nodeType = nodeType
        self.name = name
        self.parentName = parentName
        self.grandParentName = grandParentName
        if nodeType == 'tree':
            self.contentType = nodeType
            displayStrings = [source.name, 'Tree']
        elif nodeType == 'tree field':
            self.contentType = source.fieldType
            displayStrings = [name, source.hidden and source.fieldType  + ' (hidden)' or source.fieldType]
        elif nodeType == 'data field':
            self.contentType = source['type']
            displayStrings = [name, source['type']]
        elif nodeType=='parameter list':
            self.contentType = nodeType
            displayStrings = [name, f'{source}'[1:-1]]
        else:
            self.contentType = nodeType
            displayStrings = [name or source.name, nodeType]
        super().__init__(displayStrings)

        # recurse
        if nodeType=='tree':
            for name, field in source.fields.items():
                [index] = [n for n,x in enumerate(source.fieldOrder) if x==name] or [-1]
                child = QMetaNode(field, self.forest,
                                  name=name, parentName=self.name, grandParentName=self.parentName,
                                  nodeType='tree field')
                super().addChild(child)
        elif nodeType=='tree field':
            child1 = QMetaNode(source.ownFields, self.forest,
                               name='own-fields', parentName=self.name, grandParentName=self.parentName,
                               nodeType='parameter list')
            super().addChild(child1)
            child2 = QMetaNode(source.childFields, self.forest,
                               name='child-fields', parentName=self.name, grandParentName=self.parentName,
                               nodeType='parameter list')
            super().addChild(child2)
            child3 = QMetaNode(source.siblingFields, self.forest,
                               name='sibling-fields', parentName=self.name, grandParentName=self.parentName,
                               nodeType='parameter list')
            super().addChild(child3)
            child4 = QMetaNode(source.parentFields, self.forest,
                               name='parent-fields', parentName=self.name, grandParentName=self.parentName,
                               nodeType='parameter list')
            super().addChild(child4)
        elif nodeType=='data item':
            for nm, fld in source.fields.items():
                child = QMetaNode(fld, self.forest, name=nm, parentName=self.name, grandParentName=self.parentName,
                                  nodeType='data field')
                super().addChild(child)

    def changeName(self, newName):
        """
        The name of the field or tree has been changed in the GUI. The GUI calls this function, which checks for
        feasibility (some changes are not allowed), then changes the name in the QMetaNode, then hands down to the
        QNodes and/or items. This might change some field definitions, if that's the case the Tree.fields will
        send a definition-changed message to their respective QMetaNodes.
        Checks whether a user-edited name can be used for tree or field. In some cases names need to be unique.
        If the name is not okay, the user sees a message box, after they click OK this function returns.
        """

        # check name
        changeOk = False    # default for parameter lists
        message = None
        if self.nodeType == 'data item':
            changeOk = True
            message = None
        elif self.nodeType == 'tree':
            existingNames = [c.name for c in self.forest.children]
            if newName in existingNames:
                existingNames = f'{[n for n in existingNames if n != self.name]}'[1:-1]
                changeOk = False
                message = (f'Tree names must be unique. '
                           f'New tree name cannot be one of {existingNames}.')
            else:
                changeOk = True
        elif self.nodeType == 'tree field':
            existingNames = self.forest.listFieldNames(self.parent().name)
            if newName in existingNames:
                existingNames = f'{[n for n in existingNames if n != self.name]}'[1:-1]
                changeOk = False
                message = (f'A tree field names cannot be identical to another tree field in the same tree, nor a data'
                           f'field. New tree field name cannot be one of {existingNames}.')
            else:
                changeOk = True
        elif self.nodeType == 'data field':
            existingNames = self.forest.listFieldNames()
            if newName in existingNames:
                existingNames = f'{[n for n in existingNames if n != self.name]}'[1:-1]
                changeOk = False
                message = (f'A data field cannot be identical to another data or tree field. '
                           f'New data field name cannot be one of {existingNames}.')
            else:
                changeOk = True

        # if change allowed, change
        if changeOk:
            if self.nodeType == 'data item':
                self.source.changeName(newName)
                self.name = newName
            elif self.nodeType == 'tree':
                self.source.changeName(newName)
                self.name = newName
            elif self.nodeType == 'tree field':
                changedFields = self.forest.changeTreeFieldName(self.parentName, self.name, newName)
                self.name = newName
                self.parent().updateChildren()
            elif self.nodeType == 'data field':
                self.forest.changeDataFieldName(self.name, newName)
                root = self.parent().parent()
                self.name = newName
                for c in range(1, root.childCount()):
                    root.child(c).updateChildren()
            else:
                print(f"Name changing not implemented yet for type {self.nodeType}.")

            # publish main text
            super().setText(0, newName)

        # if change not allowed, notify user
        elif message:
            errorMessage("Name change is not possible.")

        return changeOk

    def changeType(self, newType):
        self.contentType = newType.removesuffix(' [numerical]').removesuffix(' [text]').removesuffix(' [any]')
        if self.nodeType == 'tree field':
            self.source.fieldType = self.contentType
            self.forest.updateTreeFieldType(self.parentName, self.name)
            super().setText(1, self.source.hidden and self.contentType + ' (hidden)' or self.contentType)
        else:
            self.source['type'] = self.contentType
            self.forest.updateDataFieldType(self.name)
            super().setText(1, self.contentType)
        return True

    def changeContent(self, newContent, index=-1):
        if self.nodeType == 'data field':
            self.source['content'] = newContent     # this is silent, no need to signal anyone
        elif self.nodeType == 'parameter list':
            currentEntries = len(self.source)
            if newContent:
                if self.parent().contentType in ('node-name', 'node-path'):
                    bare = newContent.removesuffix(' [tree]')
                    [bare] = [n for n,t in enumerate(self.forest.children) if t.name==bare]     # find index
                else:
                    bare = newContent.removesuffix(' [text]').removesuffix(' [numerical]').removesuffix(' [any]')
                if index >= currentEntries:
                    self.source += [bare]
                else:
                    self.source[index] = bare
            else:
                self.source.pop(index)
            self.forest.updateTreeFieldParameters(self.grandParentName, self.parentName, self.name)
            super().setText(1, f'{self.source}'[1:-1])
        else:
            print(f"Content change to {newContent} at index {index} not implemented yet for type {self.nodeType}.")
        return True

    def changeVisibility(self, newVisibility):
        """ Change the visibility of a node. Changes the visiblilty in the tree field directly, then notifies the
        forest to update all nodes
        :param newVisibility: Checkbox state (True/False)
        :return: True
        """
        if self.nodeType == 'tree field':
            self.source.hidden = (newVisibility == QtCore.Qt.CheckState.Checked)
            self.forest.updateTreeFieldVisibility(self.parentName, self.name)
        else:
            print(f"Visibility change to hidden={newVisibility and True} not implemented yet for type {self.nodeType}.")
        super().setText(1, self.source.hidden and self.source.fieldType + ' (hidden)' or self.source.fieldType)
        return True

    def newDataField(self):
        """ Add a new data field by calling the forest and then adding a new child/sibling node
        :return: void
        """
        name, field = self.forest.newDataField()
        if self.nodeType == 'data item':
            parent = self
        else:
            parent = self.parent()
        child = QMetaNode(field, self.forest, name=name, parentName=parent.name,
                          grandParentName=parent.parentName, nodeType='data field')
        parent.addChild(child)
        parent.setExpanded(True)
        return child

    def newTreeField(self):
        if self.nodeType == 'parameter list':
            parent = self.parent().parent()
        elif self.nodeType == 'tree field':
            parent = self.parent()
        else:
            parent = self
        name, field = self.forest.newTreeField(parent.name)
        child = QMetaNode(field, self.forest, name=name, parentName=parent.name,
                          grandParentName=parent.parentName, nodeType='tree field')
        parent.addChild(child)
        parent.setExpanded(True)
        return parent.name, child

    def newTree(self):
        if self.nodeType == 'parameter list':
            root = self.parent().parent().parent()
        elif self.nodeType == 'tree field':
            root = self.parent().parent()
        elif self.nodeType == 'tree':
            root = self.parent()
        tree = self.forest.newTree()
        node = QMetaNode(tree, self.forest, name=tree.name, parentName=tree.name,
                          grandParentName=tree.name, nodeType='tree')
        root.addChild(node)
        return tree, node

    def deleteDataField(self):
        """ Remove the current data field
        :return:
        """
        self.forest.deleteDataField(self.name)
        root = self.parent().parent()
        for c in range(1, root.childCount()):
            root.child(c).updateChildren()
        self.parent().removeChild(self)

    def deleteTreeField(self):
        fieldName = self.nodeType == 'tree field' and self.name or self.parentName
        treeName = self.nodeType == 'tree field' and self.parentName or self.grandParentName
        self.forest.deleteTreeField(treeName, fieldName)
        if self.nodeType == 'tree field':
            field = self
        else:
            field = self.parent()
        for c in range(0, field.childCount()):
            field.removeChild(self.child(c))
        field.parent().removeChild(self)
        return treeName

    def deleteTree(self):
        """ Delete a tree. This function is always called from the tree node, we disabled it from the param list and
        from the field node, because it can be too easily clicked and will remove quite some data.
        :return:
        """

        # get tree name
        treeName = self.name

        # remove parameters and fields
        for f in range(0, self.childCount()):
            field = self.child(f)
            for p in range(0, field.childCount()):
                plist = field.child(p)
                field.removeChild(plist)
        for f in range(0, self.childCount()):
            self.removeChild(self.child(f))

        # delete the tree structure and data
        n = self.forest.deleteTree(treeName)

        # remove myself
        self.parent().removeChild(self)
        return n

    def availableFields(self, exclude=None, datatype='any'):
        """ Get avaiable fields for combo box display. Only delivers data if this is a tree type.
        :param exclude: A field name to not have in the result list (to prevent recursion)
        :param datatype: Limit the list to a certain datatype. One of 'text', 'numerical', 'any' (default).
        :return: Two lists and two dictionaries.
                 1. Bare names
                 2. Annotated names (with type)
                 3. Dict bare→annotated
                 4. Dict annotated→bare
        """
        fields = []
        annotated = ['']
        bare = ['']

        if self.nodeType == 'tree':
            dataItem = self.parent().child(0).source

            # tuple for type checking
            checktype = datatype == 'any' and ('text', 'numerical', 'any') or (datatype)

            # collect tuples of fields and types, ommitig "exclude" items and matching "datatype"s.
            fields += [(f[0], Item.FieldTypes[f[1]['type']])
                       for f in dataItem.fields.items()
                       if f[0]!=exclude and Item.FieldTypes[f[1]['type']] in checktype]
            fields += [(f[0], Field.Types[f[1].fieldType])
                       for f in self.source.fields.items()
                       if f[0]!=exclude and Field.Types[f[1].fieldType] in checktype]

            # build result lists and dictionaries
            bare += [f[0] for f in fields]
            annotated += [f'{f[0]} [{f[1]}]' for f in fields]
        return bare, annotated, {f[0]:f[1] for f in zip(bare, annotated)}, {f[0]:f[1] for f in zip(annotated, bare)}

    def updateChildren(self):
        """
        Redisplays all children and adjusts changed names
        :return:
        """
        if self.nodeType in ('tree', 'tree field'):
            for c in range(self.childCount()):
                self.child(c).parentName = self.name
                self.child(c).grandParentName = self.parentName
                self.child(c).updateChildren()
        elif self.nodeType == "parameter list":
            super().setText(1, f'{self.source}'[1:-1])

    def parent(self):
        """
        Returns the parent of a node
        """
        return super().parent() or self.treeWidget().invisibleRootItem() or None

    def notifyDefinitionChange(self):
        if self.nodeType == "parameter list":
            super().setText(1, f'{self.source}'[1:-1])
        else:
            for c in range(self.childCount()):
                self.child(c).notifyDefinitionChange()


class UrlWidget(QtWidgets.QWidget):
    """
    Special custom widget class for URL fields
    """

    def __init__(self, url, callback, font, parent=None):
        """
        Initialise
        """

        # init
        QtWidgets.QWidget.__init__(self, parent=parent)
        self.url = url
        self.callback = callback

        # create line edit control and open button
        linewidget = QtWidgets.QLineEdit(url)
        linewidget.setFont(font)
        linewidget.textChanged.connect(self.textChanged)
        openbutton = QtWidgets.QPushButton("Open")
        openbutton.setFont(font)
        openbutton.clicked.connect(self.buttonClicked)

        # put them next to each other
        layout = QtWidgets.QHBoxLayout(self)
        layout.addWidget(openbutton)
        layout.addWidget(linewidget)

    def textChanged(self, text):
        """
        Callback, to save the new text and notify the parent that the text has changed
        """
        self.url = text
        self.callback()

    def toPlainText(self):
        """
        Callback, called by the GUI to retrieve (possibly changed) URL
        """
        return self.url

    def buttonClicked(self):
        """
        Callback, called when the button has been clicked. Opens the URL with the system
        default software.
        """
        QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.url))


class TextEdit(QtWidgets.QPlainTextEdit):
    """
    Special custom widget class for text fields
    """

    update_timer = None

    def __init__(self, text, callback, height, parent=None):
        """
        Initialise
        """

        # init
        QtWidgets.QPlainTextEdit.__init__(self, text, parent=parent)
        self.callback = callback
        self.has_changed = False
        self.textChanged.connect(self.notifyChange)
        self.setFixedHeight(height * int(self.height()/25))
        self.update_timer = QtCore.QTimer(self)
        self.update_timer.timeout.connect(self.update)

    def notifyChange(self):
        self.has_changed = True

    def update(self):
        if self.has_changed:
            self.has_changed = False
            self.callback()

    def focusInEvent(self, e: QtGui.QFocusEvent):
        """
        Callback, to start timer for periodic update. Doing this on focus-out is too slow during meetings.
        """
        self.update_timer.start(1000)
        super().focusInEvent(e)

    def focusOutEvent(self, e: QtGui.QFocusEvent):
        """
        Callback, to save the new text and notify the parent that the text has changed
        """
        if self.has_changed:
            self.has_changed = False
            self.callback()
        self.update_timer.stop()
        super().focusOutEvent(e)

    def leaveEvent(self, e: QtCore.QEvent):
        """
        Callback, to save the new text and notify the parent that the text has changed
        """
        self.update()
        super().leaveEvent(e)


class TimerWidget(QtWidgets.QWidget):
    """
    A widget displaying an hour timer - a time display (hours:minutes:seconds)
    and a button "start"/"stop"
    """

    timer = None

    def __init__(self, partial, running_since, callback, font, parent=None):
        """
        initialise the timer object
        """

        # init
        QtWidgets.QWidget.__init__(self, parent=parent)
        self.start = running_since and datetime.datetime.strptime(running_since, "%Y-%m-%d %H:%M:%S")  # time when the timer was started last, or None if not running
        self.partial = partial or 0.0     # stored time span + time since timer was started
        self.total = self.partial
        if self.start:
            buttontext = "Stop"
            linetext = "(running)"
        else:
            buttontext = "Start"
            linetext = str(self.partial)
        self.callback = callback

        # create line edit control and open button
        self.linewidget = QtWidgets.QLineEdit(linetext)
        self.linewidget.setFont(font)
        self.linewidget.textEdited.connect(self.textEdited)
        self.startstopbutton = QtWidgets.QPushButton(buttontext)
        self.startstopbutton.setFont(font)
        self.startstopbutton.clicked.connect(self.buttonClicked)

        # put them next to each other
        layout = QtWidgets.QHBoxLayout(self)
        layout.addWidget(self.startstopbutton)
        layout.addWidget(self.linewidget)

        # start timer if it is loaded as running from file
        if self.start:
            self.updateValue()

    def textEdited(self, text):
        """
        Callback, to save the new text and notify the parent that the text has changed
        """
        if self.start:
            self.linewidget.setText("stop watch running")
        else:
            if text:
                try:
                    d = json.loads(text)
                except json.JSONDecodeError:
                    d = 0.0
            else:
                d = 0.0
            self.total = d
            self.partial = self.total
            self.callback()

    def toPlainText(self):
        """
        Callback, called by the GUI to retrieve (possibly changed) total value
        """
        return str(self.total)

    def runningSince(self):
        """
        Callback, returns the time the timer was started as YYYY-MM-DD hh.mm.ss string
        """
        return self.start and self.start.strftime("%Y-%m-%d %H:%M:%S") or False

    def buttonClicked(self):
        """
        Callback, called when the button has been clicked. Opens the URL with the system
        default software.
        """
        if self.start:
            self.stopTimer()
        else:
            self.startTimer()

    def startTimer(self):
        """
        Starts the timer
        """

        # save the time the timer was started
        self.start = datetime.datetime.now()
        self.startstopbutton.setText("Stop")
        self.linewidget.setText("stop watch running")

        # trigger the first update
        self.updateValue()

    def stopTimer(self):
        """
        Stops the timer
        """
        if self.start:

            # stop the timer, remove possible running timer objects
            self.startstopbutton.setText("Start")

            # store total value
            self.updateValue(update=False)
            self.partial = round(self.total*10000.0) / 10000.0
            self.total = self.partial
            self.start = None
            self.linewidget.setText(str(self.total))

            # notify back-end
            self.callback()

    def updateValue(self, update=True):
        """
        Updates the value in the GUI once and triggers the next update
        Used for sequencial updates while the timer is running. Updates are triggered using a threading.Timer object
        and occur every five seconds
        """

        # first, update the total time
        elapsed = datetime.datetime.now() - self.start
        self.total = self.partial \
                     + elapsed.days * 24.0 \
                     + elapsed.seconds / 60.0 / 60.0 \
                     + elapsed.microseconds / 60.0 / 1000000.0
        if update:
            self.callback()


class TreeTimeWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    """
    Implements the main part of the GUI.
    """

    def __init__(self, filename=None):
        """
        Initialise the application, connect all button signals to application functions, load theme and last file
        """

        print("starting TreeTime...")

        # initialise main window
        super().__init__()
        self.setupUi(self)

        # connect all button signals
        print("setting up GUI...")
        self.pushButtonNewChild.clicked.connect(lambda: self.createNode("child", False))
        self.pushButtonNewSibling.clicked.connect(lambda: self.createNode("sibling", False))
        self.pushButtonNewParent.clicked.connect(lambda: self.createNode("parent", False))
        self.pushButtonCopyNodeChild.clicked.connect(lambda: self.createNode("child", True))
        self.pushButtonCopyNodeSibling.clicked.connect(lambda: self.createNode("sibling", True))
        self.pushButtonCopyNodeParent.clicked.connect(lambda: self.createNode("parent", True))
        self.pushButtonCopyBranchSibling.clicked.connect(lambda: self.createNode("sibling", True, True))
        self.pushButtonNewFromTemplate.clicked.connect(self.pushButtonNewFromTemplateClicked)
        self.pushButtonNewFile.clicked.connect(self.pushButtonNewFileClicked)
        self.pushButtonSaveSnapshot.setEnabled(False)
        self.pushButtonLoadFile.clicked.connect(self.pushButtonLoadFileClicked)
        self.pushButtonSaveToFile.clicked.connect(self.pushButtonSaveToFileClicked)
        self.pushButtonExport.clicked.connect(self.pushButtonExportClicked)
        self.pushButtonRemoveNode.clicked.connect(self.pushButtonRemoveNodeClicked)
        self.pushButtonDeleteNode.clicked.connect(self.pushButtonDeleteNodeClicked)
        self.pushButtonRemoveBranch.clicked.connect(lambda: self.moveCurrentItemToNewParent(self.currentTree, None))
        self.pushButtonDeleteBranch.clicked.connect(self.pushButtonDeleteBranchClicked)
        self.pushButtonNewDataField.clicked.connect(self.pushButtonNewDataFieldClicked)
        self.pushButtonNewTreeField.clicked.connect(self.pushButtonNewTreeFieldClicked)
        self.pushButtonNewTree.clicked.connect(self.pushButtonNewTreeClicked)
        self.pushButtonDeleteDataField.clicked.connect(self.pushButtonDeleteDataFieldClicked)
        self.pushButtonDeleteTreeField.clicked.connect(self.pushButtonDeleteTreeFieldClicked)
        self.pushButtonDeleteTree.clicked.connect(self.pushButtonDeleteTreeClicked)
        self.sliderZoom.valueChanged.connect(self.sliderZoomChanged)
        self.tableWidget.cellChanged.connect(self.tableWidgetCellChanged)
        self.tableWidget.verticalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
        self.tableWidget.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Fixed)     # column 0: fixed
        self.tableWidget.horizontalHeader().setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Fixed)     # column 1: fixed, 100
        self.tableWidget.horizontalHeader().resizeSection(1, 100)
        self.tableWidget.horizontalHeader().setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.Fixed)     # column 2: fixed
        self.tableWidget.horizontalHeader().setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeMode.Stretch)     # column 3: stretch
        self.tableWidget.horizontalHeader().setSectionResizeMode(4, QtWidgets.QHeaderView.ResizeMode.Fixed)     # column 4: fixed
        self.tabWidget.currentChanged.connect(self.tabWidgetCurrentChanged)
        self.toolBox.currentChanged.connect(self.toolBoxCurrentChanged)
        self.write_delay = 0
        self.write_timer = False
        self.locked = True
        self.export_continuous = False
        self.auto_updates = []
        self.update_timer = QtCore.QTimer(self)
        self.update_timer.timeout.connect(self.updateTimers)
        self.update_timer.start(1000)
        self.tabWidgets = []
        self.treeWidgets = []
        self.currentTree = 0
        self.currentItem = None
        self.currentMetaNode = None
        self.currentColumn = None
        self.gridInitialised = False

        # init application settings
        print("loading system settings...")
        self.settings = QtCore.QSettings('FreeSoftware', 'TreeTime')

        # store system font size
        self._dataFontPointSize = int(self.centralwidget.font().pointSize())

        # library path otherwise the unpacked graphviz .so files don't load with linux/pyinstaller
        if getattr(sys, 'frozen', False):
            for pname in ('LD_LIBRARY_PATH', 'PATH'):
                path = os.environ.get(pname)
                if path:
                    path = path + os.pathsep + sys._MEIPASS
                else:
                    path = sys._MEIPASS
                os.environ[pname] = path

        # show last exported file
        self.labelCurrentExportFile.setText(self.settings.value('exportLink') or self.settings.value('exportFile') or "[last exported file]")
        self.labelCurrentExportFile.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextBrowserInteraction)
        self.labelCurrentExportFile.setOpenExternalLinks(True)

        # load last file
        print("opening last file...")
        if filename:
            loadFile = filename
        else:
            loadFile = self.settings.value('lastFile')

        # try loading silently once
        try:
            self.loadFile(loadFile)
            self.setWindowTitle("TreeTime - " + loadFile)
            self.settings.setValue('fileDir', os.path.dirname(loadFile))
            self.settings.setValue('lastFile', loadFile)
            self.labelCurrentFile.setText(loadFile)
            fileLoaded = True
        except BaseException:
            fileLoaded = False

        # disable all buttons
        if not fileLoaded:
            self.editMode = 'empty'  # one of 'content', 'tree', or 'data'
            self.changeEditMode('empty')

        # init themes and set last theme
        print("initialising fonts and colours...")
        self.system_palette = self.palette()
        self.fillThemeBox()
        self.fillColourBox()
        self.cboxThemeTextChanged()
        self.cboxColoursTextChanged()
        self.cboxTheme.currentTextChanged.connect(self.cboxThemeTextChanged)
        self.cboxColours.currentTextChanged.connect(self.cboxColoursTextChanged)

        # show window
        print("showing main window...")
        self.showMaximized()

    def startWebServer(self, logo):
        """
        Starts the web server. Function called externally after application started (for symmetry).
        :return:
        """
        # start web server
        print("starting web server...")
        self._web_site_wrapper = \
            """
            <!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
            <title>TreeTime Export</title>
            <script type="text/javascript">
                var baseurl = "http://" + window.location.href.split("/")[2];
                var stateurl = baseurl + "/state";
                var laststate = 'new';
                var frame = 0;
                function init() {
                    console.log("init");
                    // get the iframe
                    frame = document.getElementById("mainframe");
                    frame.src = baseurl;
                    // start
                    update();
                }
                async function update() {
                    // get new version and possibly update
                    let response = await fetch(stateurl);
                    let state = await response.text();
                    console.log(state);
                    if (laststate != state) {
                        laststate = state;
                        console.log("new state, reloading");
                        frame.src = baseurl;
                    }
                    // recursion
                    setTimeout(update, 500);
                }
            </script></head>
            <body onload="init()">
                <iframe id="mainframe" style="position: absolute; border-width: 0; left: 0; top: 0px; width: 100%;
                                              height: 100%;">
                    TreeTime software not running
                </iframe>
            </body></html>
            """
        self._web_default_string = ('<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
                                    '<title>TreeTime Export</title>'
                                    '</head><body>TreeTime Web Service<br/>Not running</body></html>')
        self._web_string = self._web_default_string
        self._web_type = 'text/html'
        self._web_port = None
        tries = 20
        while tries and not self._web_port:
            try:
                port = 2020 + 20 - tries
                self._web_service = make_server('0.0.0.0', port, self.serve_web)
                self._web_port = port
            except:
                tries -= 1
        if self._web_port:
            self._web_thread = Thread(target=self._web_service.serve_forever)
            self._web_favicon = logo
            self._web_timestamp = 0
            self._web_timestring = '000'
            self._web_thread.start()
            print(f"... web server running on http://0.0.0.0:{self._web_port}")
        else:
            print("... could not start web server.")
            self.radioButtonExportToWebServer.setEnabled(False)

    def stopWebServer(self):
        """
        Stops the web server. Function called by application callback when the main window is closed.
        """
        # stop webserver
        print("stopping web server...")
        self._web_service.shutdown()
        self._web_thread.join()

    def endisableButtons(self, mode):
        """
        Enables all edit buttons if state==True, disables them except the new file buttons
        :param state: the button state to set
        """

        if mode == 'content':
            contentState = True
            fileState = True
            metaState = True
        elif mode == 'empty':
            contentState = False
            fileState = True
            metaState = False
        else:
            contentState = False
            fileState = False
            metaState = True

        # new/load file buttons
        self.pushButtonNewFromTemplate.setEnabled(fileState)
        self.pushButtonNewFile.setEnabled(fileState)
        self.pushButtonLoadFile.setEnabled(fileState)

        # edit/export buttons
        self.pushButtonNewChild.setEnabled(contentState)
        self.pushButtonNewSibling.setEnabled(contentState)
        self.pushButtonNewParent.setEnabled(contentState)
        self.pushButtonCopyNodeChild.setEnabled(contentState)
        self.pushButtonCopyNodeSibling.setEnabled(contentState)
        self.pushButtonCopyNodeParent.setEnabled(contentState)
        self.pushButtonCopyBranchSibling.setEnabled(contentState)
        self.comboBoxCopyDepth.setEnabled(contentState)
        self.label_6.setEnabled(contentState)
        self.pushButtonSaveToFile.setEnabled(contentState)
        self.pushButtonExport.setEnabled(contentState)
        self.pushButtonRemoveNode.setEnabled(contentState)
        self.pushButtonDeleteNode.setEnabled(contentState)
        self.pushButtonRemoveBranch.setEnabled(contentState)
        self.pushButtonDeleteBranch.setEnabled(contentState)

        # meta tab
        self.toolBox.setItemEnabled(1, metaState)
        self.toolBox.setItemEnabled(2, metaState)
        self.toolBox.setItemEnabled(3, metaState)

    def endisableMetaButtons(self, metaType, state):
        """
        Enables state related buttons if state==True, disables them otherwise
        Disabling (old state) should be called before enabling (new state)
        :param state: the button state to set
        """
        if metaType in ('tree'):
            self.pushButtonNewTree.setEnabled(state)
            self.pushButtonDeleteTree.setEnabled(state)
            self.pushButtonNewTreeField.setEnabled(state)
        if metaType in ('parameter list', 'tree field'):
            self.pushButtonNewTreeField.setEnabled(state)
            self.pushButtonDeleteTreeField.setEnabled(state)
        if metaType == 'data item':
            self.pushButtonNewDataField.setEnabled(state)
        if metaType == 'data field':
            self.pushButtonNewDataField.setEnabled(state)
            self.pushButtonDeleteDataField.setEnabled(state)
        if len(self.forest.children) == 1:     # Prohibit deleting of the last tree
            self.pushButtonDeleteTree.setEnabled(False)

    def closeEvent(self, event):
        """
        Do some cleanup before you close the window
        """

        # Write state to file
        if self.write_timer:
            self.write_timer.cancel()
        self.write_delay = 0
        try:
            self.writeToFile()
        except AttributeError:
            pass

        # continue closing the window
        event.accept()  # let the window close

    def fillThemeBox(self):
        """
        Fills the theme selection box with all themes the system is capable of
        """

        # our own theme, basically Fusion with some changes
        self.cboxTheme.addItem("Organic")

        # built-in themes
        for k in QtWidgets.QStyleFactory.keys():
            self.cboxTheme.addItem(k)
        current = self.settings.value('theme')
        if current:
            self.cboxTheme.setCurrentText(current)

    def fillColourBox(self):
        """
        Fills the theme selection box with all themes the system is capable of
        """
        self.cboxColours.addItem("Dark")
        self.cboxColours.addItem("Light")
        self.cboxColours.addItem("System")
        current = self.settings.value('colours')
        if current:
            self.cboxColours.setCurrentText(current)

    def cboxThemeTextChanged(self):
        """
        Callback from the theme selector combo box. Sets a new theme and stores it in the settings.
        """
        application = QtWidgets.QApplication.instance()
        style = self.cboxTheme.currentText()
        self.settings.setValue('theme', style)

        # set style sheets first and then apply QStyle, otherwise the stylesheet break everything
        if 'Organic' in style:
            wd = __file__[:-12].replace('\\', '/')     # for windows file separators
            colour = wd + '/themes/' + (self.cboxColours.currentText() == "Light" and "light" or "dark")

            # QML for more padding and folding icons
            baseColour = application.palette().base().color().name()
            qss = (
                # padding on the buttons
                "QPushButton { padding: 0.3em;}"
                "QToolButton { padding: 0.3em; margin-top: 0.1em; margin-bottom: 0.1em; margin-right: 0.0em; margin-left: 0.0em;}"
                "QTreeView::item { padding: 0.2em; }"
                                                                                                                  
                # tree decorators
                # has-siblings: not the last in the sibling list; !has-siblings: the last in the sibling list
                # adjoins-item: the direct markers (the last before the text); 
                # !adjoins-item: the higher-level unfolded markers (the non-rightmost tree markers);
                #                if set the children and siblings flags apply to the respective level parent
                # has-children / !has-children: whether or nor there are children
                # open / closed: whether the item is folded or unfolded
                # border-image: scales with multi-line entries
                # image: does not scale with multi-line entres
                "QTreeView::branch:has-children:closed:adjoins-item { image:url(" + colour + "_triangle_right.svg); }"
                "QTreeView::branch:has-children:closed:adjoins-item:hover { image:url(" + colour + "_triangle_right_hover.svg); }"
                "QTreeView::branch:has-children:open:adjoins-item { image:url(" + colour + "_triangle_down.svg); }"
                "QTreeView::branch:has-children:open:adjoins-item:hover { image:url(" + colour + "_triangle_down_hover.svg); }"
                "QTreeView::branch:has-siblings:adjoins-item { border-image:url(" + colour + "_branch_full.svg); }"
                "QTreeView::branch:has-siblings:!has-children:adjoins-item { image:url(" + colour + "_twig_empty.svg); }"
                "QTreeView::branch:!has-siblings:adjoins-item { border-image:url(" + colour + "_branch_top.svg); }"
                "QTreeView::branch:!has-siblings:!has-children:adjoins-item { image:url(" + colour + "_twig_empty.svg); }"
                "QTreeView::branch:has-siblings:!adjoins-item { border-image:url(" + colour + "_branch_full.svg); }"
                                                                                              
                # make sure the branch area is not made invisible because it has the same colour as the selection
                "QTreeView::branch:selected { background-color: " + baseColour + ";}"
            )
            application.setStyleSheet(qss)

        style = QtWidgets.QStyleFactory.create(style)
        application.setStyle(style)
        self.itemSelectionChanged()

    def cboxColoursTextChanged(self):
        """
        Callback from the theme selector combo box. Sets a new theme and stores it in the settings.
        """
        application = QtWidgets.QApplication.instance()
        colour = self.cboxColours.currentText()
        self.settings.setValue('colours', colour)
        palette = QPalette()

        # Apply dark colours
        if colour == "Dark":
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Window, QColor(50, 52, 54))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.WindowText, QColor(255, 255, 255))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.AlternateBase, QColor(50, 52, 54))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.ToolTipBase, QColor(0, 0, 0))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.ToolTipText, QColor(255, 255, 255))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Text, QColor(255, 255, 255))
            palette.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Text, QColor(255, 255, 255))
            palette.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Text, QColor(255, 255, 255))
            palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor(150, 150, 150))
            palette.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.ButtonText, QColor(255, 255, 255))
            palette.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.ButtonText, QColor(255, 255, 255))
            palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor(100, 100, 100))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Button, QColor(60, 62, 64))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.BrightText, QColor(255, 0, 0))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Link, QColor(60, 200, 255))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Highlight, QColor(60, 200, 255))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.HighlightedText, QColor(0, 0, 0))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Light, QColor(80, 84, 88))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Midlight, QColor(60, 63, 66))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Base, QColor(40, 42, 44))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Mid, QColor(30, 31, 33))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Dark, QColor(20, 21, 22))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Shadow, QColor(10, 10, 11))

        # apply light colours
        elif colour == "Light":
            palette = QPalette()
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Window, QColor(255-53, 255-53, 255-53))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.WindowText, QColor(0, 0, 0))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.AlternateBase, QColor(255-53, 255-53, 255-53))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.ToolTipBase, QColor(255, 255, 255))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.ToolTipText, QColor(0, 0, 0))
            palette.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.Text, QColor(0, 0, 0))
            palette.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Text, QColor(0, 0, 0))
            palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor(70, 70, 70))
            palette.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.ButtonText, QColor(0, 0, 0))
            palette.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.ButtonText, QColor(0, 0, 0))
            palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor(150, 150, 150))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Button, QColor(255-53, 255-53, 255-53))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.BrightText, QColor(0, 0, 255))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Link, QColor(255-42, 255-130, 255-218))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Highlight, QColor(255-42, 255-130, 255-218))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.HighlightedText, QColor(255, 255, 255))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Light, QColor(255-10, 255-10, 255-10))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Midlight, QColor(255-20, 255-20, 255-20))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Base, QColor(255-25, 255-25, 255-25))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Mid, QColor(255-100, 255-100, 255-100))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Dark, QColor(255-150, 255-150, 255-150))
            palette.setColor(QPalette.ColorGroup.All, QPalette.ColorRole.Shadow, QColor(255-200, 255-200, 255-200))

        # apply system colours
        elif colour == "System":
            palette = self.system_palette
        application.setPalette(palette)
        self.cboxThemeTextChanged()

    def sliderZoomChanged(self):
        """
        Scales the font size of the data display part of the UI by the offide of the slider
        """

        # get current value of slider and store default font size
        offset = self.sliderZoom.value()
        font = self.centralwidget.font()
        system_size = font.pointSize()
        self._dataFontPointSize = int(system_size + offset)
        font.setPointSize(self._dataFontPointSize)

        # change font in data item view
        self.tableWidget.setFont(font)
        self.showContentInDataView(self.currentTree)    # redraw data pane

        # change font in all tree widgets
        for tree in self.treeWidgets:
            tree.setFont(font);

        # change label
        self.labelZoom.setText("Zoom {}{}".format(offset > 0 and '+' or '', offset))

    def pushButtonSaveToFileClicked(self):
        """
        Callback for the save-file button. Saves the current data to a new file and keeps that file connected.
        """
        fileDir = self.settings.value('fileDir') or ''
        file = QtWidgets.QFileDialog.getSaveFileName(self, "Save new Data File", fileDir, 'TreeTime Files (*.trt)')[0]
        if file != '':
            self.labelCurrentFile.setText(file)
            self.writeToFile()
            self.setWindowTitle("TreeTime - " + file)
            self.settings.setValue('fileDir', os.path.dirname(file))
            self.settings.setValue('lastFile', file)

    def pushButtonNewFileClicked(self):
        """
        Callback for the load-file button. Loads new file and keeps that file connected.
        """
        fileDir = self.settings.value('fileDir') or ''
        file = QtWidgets.QFileDialog.getSaveFileName(self, "New Data File", fileDir, 'TreeTime Files (*.trt)')[0]
        if file != '':
            self.newFile(file)
            self.setWindowTitle("TreeTime - " + file)
            self.settings.setValue('fileDir', os.path.dirname(file))
            self.settings.setValue('lastFile', file)
            self.labelCurrentFile.setText(file)

    def pushButtonLoadFileClicked(self):
        """
        Callback for the load-file button. Loads new file and keeps that file connected.
        """
        fileDir = self.settings.value('fileDir') or ''
        file = QtWidgets.QFileDialog.getOpenFileName(self, "Load Data File", fileDir, 'TreeTime Files (*.trt)')[0]
        if file != '':
            self.loadFile(file)
            self.setWindowTitle("TreeTime - " + file)
            self.settings.setValue('fileDir', os.path.dirname(file))
            self.settings.setValue('lastFile', file)
            self.labelCurrentFile.setText(file)

    def pushButtonExportClicked(self):

        file = ""
        link = ""

        # We're not in contiuous mode -- show file dialog
        if not self.export_continuous:
            if self.currentItem:

                exportFormat = self.comboBoxExportFormat.currentText()
                exportToFile = self.radioButtonExportToFile.isChecked()
                exportToWebServer = self.radioButtonExportToWebServer.isChecked()
                exportContinuously = self.radioButtonExportContinuously.isChecked()
                if exportToFile:
                    extensions = {
                        "HTML (Document)": "HTML Files (*.html)",
                        "HTML (Tiles)": "HTML Files (*.html)",
                        "HTML (List)": "HTML Files (*.html)",
                        "Text (Line Art)": "Text Files (*.txt)",
                        "Text (Markdown)": "Markdown Files (*.md)",
                        "Image/PNG (top-down)": "Image Files (*.png)",
                        "Image/SVG (top-down)": "Image Files (*.svg)",
                        "Image/PNG (circular)": "Image Files (*.png)",
                        "Image/SVG (circular)": "Image Files (*.svg)",
                        "Image/PNG (spread-out)": "Image Files (*.png)",
                        "Image/SVG (spread-out)": "Image Files (*.svg)",
                        "CSV+ (incl tree info)": "CSV (Comma-separated Values) Files (*.csv)",
                        "CSV (Names/values only)":  "CSV (Comma-separated Values) Files (*.csv)",
                        "TSV+ (incl tree info)": "TSV (Tab-separated Values) Files (*.tsv)",
                        "TSV (Mames/values only)": "TSV (Tab-separated Values) Files (*.tsv)"
                    }
                    fileDir = self.settings.value('exportFile') and os.path.dirname(self.settings.value('exportFile')) or ''
                    file = QtWidgets.QFileDialog.getSaveFileName(self, "Export to " + exportFormat, fileDir,
                                                                 extensions[exportFormat])[0]
                    link = f'<a href="{pathlib.Path(file).as_uri()}">{file}</a>'
                    if not file:
                        return

                if exportToWebServer:
                    ips = [('localhost', '(local)')]
                    try:
                        iface = netifaces.gateways()['default'][netifaces.AF_INET][1]
                        lan_ip = netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['addr']
                        note = '(network)'
                        ips += [(lan_ip, note)]
                    except:
                        ips += [('', 'not available on network')]
                    try:
                        web_ip = requests.get('https://api.ipify.org').text
                        avail = requests.get(f'http://{web_ip}:{self._web_port}', timeout=0.5)
                        note = '(internet)'
                        ips += [(web_ip, note)]
                    except:
                        ips += [('', 'not available on internet')]
                    file = ''
                    link = ''
                    for ip, note in ips:
                        path = ip and f'{ip}:{self._web_port}{(exportContinuously and "/follow") or ''}' or ''
                        file = path and f'http://{path}' or file
                        href = path and f'<a href="http://{path}">{path}</a>' or ''
                        link += f'{href} {note}<br/>'

                # we're starting continuous mode -- set the texts, set the flag, and call the timer function
                if self.radioButtonExportContinuously.isChecked():
                    self.pushButtonExport.setText("Stop continuous export")
                    self.labelExportFileDescription.setText("Exporting continuously to:")
                    self.radioButtonExportContinuously.setDisabled(True)
                    self.radioButtonExportOnce.setDisabled(True)
                    self.export_continuous = True
                    self.delayedContinuousExport(file, link=link)

                # we're doing a standard export
                self.export(file, link=link)

        # We're stopping continuous mode -- set the texts, set the flag
        else:
            self.pushButtonExport.setText("Export")
            self.labelExportFileDescription.setText("Last exported file:")
            self.radioButtonExportContinuously.setEnabled(True)
            self.radioButtonExportOnce.setEnabled(True)
            self.export_continuous = False

    def serve_web(self, environ, start_response):
        """
        localhost:2020/               standard files and non-updating stuff
        localhost:2020/state          time stamp queries, returning a number that increments when the file changes
        localhost:2020/favicon.ico    the icon
        localhost:2020/follow         short polling wrapper file, reloads the things from / when the /state changes
        :param start_response:
        :return:
        """
        target = environ['REQUEST_URI']
        if target == '/':
            response = WzResponse(self._web_string, content_type=self._web_type)
        elif target == '/favicon.ico':
            response = WzResponse(self._web_favicon, content_type='image/ico')
        elif target == '/state':
            response = WzResponse(self._web_timestring, content_type='text/plain')
        elif target == '/follow':
            response = WzResponse(self._web_site_wrapper, content_type='text/html')
        else:
            response = WzResponse(self._web_string, content_type=self._web_type)
        return response(environ, start_response)

    def delayedContinuousExport(self, file, link=''):
        """
        Writes to file using current settings, then checks current state (user may have stopped), then waits for three
        seconds, then calls itself
        Used to loop the continuous file writing.
        """

        # First writes to file
        self.export(file, link=link, continuous=True)

        # If mode active, either calls itself after two seconds
        if self.export_continuous:
            exportToWebServer = self.radioButtonExportToWebServer.isChecked()
            self.export_timer = Timer(exportToWebServer and 1 or 3, lambda: self.delayedContinuousExport(file, link=link))
            self.export_timer.start()

        # If not, do nothing

    def export(self, file, link='', continuous=False):
        """
        Callback for the text/html/csv export. Asks for a file name, then writes branch text export into it.
        """

        if self.currentItem:

            # read file format
            exportFormat = self.comboBoxExportFormat.currentText()
            nodeNames = self.checkBoxExportNodeNames.isChecked()
            fieldNames = self.checkBoxExportFieldNames.isChecked()
            fieldContent = self.checkBoxExportFieldContent.isChecked()

            # Select target file or cancel
            exportToFile = self.radioButtonExportToFile.isChecked()
            exportToWebServer = self.radioButtonExportToWebServer.isChecked()

            # The target string for file write / clipboard write
            data = ""
            webtype = ""
            wtype = 'w'     # type to write, either 'w' for writing string, or 'wb' for writing bytes
            depth = self.comboBoxExportDepth.currentIndex() - 1

            # export current branch
            if self.radioButtonExportBranch.isChecked():

                currentNode = self.currentItem.viewNodes[self.currentTree]
                if currentNode:

                    # write to data string
                    if exportFormat == "CSV+ (incl tree info)":
                        webtype = 'text/csv'
                        data = currentNode.to_csv(depth=depth,
                                                  nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                                  sep=',', treeColumn=True)
                    elif exportFormat == "CSV (Names/values only)":
                        webtype = 'text/csv'
                        data = currentNode.to_csv(depth=depth,
                                                  nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                                  sep=',', treeColumn=False)
                    elif exportFormat == "TSV+ (incl tree info)":
                        webtype = 'text/csv'
                        data = currentNode.to_csv(depth=depth,
                                                  nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                                  sep='\t', treeColumn=True)
                    elif exportFormat == "TSV (Names/values only)":
                        webtype = 'text/csv'
                        data = currentNode.to_csv(depth=depth,
                                                  nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                                  sep='\t', treeColumn=False)
                    elif exportFormat == "Text (Line Art)":
                        webtype = 'text/plain'
                        data = currentNode.to_txt(depth=depth,
                                                  nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent)
                    elif exportFormat == "Text (Markdown)":
                        webtype = 'text/plain'
                        data = currentNode.to_md(depth=depth,
                                                 nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent)
                    elif exportFormat == "HTML (Document)":
                        webtype = 'text/html'
                        dummy, data = currentNode.to_html(header=True, footer=True, depth=depth, nodeNames=nodeNames,
                                                          fieldNames=fieldNames, fieldContent=fieldContent,
                                                          style='document')
                    elif exportFormat == "HTML (List)":
                        webtype = 'text/html'
                        dummy, data = currentNode.to_html(header=True, footer=True, depth=depth, nodeNames=nodeNames,
                                                          fieldNames=fieldNames, fieldContent=fieldContent,
                                                          style='list')
                    elif exportFormat == "HTML (Tiles)":
                        webtype = 'text/html'
                        dummy, data = currentNode.to_html(header=True, footer=True, depth=depth, nodeNames=nodeNames,
                                                          fieldNames=fieldNames, fieldContent=fieldContent,
                                                          style='tiles')
                    elif exportFormat == "Image/PNG (top-down)":
                        webtype = 'image/png'
                        data = currentNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames,
                                                    fieldContent=fieldContent, depth=depth, engine='dot', format='png')
                        wtype = 'wb'
                    elif exportFormat == "Image/SVG (top-down)":
                        webtype = 'image/svg+xml'
                        data = currentNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames,
                                                    fieldContent=fieldContent, depth=depth, engine='dot', format='svg')
                        wtype = 'wb'
                    elif exportFormat == "Image/PNG (circular)":
                        webtype = 'image/png'
                        data = currentNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames,
                                                    fieldContent=fieldContent, depth=depth, engine='twopi',
                                                    format='png')
                        wtype = 'wb'
                    elif exportFormat == "Image/SVG (circular)":
                        webtype = 'image/svg+xml'
                        data = currentNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames,
                                                    fieldContent=fieldContent, depth=depth, engine='twopi',
                                                    format='svg')
                        wtype = 'wb'
                    elif exportFormat == "Image/PNG (spread-out)":
                        webtype = 'image/png'
                        data = currentNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames,
                                                    fieldContent=fieldContent, depth=depth, engine='sfdp',
                                                    format='png')
                        wtype = 'wb'
                    elif exportFormat == "Image/SVG (spread-out)":
                        webtype = 'image/svg+xml'
                        data = currentNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames,
                                                    fieldContent=fieldContent, depth=depth, engine='sfdp',
                                                    format='svg')
                        wtype = 'wb'
                    else:
                        data = None
                else:
                    if continuous:
                        return
                    else:
                        data = ("No branch selected, export is empty")

            # export entire tree
            elif self.radioButtonExportTree.isChecked():

                # pick up the source
                rootNode = self.forest.children[self.currentTree]
                children = sorted(rootNode.children, key=lambda a: a.name)

                # write data string
                if exportFormat == "CSV":
                    webtype = 'text/csv'
                    first = True
                    for c in children:
                        data += c.to_csv(first=first, depth=depth, nodeNames=nodeNames, fieldNames=fieldNames,
                                         fieldContent=fieldContent)
                        first = False
                if exportFormat == "CSV+ (incl tree info)":
                    webtype = 'text/csv'
                    first = True
                    for c in children:
                        data += c.to_csv(first=first, depth=depth, nodeNames=nodeNames, fieldNames=fieldNames,
                                         fieldContent=fieldContent, sep=',', treeColumn=True)
                        first = False
                elif exportFormat == "CSV (Names/values only)":
                    webtype = 'text/csv'
                    first = True
                    for c in children:
                        data += c.to_csv(first=first, depth=depth, nodeNames=nodeNames, fieldNames=fieldNames,
                                         fieldContent=fieldContent, sep = ',', treeColumn=False)
                        first = False
                elif exportFormat == "TSV+ (incl tree info)":
                    webtype = 'text/csv'
                    first = True
                    for c in children:
                        data += c.to_csv(first=first, depth=depth, nodeNames=nodeNames, fieldNames=fieldNames,
                                         fieldContent=fieldContent, sep='\t', treeColumn=True)
                        first = False
                elif exportFormat == "TSV (Names/values only)":
                    webtype = 'text/csv'
                    first = True
                    for c in children:
                        data += c.to_csv(first=first, depth=depth, nodeNames=nodeNames, fieldNames=fieldNames,
                                         fieldContent=fieldContent, sep='\t', treeColumn=False)
                        first = False
                elif exportFormat == "Text (Line Art)":
                    webtype = 'text/plain'
                    for c in children:
                        data += '\n'
                        data += c.to_txt(depth=depth, nodeNames=nodeNames, fieldNames=fieldNames,
                                         fieldContent=fieldContent)
                        data += '\n'
                elif exportFormat == "Text (Markdown)":
                    webtype = 'text/plain'
                    data += rootNode.to_md(depth=depth, nodeNames=nodeNames, fieldNames=fieldNames,
                                           fieldContent=fieldContent)
                elif (exportFormat == "HTML (List)"
                      or exportFormat == "HTML (Tiles)"
                      or exportFormat == "HTML (Document)"):
                    webtype = 'text/html'
                    style = ((exportFormat == "HTML (List)" and "list")
                             or (exportFormat == "HTML (Tiles)" and "tiles") or "document")
                    for c in range(0, len(children)):
                        if c == 0:
                            data += children[c].to_html(header=True, depth=depth,
                                                        nodeNames=nodeNames, fieldNames=fieldNames,
                                                        fieldContent=fieldContent, style=style)[1]
                        elif c == len(children) - 1:
                            data += children[c].to_html(footer=True, depth=depth,
                                                        nodeNames=nodeNames, fieldNames=fieldNames,
                                                        fieldContent=fieldContent, style=style)[1]
                        else:
                            data += children[c].to_html(depth=depth,
                                                        nodeNames=nodeNames, fieldNames=fieldNames,
                                                        fieldContent=fieldContent, style=style)[1]
                elif exportFormat == "Image/PNG (top-down)":
                    webtype = 'image/png'
                    data = rootNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                             depth=depth, engine='dot', format='png', exclude_root=True)
                    wtype = 'wb'
                elif exportFormat == "Image/SVG (top-down)":
                    webtype = 'image/svg+xml'
                    data = rootNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                             depth=depth, engine='dot', format='svg', exclude_root=True)
                    wtype = 'wb'
                elif exportFormat == "Image/PNG (circular)":
                    webtype = 'image/png'
                    data = rootNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                             depth=depth, engine='twopi', format='png', invisible_root=True)
                    wtype = 'wb'
                elif exportFormat == "Image/SVG (circular)":
                    webtype = 'image/svg+xml'
                    data = rootNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                             depth=depth, engine='twopi', format='svg', invisible_root=True)
                    wtype = 'wb'
                elif exportFormat == "Image/PNG (spread-out)":
                    webtype = 'image/png'
                    data = rootNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                             depth=depth, engine='sfdp', format='png', exclude_root=True)
                    wtype = 'wb'
                elif exportFormat == "Image/SVG (spread-out)":
                    webtype = 'image/svg+xml'
                    data = rootNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                             depth=depth, engine='sfdp', format='svg', exclude_root=True)
                    wtype = 'wb'
                else:
                    data = None

            # export current node with context
            else:
                path = self.currentItem.trees[self.currentTree]
                currentNode = self.forest.children[self.currentTree].findNode(path[:1])
                if currentNode:

                    # write to data string
                    if exportFormat == "CSV+ (incl tree info)":
                        webtype = 'text/csv'
                        data = currentNode.to_csv(depth=depth,
                                                  nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                                  sep=',', treeColumn=True)
                    elif exportFormat == "CSV (Names/values only)":
                        webtype = 'text/csv'
                        data = currentNode.to_csv(depth=depth,
                                                  nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                                  sep=',', treeColumn=False)
                    elif exportFormat == "TSV+ (incl tree info)":
                        webtype = 'text/csv'
                        data = currentNode.to_csv(depth=depth,
                                                  nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                                  sep='\t', treeColumn=True)
                    elif exportFormat == "TSV (Names/values only)":
                        webtype = 'text/csv'
                        data = currentNode.to_csv(depth=depth,
                                                  nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                                  sep='\t', treeColumn=False)
                    elif exportFormat == "Text (Line Art)":
                        webtype = 'text/plain'
                        data = currentNode.to_txt(depth=depth, nodeNames=nodeNames, fieldNames=fieldNames,
                                                  fieldContent=fieldContent, context=path)
                    elif exportFormat == "Text (Markdown)":
                        webtype = 'text/plain'
                        data = currentNode.to_md(depth=depth, nodeNames=nodeNames, fieldNames=fieldNames,
                                                 fieldContent=fieldContent, context=path)
                    elif exportFormat == "HTML (Document)":
                        webtype = 'text/html'
                        dummy, data = currentNode.to_html(header=True, footer=True, depth=depth, context=path,
                                                          nodeNames=nodeNames, fieldNames=fieldNames,
                                                          fieldContent=fieldContent, style='document')
                    elif exportFormat == "HTML (List)":
                        webtype = 'text/html'
                        dummy, data = currentNode.to_html(header=True, footer=True, depth=depth, context=path,
                                                          nodeNames=nodeNames, fieldNames=fieldNames,
                                                          fieldContent=fieldContent, style='list')
                    elif exportFormat == "HTML (Tiles)":
                        webtype = 'text/html'
                        dummy, data = currentNode.to_html(header=True, footer=True, depth=depth, context=path,
                                                          nodeNames=nodeNames, fieldNames=fieldNames,
                                                          fieldContent=fieldContent, style='tiles')
                    elif exportFormat == "Image/PNG (top-down)":
                        webtype = 'image/png'
                        data = currentNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames,
                                                    fieldContent=fieldContent, depth=depth, engine='dot', format='png',
                                                    context=path)
                        wtype = 'wb'
                    elif exportFormat == "Image/SVG (top-down)":
                        webtype = 'image/svg+xml'
                        data = currentNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames,
                                                    fieldContent=fieldContent, depth=depth, engine='dot', format='svg',
                                                    context=path)
                        wtype = 'wb'
                    elif exportFormat == "Image/PNG (circular)":
                        webtype = 'image/png'
                        data = currentNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames,
                                                    fieldContent=fieldContent, depth=depth, engine='twopi',
                                                    format='png', context=path)
                        wtype = 'wb'
                    elif exportFormat == "Image/SVG (circular)":
                        webtype = 'image/svg+xml'
                        data = currentNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames,
                                                    fieldContent=fieldContent, depth=depth, engine='twopi',
                                                    format='svg', context=path)
                        wtype = 'wb'
                    elif exportFormat == "Image/PNG (spread-out)":
                        webtype = 'image/png'
                        data = currentNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames,
                                                    fieldContent=fieldContent, depth=depth, engine='sfdp', format='png',
                                                    context=path)
                        wtype = 'wb'
                    elif exportFormat == "Image/SVG (spread-out)":
                        webtype = 'image/svg+xml'
                        data = currentNode.to_image(nodeNames=nodeNames, fieldNames=fieldNames,
                                                    fieldContent=fieldContent, depth=depth, engine='sfdp', format='svg',
                                                    context=path)
                        wtype = 'wb'
                    else:
                        data = None
                else:
                    data = ("No branch selected, export is empty")

            # save to file
            if exportToFile:
                with open(file, wtype) as f:
                    f.write(data)
                self.settings.setValue("exportFile", file)
                self.settings.setValue("exportLink", link)
                self.labelCurrentExportFile.setText(link)

            # save to web server
            elif exportToWebServer:

                # save string
                if self._web_string != data:                # I find no better way than comparing the entire string;
                                                            # some changes (timer states) aren't in the data)
                    self._web_string = data    # where it will be picked up by the server
                    self._web_type = webtype
                    self._web_timestamp = (self._web_timestamp < 999) and (self._web_timestamp + 1) or 0
                    self._web_timestring = f'{self._web_timestamp:03}'

                # set gui
                self.settings.setValue("exportFile", file)
                self.settings.setValue("exportLink", link)
                self.labelCurrentExportFile.setText(link)

            # save to clipboard
            else:

                # decide on clipboard function (setText/setImage) and data preparation (leave as string or QImage)
                clipboard = QGuiApplication.clipboard()
                if exportFormat in ("CSV+ (incl tree info)", "CSV (Names/values only)",
                                    "TSV+ (incl tree info)", "TSV (Names/values only)", "Text (Line Art)",
                                    "Text (Markdown)", "HTML (List)", "HTML (Tiles)", "HTML (Document)"):
                    toClipboard = clipboard.setText
                elif exportFormat in ("Image/PNG (top-down)", "Image/SVG (top-down)",
                                      "Image/PNG (circular)", "Image/SVG (circular)",
                                      "Image/PNG (spread-out)", "Image/SVG (spread-out)"):
                    toClipboard = clipboard.setImage
                    data = QtGui.QImage.fromData(data)
                else:
                    print(f"Clipboard doesn't understand export type of {exportFormat}.")
                    toClipboard = None

                # save to clipboard
                if toClipboard:
                    toClipboard(data, QClipboard.Mode.Clipboard)
                    toClipboard(data, QClipboard.Mode.FindBuffer)
                    if clipboard.supportsSelection():
                        toClipboard(data, QClipboard.Mode.Selection)
                    time.sleep(0.001)

    def pushButtonNewFromTemplateClicked(self):
        """
        Callback for the new-from-template button. Loads new file and immediately saves it to a different file.
        """

        # First load template file
        templateDir = self.settings.value('templateDir') or ''
        template = QtWidgets.QFileDialog.getOpenFileName(self, "Load Template", templateDir,
                                                         'TreeTime Templates (*.empty.trt)')[0]
        if template != '':
            self.loadFile(template)
            self.settings.setValue('templateDir', os.path.dirname(template))

            # Then save as new file
            self.pushButtonSaveToFileClicked()

    def pushButtonRemoveNodeClicked(self):
        """
        Remove the current node from the tree and move all children in this tree to their parent
        """
        message = "Removing node \"" \
                  + self.currentItem.name \
                  + "\". \n\n" \
                  + "This will move all children in this tree to the node's parent.\n" \
                  + "Changes are saved to file immediately and cannot be reverted."
        msgBox = QtWidgets.QMessageBox()
        msgBox.setText(message)
        msgBox.setWindowTitle("TreeTime Message")
        msgBox.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel)
        result = msgBox.exec()

        # remove if the user has confirmed
        if result == QtWidgets.QMessageBox.StandardButton.Ok:

            # move all children to parent node
            currentNode = self.currentItem.viewNodes[self.currentTree]
            parent = currentNode.parent
            while len(currentNode.children):
                currentNode.children[0].item.moveInTree(self.currentTree, parent.path)

            # delete item and update file
            item = self.currentItem
            item.removeFromTree(self.currentTree)
            if not sum([len(t) for t in item.trees]):
                self.forest.itemPool.deleteItem(item)
            self.delayedWriteToFile()

    def pushButtonDeleteNodeClicked(self):
        """
        Delete Node single node and move all children in all branches to the node's parent
        """
        message = "Deleting node \"" + self.currentItem.name + "\". \n\n" \
                  + "This will move all children in all trees to their nodes' parents.\n" \
                  + "Changes are saved to file immediately and cannot be reverted."
        msgBox = QtWidgets.QMessageBox()
        msgBox.setText(message)
        msgBox.setWindowTitle("TreeTime Message")
        msgBox.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel)
        result = msgBox.exec()

        # delete if the user has confirmed
        if result == QtWidgets.QMessageBox.StandardButton.Ok:

            # move all children in all trees to parent node
            for t in range(0, len(self.forest.children)):
                node = self.currentItem.viewNodes[t]
                if node:
                    while len(node.children):
                        node.children[0].item.moveInTree(t, node.parent.path)

            # remove running timers
            for f in self.currentItem.fields.keys():
                if self.currentItem.fields[f]['type'] == 'timer':
                    self.currentItem.fields[f]['running_since'] = False
                    self.adjustAutoUpdate(self.currentItem, f)

            # delete item and update file
            self.forest.itemPool.deleteItem(self.currentItem)
            self.delayedWriteToFile()

    def pushButtonDeleteBranchClicked(self):
        """
        Delete Node single node and move all children in all branches to the node's parent
        """
        message = "Deleting branch \"" + self.currentItem.name + "\". \n\n" \
                  + "This will delete all branches in all trees.\n" \
                  + "This remove large parts of your data due to connections between trees.\n" \
                  + "Changes are saved to file immediately and cannot be reverted."
        msgBox = QtWidgets.QMessageBox()
        msgBox.setText(message)
        msgBox.setWindowTitle("TreeTime Message")
        msgBox.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel)
        result = msgBox.exec()

        # delete if the user has confirmed
        if result == QtWidgets.QMessageBox.StandardButton.Ok:

            # recursive function to collect all child nodes in all trees
            def collect_children(item, item_list):
                if item not in item_list:
                    item_list += [item]
                    for t in range(0, len(item.trees)):
                        node = item.viewNodes[t]
                        if node:
                            for child in node.children:
                                collect_children(child.item, item_list)

            # find unique list of children to delete, in all trees
            to_delete = []
            collect_children(self.currentItem, to_delete)

            message = ("The selected action will remove {} of all {} nodes.\n"
                       + "This is {:.0f} % of your data.\n"
                       + "Are you sure?").format(len(to_delete), len(self.forest.itemPool.items),
                                                 100.0*len(to_delete)/len(self.forest.itemPool.items))
            msgBox = QtWidgets.QMessageBox()
            msgBox.setText(message)
            msgBox.setWindowTitle("TreeTime Message")
            msgBox.setStandardButtons(
                QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel)
            result = msgBox.exec()

            # delete if the user has confirmed
            if result == QtWidgets.QMessageBox.StandardButton.Ok:
                # remove all running timers
                for i in to_delete:
                    for f in i.fields.keys():
                        if i.fields[f]['type'] == 'timer':
                            i.fields[f]['running_since'] = False
                            self.adjustAutoUpdate(i, f)

                # delete items and update file
                for i in to_delete:
                    self.forest.itemPool.deleteItem(i)
                self.delayedWriteToFile()

    def pushButtonNewDataFieldClicked(self):
        if self.currentMetaNode:
            self.locked = True
            node = self.currentMetaNode.newDataField()

            # select and unfold new node
            node.setExpanded(True)
            self.currentMetaNode.setSelected(False)
            self.locked = False
            node.setSelected(True)
            self.delayedWriteToFile()

    def pushButtonNewTreeFieldClicked(self):
        if self.currentMetaNode:
            self.locked = True
            treeName, node = self.currentMetaNode.newTreeField()
            self.notifyTreeColumnChange(self.forest.treeIndexFromName(treeName))

            # select and unfold new node
            node.setExpanded(True)
            self.currentMetaNode.setSelected(False)
            self.locked = False
            node.setSelected(True)
            self.delayedWriteToFile()

    def pushButtonNewTreeClicked(self):
        if self.currentMetaNode:
            self.locked = True

            # create tree
            tree, node = self.currentMetaNode.newTree()

            # insert new tab
            n = len(self.treeWidgets)-1
            self.createBranchTab(n, 'tabX', tree.name, 0)
            root = self.fillTreeWidget(n, [])
            tree.viewNode = root

            # select and unfold new node
            node.setExpanded(True)
            self.currentMetaNode.setSelected(False)
            self.locked = False
            node.setSelected(True)
            self.delayedWriteToFile()

    def pushButtonDeleteDataFieldClicked(self):
        if self.currentMetaNode:

            # warning message
            message = ("You are about the delete a data field. This will remove data.")
            msgBox = QtWidgets.QMessageBox()
            msgBox.setText(message)
            msgBox.setWindowTitle("TreeTime Message")
            msgBox.setStandardButtons(
                QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel)
            result = msgBox.exec()

            # continue
            if result == QtWidgets.QMessageBox.StandardButton.Ok:

                self.currentMetaNode.deleteDataField()
                self.delayedWriteToFile()

    def pushButtonDeleteTreeFieldClicked(self):
        if self.currentMetaNode:
            treeName = self.currentMetaNode.deleteTreeField()
            self.notifyTreeColumnChange(self.forest.treeIndexFromName(treeName))
            self.delayedWriteToFile()

    def pushButtonDeleteTreeClicked(self):
        if self.currentMetaNode:

            # warning message
            message = ("You are about the delete an entire tree. This will remove the tree structure and delete all "
                       "data items that are only in this tree.")
            msgBox = QtWidgets.QMessageBox()
            msgBox.setText(message)
            msgBox.setWindowTitle("TreeTime Message")
            msgBox.setStandardButtons(
                QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel)
            result = msgBox.exec()

            # continue to remove tree and tab
            if result == QtWidgets.QMessageBox.StandardButton.Ok:
                n = self.currentMetaNode.deleteTree()
                self.tabWidget.removeTab(n)
                self.treeWidgets.pop(n)
                self.delayedWriteToFile()

    def changeEditMode(self, mode):
        """
        Changes the current editing mode to the mode in the parameters.
        Disables/enables buttons, changes button text.
        :param mode: The new mode
        """

        print(f'{mode} editing mode enabled.')

        # set button states and texts
        self.endisableButtons(mode)

        # clear data view
        self.tableWidget.clear()
        self.gridInitialised = False

        # entering tree field mode
        if mode == 'meta' and self.editMode != 'meta':
            self.editMode = mode

            # init display
            self.tableWidget.clear()
            self.gridInitialised = False
            if not len(self.treeWidgets[-1].selectedItems()):
                self.treeWidgets[-1].topLevelItem(0).setSelected(True)

            # if not active, select meta tab
            if self.tabWidget.currentIndex() != len(self.forest.children):
                self.tabWidget.setCurrentIndex(len(self.forest.children))

            # if not active, select meta structure toolbar tab
            if self.toolBox.currentIndex() != 3:
                self.toolBox.setCurrentIndex(3)

            self.itemSelectionChanged()

        # entering content edit mode
        elif mode != 'meta' and self.editMode == 'meta':
            self.editMode = mode

            # if meta tab active, select first tab
            if self.tabWidget.currentIndex() == len(self.forest.children):
                self.tabWidget.setCurrentIndex(0)

            # if meta toolbar tab active, select second tab (edit content)
            if self.toolBox.currentIndex() == 3:
                self.toolBox.setCurrentIndex(1)

            # enable tabs
            for tab in self.tabWidgets:
                tab.setEnabled(True)

            # init display
            self.tableWidget.clear()
            self.gridInitialised = False
            self.itemSelectionChanged()

        # entering empty editmode
        else:
            self.mode = mode

    def loadFile(self, filename):
        self.locked = True
        self.removeBranchTabs()
        self.tabWidgets = []
        self.treeWidgets = []
        self.currentTree = 0
        self.currentItem = None
        self.gridInitialised = False
        self.editMode = 'content'     # one of 'content', 'meta', or 'data'
        if filename is not None and filename != '':
            try:
                # load file
                self.forest = Forest(filename)
                self.createBranchTabs()
                self.fillTreeWidgets()

                # init all timers
                self.initAllAutoUpdates()

                # select first item
                if len(self.treeWidgets):
                    firstTree = self.treeWidgets[0]
                    items = firstTree.topLevelItemCount()
                    if items:
                        firstTree.topLevelItem(0).setSelected(True)

                self.locked = False

            # in case of failure, show user message and reload
            except KeyError as e:

                errorMessage("File loading failed. Retrying.")
                self.locked = False
                self.pushButtonLoadFileClicked()

        else:
            self.locked = False
            self.pushButtonLoadFileClicked()

    def newFile(self, filename):
        if filename is not None and filename != '':

            # write empty file
            with open(filename, "w") as f:
                f.write('--trees--\n\n')
                f.write('tree "Tree"\n\n')
                f.write('--item-types--\n\n')
                f.write('item Entry\n')
                f.write('    fields {}\n')
                f.write('    trees [[]]\n\n')
                f.write('--item-pool--\n\n')
                f.write('item Entry\n')
                f.write('    fields {}\n')
                f.write('    trees [[0]]\n\n')

            self.loadFile(filename)

        else:
            self.pushButtonNewFileClicked()

    def delayedWriteToFile(self, countdown=False):
        """
        Schedules a write after 5 seconds. A timer is called that stops after 1 second and decreases a counter.
        If the counter is at zero, a write-to-file is performed. If the counter is > 0, it is counted down and a
        new timer is started.
        When changing cell contents, each cell change calls a delayed write. The file is only written if there has
        been no change for 5 seconds (otherwise the continuous file writing slows the user input down).
        :param countdown: Whether to count down and trigger a write at zero (=True),
                          or whether to init a new run (=False)
        :return:
        """

        # We've been called by a timer process, count down and possibly write
        if countdown:
            if self.write_delay > 0:
                self.write_delay -= 1
                if self.write_delay == 0:
                    self.writeToFile()
                self.write_timer = False

        # We've been called after a cell change, init the counter
        else:
            self.write_delay = 5

        # The counter is up, start another 2-second timer
        if self.write_delay > 0 and not self.write_timer:
            self.write_timer = Timer(1, self.delayedWriteToFile, kwargs={'countdown': True})
            self.write_timer.start()

    def writeToFile(self, delayed=False, countdown=False):
            self.forest.writeToFile(self.labelCurrentFile.text())

    def removeBranchTabs(self):
            self.tabWidget.clear()
            self.tabWidgets = []

    def createBranchTab(self, index, tabName, tabText, nColumns):
        newTab = QtWidgets.QWidget()
        newTab.setObjectName(tabName)
        newGridLayout = QtWidgets.QGridLayout(newTab)
        self.tabWidget.insertTab(index, newTab, "")
        self.tabWidgets += [newTab]
        self.tabWidget.setTabText(index, tabText)
        newTree = QtWidgets.QTreeWidget(newTab)
        newTree.setGeometry(QtCore.QRect(0, 0, 731, 751))
        newTree.setAllColumnsShowFocus(True)
        newTree.setWordWrap(True)
        newTree.headerItem().setText(0, "1")
        newTree.setColumnCount(nColumns)
        newGridLayout.addWidget(newTree, 0, 0, 1, 1)
        self.treeWidgets.insert(index, newTree)

    def createBranchTabs(self):
        
        self.locked = True
        
        # create tabs and tree widgets
        for n,c in enumerate(self.forest.children):
            self.createBranchTab(len(self.tabWidgets), "tab" + str(n), c.name, len(c.fieldOrder))

        # create meta tabs and widget
        self.createBranchTab(n+1, "tabM", "[Meta Structure]", 2)

        self.locked = False

    def notifyTreeNameChange(self, n):
        self.tabWidget.setTabText(n, self.forest.children[n].name)

    def notifyTreeFieldNameChange(self, n, m):
        newName = self.forest.children[n].fieldOrder[m]
        self.treeWidgets[n].headerItem().setText(m+1, newName)

    def notifyTreeColumnChange(self, n):
        """
        Re-reads the column information from tree n and adapts the display.
        :param n: Tree index
        :return: void
        """
        fieldOrder = self.forest.children[n].fieldOrder
        treeWidget = self.treeWidgets[n]
        treeWidget.setColumnCount(len(fieldOrder)+1)
        for f, field in enumerate(fieldOrder):
            treeWidget.headerItem().setText(f+1, field)
        return True

    def resizeNameColumn(self):
        if self.editMode == 'content':
            self.treeWidgets[self.currentTree].resizeColumnToContents(0)
        else:
            self.treeWidgets[-1].resizeColumnToContents(0)

    def fillTreeWidget(self, n, labels):
        self.treeWidgets[n].itemSelectionChanged.connect(self.itemSelectionChanged)
        self.treeWidgets[n].itemCollapsed.connect(self.resizeNameColumn)
        self.treeWidgets[n].itemExpanded.connect(self.resizeNameColumn)
        self.treeWidgets[n].setHeaderLabels(labels)
        return self.treeWidgets[n].invisibleRootItem()

    def fillTreeWidgets(self):
        """
        Fills all tree tabs by creating all nodes
        """

        # for each tree in the forest
        for n, c in enumerate(self.forest.children):
            root = self.fillTreeWidget(n, [""] + c.fieldOrder)
            c.viewNode = root

            # add branch
            for b in c.children:
                parent = QNode(b, c.fieldOrder)
                root.addChild(parent)

            # init sorting
            self.treeWidgets[n].setSortingEnabled(True)
            self.treeWidgets[n].sortItems(0, QtCore.Qt.SortOrder.AscendingOrder)

            # expand name column so all names are readable
            self.treeWidgets[n].resizeColumnToContents(0)

        # for the meta tree
        m = len(self.forest.children)
        root = self.fillTreeWidget(m, ["Name", "Type / Content"])

        # add items
        for it in self.forest.itemTypes.items:
            parent = QMetaNode(it, self.forest, name=it.name, nodeType='data item')
            root.addChild(parent)

        # add trees
        for t, tree in enumerate(self.forest.children):
            parent = QMetaNode(self.forest.children[t], self.forest, name=tree.name, nodeType='tree')
            root.addChild(parent)

        # expand name column so all names are readable
        self.treeWidgets[m].resizeColumnToContents(0)

    def _protectCells(self, row, columns):

        if not self.gridInitialised:
            nonEditFlags = QtCore.Qt.ItemFlag.NoItemFlags
            empties = []
            for i in columns:
                if not self.tableWidget.item(row, i):
                    empties.append(QtWidgets.QTableWidgetItem(""))
                    empties[-1].setFlags(nonEditFlags)
                    self.tableWidget.setItem(row, i, empties[-1])
                else:
                    self.tableWidget.item(row, i).setFlags(nonEditFlags)

    def itemSelectionChanged(self):

        if self.editMode == 'content':
            self.showContentInDataView(self.currentTree)
        elif self.editMode == 'meta':
            if self.currentMetaNode:
                self.endisableMetaButtons(self.currentMetaNode.nodeType, False)   # disable old
            self.showTreeFieldInDataView()   # display content and set currentMetaNode
            self.endisableMetaButtons(self.currentMetaNode.nodeType, True)    # enable new

    def showContentInDataView(self, treeIndex):
        if not self.locked:
            
            self.locked = True

            # init
            selectedItems = self.treeWidgets[treeIndex].selectedItems()

            # we have something to write
            if selectedItems != []:

                qnode = selectedItems[0]
                
                # unselect previous item in all trees
                if self.currentItem is not None:
                    try:
                        self.currentItem.select(False)
                    except:
                        print("warning: Unselecting of {} didn't work".format(self.currentItem.name))
                        pass

                # select current item in all trees
                self.currentItem = qnode and (qnode.sourceNode and qnode.sourceNode.item or None) or None
                if self.currentItem is None:
                    self.tableWidget.clear()
                    self.gridInitialised = False
                    self.locked = False
                    return
                self.currentItem.select(True)
                
                # expand current item in current tree
                parent = qnode.parent()
                while parent is not None:
                    parent.setExpanded(True)
                    parent = parent.parent()
                
                # create non-edit flags
                nonEditFlags = QtCore.Qt.ItemFlag.NoItemFlags

                # go through all lines of the table
                n = 0

                # empty line
                self._protectCells(0, [0, 1, 2, 3, 4])
                n += 1

                # add name
                name = QtWidgets.QTableWidgetItem(self.currentItem.name)
                font = name.font()
                font.setPointSize(self._dataFontPointSize + 3)
                name.setFont(font)
                font.setPointSize(self._dataFontPointSize)
                self.tableWidget.setItem(n, 3, name)
                self._protectCells(n, [0, 2, 4])
                n += 1
                
                # empty line
                self._protectCells(n, [0, 1, 2, 3, 4])
                n += 1

                # add all parents in tree parent path
                for treeNumber, path in enumerate(self.currentItem.trees):
                    tree = self.forest.children[treeNumber]
                    name = QtWidgets.QTableWidgetItem(tree.name)
                    name.setFlags(nonEditFlags)
                    name.setTextAlignment(0x82)
                    self.tableWidget.setItem(n, 1, name)
                    buttonbox = QtWidgets.QDialogButtonBox()
                    if not len(path):
                        button = QtWidgets.QToolButton()
                        button.setFont(font)
                        button.setText("")
                        button.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextOnly)
                        button.setFixedWidth(button.sizeHint().height())
                        button.setFixedHeight(button.sizeHint().height())
                        button.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.InstantPopup)     # no down arrow is shown, menu pops up on click
                        button.setStyleSheet("::menu-indicator{ image: none; }")
                        button.setMenu(self.createParentMenu(treeNumber, self.forest))
                        buttonbox.addButton(button, QtWidgets.QDialogButtonBox.ButtonRole.ResetRole)
                    elif len(path) == 1:
                        parent = tree.findNode(path).parent
                        button = QtWidgets.QToolButton()
                        button.setFont(font)
                        button.setArrowType(QtCore.Qt.ArrowType.RightArrow)
                        # get square buttons of same size as text buttons for > buttons
                        button.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextOnly)
                        button.setStyleSheet("::menu-indicator{ image: none; }")
                        button.setFixedWidth(button.sizeHint().height())
                        button.setFixedHeight(button.sizeHint().height())
                        button.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonIconOnly)
                        button.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.InstantPopup)     # no down arrow is shown, menu pops up on click
                        button.setMenu(self.createParentMenu(treeNumber, parent))
                        buttonbox.addButton(button, QtWidgets.QDialogButtonBox.ButtonRole.ResetRole)
                    else:
                        for p in range(1, len(path)):
                            parent = tree.findNode(path[0:p])
                            button = QtWidgets.QToolButton()
                            button.setFont(font)
                            button.setArrowType(QtCore.Qt.ArrowType.RightArrow)
                            button.setText(parent.name)
                            button.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextOnly)
                            button.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.InstantPopup)     # no down arrow is shown, menu pops up on click
                            button.setStyleSheet("::menu-indicator{image:none;}")
                            button.setMenu(self.createParentMenu(treeNumber, parent.parent))
                            buttonbox.addButton(button, QtWidgets.QDialogButtonBox.ButtonRole.ResetRole)
                        button = QtWidgets.QToolButton()
                        button.setFont(font)
                        button.setArrowType(QtCore.Qt.ArrowType.RightArrow)
                        button.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextOnly)     # get square buttons of same size as text buttons for > buttons
                        button.setStyleSheet("::menu-indicator{ image: none; }")
                        button.setFixedWidth(button.sizeHint().height())
                        button.setFixedHeight(button.sizeHint().height())
                        button.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonIconOnly)
                        button.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.InstantPopup)     # no down arrow is shown, menu pops up on click
                        button.setMenu(self.createParentMenu(treeNumber, parent))
                        buttonbox.addButton(button, QtWidgets.QDialogButtonBox.ButtonRole.ResetRole)

                    self.tableWidget.setCellWidget(n, 3, buttonbox)
                    self._protectCells(n, [0, 2, 4])
                    n += 1
                    
                # empty line
                self._protectCells(n, [0, 1, 2, 3, 4])
                n += 1

                # add item fields
                for key in sorted(self.currentItem.fields):
                    name = QtWidgets.QTableWidgetItem(key)
                    name.setFlags(nonEditFlags)
                    name.setTextAlignment(0x82)
                    if self.currentItem.fields[key]['type'] in ('text', 'longtext'):
                        text = self.currentItem.fields[key]["content"]
                        text = text and str(text) or ""     # display "None" values as empty string
                        height = self.currentItem.fields[key]['type'] == 'text' and 10 or 25
                        widget = TextEdit(text, lambda row=n: self.tableWidgetCellChanged(row, 3), height)
                        widget.setFont(font)
                        self.tableWidget.setCellWidget(n, 3, widget)
                    elif self.currentItem.fields[key]['type'] == 'url':
                        value = self.currentItem.fields[key]["content"]
                        value = value and str(value) or ""  # display "None" values as empty string
                        widget = UrlWidget(value, lambda row=n: self.tableWidgetCellChanged(row, 3), font)
                        self.tableWidget.setCellWidget(n, 3, widget)
                    elif self.currentItem.fields[key]['type'] == 'timer':
                        value = self.currentItem.fields[key]["content"]
                        running_since = self.currentItem.fields[key]["running_since"]
                        widget = TimerWidget(value, running_since, lambda row=n: self.tableWidgetCellChanged(row, 3),
                                             font)
                        self.tableWidget.setCellWidget(n, 3, widget)
                    else:
                        value = self.currentItem.fields[key]["content"]
                        value = value and str(value) or ""     # display "None" values as empty string
                        value = QtWidgets.QTableWidgetItem(value)
                        self.tableWidget.setItem(n, 3, value)
                    self.tableWidget.setItem(n, 1, name)
                    self._protectCells(n, [0, 2, 4])
                    n += 1

            # an empty page, clear and initialise
            else:
                self.tableWidget.clear()
                self.gridInitialised = False
                n = 0

            # empty lines to fill the 23 lines in the main view
            if n < 23:
                for k in range(n, 23):
                    self._protectCells(k, [0, 1, 2, 3, 4])

            self.gridInitialised = True
            self.locked = False

    def showTreeFieldInDataView(self):
        """ Shows the content of the tree field indicated by the current column
        :param treeIndex: The ID of the tree
        :return: None
        """

        if not self.locked:
            self.locked = True

            # init
            selection = self.treeWidgets[-1].selectedItems()
            if len(selection):
                [currentNode] = selection
                [self.currentMetaNode] = selection

                # create non-edit flags
                nonEditFlags = QtCore.Qt.ItemFlag.NoItemFlags

                # go through all lines of the table
                n = 0

                # empty line
                self._protectCells(0, [0, 1, 2, 3, 4])
                n += 1

                # field name plus empty line
                name = QtWidgets.QTableWidgetItem(currentNode.name)
                #name.setFlags(nonEditFlags)
                font = name.font()
                font.setPointSize(self._dataFontPointSize + 3)
                name.setFont(font)
                font.setPointSize(self._dataFontPointSize)
                self.tableWidget.setItem(n, 3, name)
                #self._protectCells(n, [0, 1, 2, 4])
                n += 1
                self.tableWidget.setItem(n, 3, QtWidgets.QTableWidgetItem(currentNode.nodeType))
                self._protectCells(n, [0, 1, 2, 3, 4])
                n += 1
                self._protectCells(n, [0, 1, 2, 3, 4])
                n += 1

                # field type plus empty line
                self.tableWidget.setItem(n, 1, QtWidgets.QTableWidgetItem("Type"))
                if currentNode.nodeType == 'data field':
                    widget = QtWidgets.QComboBox()
                    widget.setFont(font)
                    widget.addItems([f'{k} [{v}]' for k,v in Item.FieldTypes.items()])
                    cnt = currentNode.contentType
                    widget.setCurrentText(f'{cnt} [{Item.FieldTypes[cnt]}]')
                    widget.currentTextChanged.connect(lambda x: currentNode.changeType(x))
                    self.tableWidget.setCellWidget(n, 3, widget)
                elif currentNode.nodeType == 'tree field':
                    widget = QtWidgets.QComboBox()
                    widget.setFont(font)
                    cnt = currentNode.contentType
                    widget.addItems([f'{k} [{v}]' for k,v in Field.Types.items()])
                    widget.setCurrentText(f'{cnt} [{Field.Types[cnt]}]')
                    widget.currentTextChanged.connect(lambda x: currentNode.changeType(x))
                    self.tableWidget.setCellWidget(n, 3, widget)
                else:
                    self.tableWidget.setCellWidget(n, 3, None)
                    self.tableWidget.setItem(n, 3, QtWidgets.QTableWidgetItem(currentNode.contentType))
                self._protectCells(n, [0, 1, 2, 3, 4])
                n += 1
                self._protectCells(n, [0, 1, 2, 3, 4])
                n += 1

                # display content according to type
                if currentNode.nodeType == 'data field':
                    self.tableWidget.setItem(n, 1, QtWidgets.QTableWidgetItem("Default"))
                    self.tableWidget.setCellWidget(n, 3, None)     # remove possible leftovers from previous fields
                    self.tableWidget.setItem(n, 3, QtWidgets.QTableWidgetItem(currentNode.source['content']))
                    self._protectCells(n, [0, 1, 2, 3, 4])
                    n += 1
                    self.tableWidget.setCellWidget(n, 3, None)     # remove possible leftovers from previous fields
                    self._protectCells(n, [0, 1, 2, 3, 4])
                    n += 1
                elif currentNode.nodeType == 'tree field':
                    self.tableWidget.setItem(n, 1, QtWidgets.QTableWidgetItem("Visibility"))
                    widget = QtWidgets.QCheckBox()
                    widget.setFont(font)
                    widget.setText("hidden")
                    widget.setChecked(currentNode.source.hidden)
                    treeIndex = self.forest.treeIndexFromName(currentNode.parentName)
                    widget.checkStateChanged.connect(lambda x, t=treeIndex: currentNode.changeVisibility(x) and self.notifyTreeColumnChange(t))
                    self.tableWidget.setCellWidget(n, 3, widget)
                    self._protectCells(n, [0, 1, 2, 3, 4])
                    n += 1
                    self.tableWidget.setCellWidget(n, 3, None)     # remove possible leftovers from previous fields
                    self._protectCells(n, [0, 1, 2, 3, 4])
                    n += 1
                elif currentNode.nodeType == 'parameter list':
                    self.tableWidget.setItem(n, 1, QtWidgets.QTableWidgetItem("Parameters"))

                    # for standard fields, get list of node names, excluding forbidden ones (avoid recursion)
                    ctype = currentNode.parent().contentType
                    if ctype not in ('node-path', 'node-name'):
                        bare, annotated, bareToAnn, annToBare = currentNode.parent().parent().availableFields(
                            exclude = ((currentNode.name in ('own-fields', 'sibling-fields') and currentNode.parentName)
                                      or None),
                            datatype = ctype != 'count' and Field.Types[currentNode.parent().contentType] or 'any')    # you can count anything

                    # for node-name and node-path fields, create tree list (for parent field)
                    elif currentNode.name == 'parent-fields':
                        bare = [''] + [str(c) for c in range(0, currentNode.parent().parent().parent().childCount() - 1)]
                        annotated = [''] + [f'{t.name} [tree]' for n, t in enumerate(self.forest.children)]
                        bareToAnn = {a[0]:a[1] for a in zip(bare, annotated)}

                    # for node-name and node-path fields, for non-parent lists, nothing's defined
                    else:
                        bare = ['']
                        annotated = ['']
                        bareToAnn = {'': ''}

                    # create drow-down box for each parameter in list
                    offset = n
                    for cnt in currentNode.source:
                        widget = QtWidgets.QComboBox()
                        widget.setFont(font)

                        # if in list, translate field to annoated version
                        if str(cnt) in bareToAnn.keys():
                            widget.addItems(annotated)
                            widget.setCurrentText(bareToAnn[str(cnt)])

                        # if not in list, type must be wrong. show warning and proceed
                        else:
                            warningentry = f'Type Mismatch: {cnt}'
                            widget.addItems(annotated + [warningentry])
                            widget.setCurrentText(warningentry)
                            errorMessage(f'Type error: field "{cnt}" as defined in tree field has wrong type.')

                        widget.currentTextChanged.connect(lambda x, p=n-offset: currentNode.changeContent(x, p)
                                                                                and self.showTreeFieldInDataView())
                        self.tableWidget.setCellWidget(n, 3, widget)
                        self._protectCells(n, [0, 1, 2, 3, 4])
                        n += 1

                    # add additional drop-down box for new entry
                    widget = QtWidgets.QComboBox()
                    widget.setFont(font)
                    widget.addItems(annotated)
                    widget.currentTextChanged.connect(lambda x, p=n-offset: currentNode.changeContent(x, p)
                                                                            and self.showTreeFieldInDataView())
                    self.tableWidget.setCellWidget(n, 3, widget)
                    self._protectCells(n, [0, 1, 2, 3, 4])
                    n += 1

                # empty lines to fill the 23 lines in the main view
                if n < 23:
                    for k in range(n, 23):
                        self.tableWidget.setItem(k, 1, QtWidgets.QTableWidgetItem(''))
                        self.tableWidget.setCellWidget(k, 3, None)
                        self.tableWidget.setItem(k, 3, QtWidgets.QTableWidgetItem(''))
                        self._protectCells(k, [0, 1, 2, 3, 4])

            self.gridInitialised = True
            self.locked = False

    def showDataFieldsInDataView(self):
        """ Shows the content of the tree field indicated by the current column
        :param treeIndex: The ID of the tree
        :return: None
        """

        if not self.locked:
            self.locked = True

            # create non-edit flags
            nonEditFlags = QtCore.Qt.ItemFlag.NoItemFlags

            fields = self.forest.itemTypes.items[0].fields

            # go through all lines of the table
            n = 0

            # empty line
            self._protectCells(n, [0, 1, 2, 3, 4])
            n += 1

            # go through all fields
            for name, field in fields.items():

                # field name
                fieldName = QtWidgets.QTableWidgetItem("Name")
                fieldName.setFlags(nonEditFlags)
                fieldName.setTextAlignment(0x82)
                self.tableWidget.setItem(n, 1, fieldName)
                self.tableWidget.setItem(n, 3, QtWidgets.QTableWidgetItem(name))
                self._protectCells(n, [0, 1, 2, 3, 4])
                n += 1

                # field type
                fieldType = QtWidgets.QTableWidgetItem("Type")
                fieldType.setFlags(nonEditFlags)
                fieldType.setTextAlignment(0x82)
                self.tableWidget.setItem(n, 1, fieldType)
                self.tableWidget.setItem(n, 3, QtWidgets.QTableWidgetItem(field['type']))
                self._protectCells(n, [0, 1, 2, 3, 4])
                n += 1

                # field content
                fieldContent = QtWidgets.QTableWidgetItem("Default Content")
                fieldContent.setFlags(nonEditFlags)
                fieldContent.setTextAlignment(0x82)
                self.tableWidget.setItem(n, 1, fieldContent)
                self.tableWidget.setItem(n, 3, QtWidgets.QTableWidgetItem(field['content']))
                self._protectCells(n, [0, 1, 2, 3, 4])
                n += 1

                # empty line
                self._protectCells(n, [0, 1, 2, 3, 4])
                n += 1

            # empty lines to fill the 23 lines in the main view
            if n < 23:
                for k in range(n, 23):
                    self._protectCells(k, [0, 1, 2, 3, 4])

            self.gridInitialised = True
            self.locked = False

    def createParentMenu(self, treeIndex, parent):
        """
        Displays a menu with possible children to select, at the current mouse cursor position.
        """
        menu = QtWidgets.QMenu()

        # root node
        if not parent.parent:
            action = QtGui.QAction("Add to Top Level", menu)
            action.triggered.connect(lambda checked, t=treeIndex,
                                            p=self.forest.children[treeIndex]: self.moveCurrentItemToNewParent(t, p))
            menu.addAction(action)
            parent = self.forest.children[treeIndex]
            menu.addSeparator()

        # top level node only
        else:
            if not parent.parent.parent:
                action = QtGui.QAction("Remove from Tree", menu)
                action.triggered.connect(lambda checked, t=treeIndex, p=False: self.moveCurrentItemToNewParent(t, p))
                menu.addAction(action)
                action = QtGui.QAction("Add to Top Level", menu)
                action.triggered.connect(lambda checked, t=treeIndex, p=self.forest.children[treeIndex]:
                                         self.moveCurrentItemToNewParent(t, p))
                menu.addAction(action)
                parent = self.forest.children[treeIndex]
                menu.addSeparator()

        # all other nodes
        currentNode = self.currentItem.viewNodes[treeIndex]
        for c in sorted(parent.children, key=lambda x: x.name):
            if c != currentNode:
                action = QtGui.QAction(c.name, menu)
                action.triggered.connect(lambda checked, t=treeIndex, p=c: self.moveCurrentItemToNewParent(t, p))
                menu.addAction(action)

        return menu

    def tabWidgetCurrentChanged(self, tree):
        '''Called when the user selects another tree in the tab widget.
        '''

        if tree == len(self.forest.children):
            self.changeEditMode('meta')
        else:
            self.changeEditMode('content')
            self.currentTree = tree
        self.itemSelectionChanged()

    def toolBoxCurrentChanged(self, page):
        '''Called when the user selects another tree in the tab widget.
        '''

        if page == 3:     # select the meta structure page
            self.changeEditMode('meta')
        elif page in (0,1,2):    # no change when "Settings" tab is selected
            if self.editMode == 'meta':
                self.changeEditMode('content')

    def tableWidgetCellChanged(self, row, column):
        """
        Called when the user wants to change the item name, field content or parent via the grid
        """

        if not self.locked:
            self.locked = True
            if column == 3:
                
                # the node name has been changed
                if row == 1:
                    newName = self.tableWidget.item(row, column).text()
                    if self.editMode == 'content':
                        self.currentItem.changeName(newName)
                    else:
                        changeOk = self.currentMetaNode.changeName(newName)
                        if changeOk:
                            metaType = self.currentMetaNode.nodeType
                            if metaType == 'tree':
                                self.notifyTreeNameChange(self.forest.treeIndexFromName(newName))
                            elif metaType == 'tree field':
                                t = self.forest.treeIndexFromName(self.currentMetaNode.parentName)
                                f = self.forest.children[t].fieldIndexFromName(newName)
                                self.notifyTreeFieldNameChange(t, f)
                        else:
                            self.tableWidget.item(row, column).setText(self.currentMetaNode.name)

                # one of the fields has been changed
                else:
                    if self.editMode == 'content':
                        fieldName = self.tableWidget.item(row, 1).text()
                        fieldType = self.currentItem.fields[fieldName]['type']
                        if fieldType in ('longtext', 'text', 'url'):
                            newValue = self.tableWidget.cellWidget(row, 3).toPlainText()
                        elif fieldType == 'timer':
                            newValue = (self.tableWidget.cellWidget(row, 3).toPlainText(),
                                        self.tableWidget.cellWidget(row, 3).runningSince())
                        else:
                            newValue = self.tableWidget.item(row, 3).text()

                        # update value and send warning if there's a recursion error
                        if not self.currentItem.changeFieldContent(fieldName, newValue):
                            errorMessage(f'Infinite recursion caused by field "{fieldName}".\n'
                                       'Please check your tree definition for circular dependencies.')

                        # add/remove timer to list
                        if fieldType == 'timer':
                            self.adjustAutoUpdate(self.currentItem, fieldName)
                    else:
                        # change type of data field (all other changes are via dropdown box)
                        newType = self.tableWidget.item(row, 3).text()
                        try:
                            self.currentMetaNode.changeContent(newType)
                            if self.currentMetaNode.nodeType == 'parameter list':
                                self.showTreeFieldInDataView()
                        except:
                            pass

            self.locked = False
            self.delayedWriteToFile()

    def adjustAutoUpdate(self, item, fieldName):
        """ Adds or removes a timer to the list of fields to auto-update.
        """
        if item.fields[fieldName]['type'] == 'timer':
            # if timer is running, add it to the list
            if item.fields[fieldName]['running_since']:
                if (item, fieldName) not in self.auto_updates:
                    self.auto_updates.append((item, fieldName))

            # if timer is stopped, remove it from the list
            else:
                if (item, fieldName) in self.auto_updates:
                    del self.auto_updates[self.auto_updates.index((item, fieldName))]

    def initAllAutoUpdates(self):
        """
        Initialise all auto-update fields after loading.
        """
        for i in self.forest.itemPool.items:
            for f in i.fields:
                    self.adjustAutoUpdate(i, f)

    def updateTimers(self):
        # update all timers that are running
        for item, fieldName in self.auto_updates:
            if not item.changeFieldContent(fieldName, [True]):
                errorMessage(f'Infinite recursion caused by field "{fieldName}".\n'
                           'Please check your tree definition for circular dependencies.')

    def createNode(self, insertas, copy, recurse = False, srcItem = None, destItem = None):
        
        treeWidget = self.treeWidgets[self.currentTree]

        # set source and destination nodes and qnodes
        if recurse and srcItem is not None and destItem is not None:
            sourceItem = srcItem
            sourceNode = srcItem.viewNodes[self.currentTree]
            sourceQNode = sourceNode.viewNode
            destNode = destItem.viewNodes[self.currentTree]
            destQNode = destNode.viewNode
        elif len(treeWidget.selectedItems()):
            sourceQNode = treeWidget.selectedItems()[0]
            sourceNode = sourceQNode.sourceNode
            sourceItem = sourceNode.item
            destNode = sourceNode.parent
            destQNode = sourceQNode.parent()
        else:
            sourceQNode = treeWidget.invisibleRootItem()
            sourceNode = self.forest.children[self.currentTree]
            sourceItem = sourceNode.item
            destQNode = sourceQNode
            destNode = sourceNode
            insertas = 'root'

        if destQNode is None:
            destQNode = treeWidget.invisibleRootItem()

        if copy:
            item = self.forest.itemPool.copyItem(sourceItem)
            for n,t in enumerate(self.forest.children):
                if n != self.currentTree and item.trees[n] != []:
                    oldNode = t.findNode(item.trees[n])
                    newNode = oldNode.parent.addItemAsChild(item)
                    newQNode = QNode(newNode, self.forest.children[n].fieldOrder)
                    parent = oldNode.viewNode.parent()
                    if parent is None:
                        parent = self.treeWidgets[n].invisibleRootItem()
                    parent.addChild(newQNode)
        else:
            # add new item by copying the first type
            item = self.forest.itemPool.copyItem(self.forest.itemTypes.items[0])

        if insertas == "root":

            # create default node and add item to it
            node = sourceNode.addItemAsChild(item)
            qnode = QNode(node, self.forest.children[self.currentTree].fieldOrder)
            sourceQNode.addChild(qnode)

            # expand new entry
            qnode.setExpanded(True)

        if insertas == "child":

            # create default node and add item to it
            node = sourceNode.addItemAsChild(item)
            qnode = QNode(node, self.forest.children[self.currentTree].fieldOrder)
            sourceQNode.addChild(qnode)

            # expand parent and select new item
            sourceQNode.setExpanded(True)

        elif insertas == "sibling":

            if destNode is None or destQNode is None:
                return

            # create default node and add item to it
            node = destNode.addItemAsChild(item)
            qnode = QNode(node, self.forest.children[self.currentTree].fieldOrder)
            destQNode.addChild(qnode)

            # expand parent and select new item
            destQNode.setExpanded(True)

        elif insertas == "parent":

            if destNode is None or destQNode is None:
                return

            node = destNode.addItemAsChild(item)

            # move original node to be child of new parent
            destNode.removeChild(sourceNode)
            node.addNodeAsChild(sourceNode)
            destQNode.removeChild(sourceQNode)
            qnode = QNode(node, self.forest.children[self.currentTree].fieldOrder)
            destQNode.addChild(qnode)

            # expand parent and select new item
            qnode.setExpanded(True)

        if recurse:
            for c in sourceNode.children:
                self.createNode(insertas, copy, recurse, c.item, node.item)

        if srcItem is None:
            treeWidget.setCurrentItem(qnode)
            self.delayedWriteToFile()
    
    def moveCurrentItemToNewParent(self, treeIndex, newParent):
        treeWidget = self.treeWidgets[self.currentTree]
        if len(treeWidget.selectedItems()):

            self.locked = True

            # save current item
            item = self.currentItem

            # error message if recursion is tried
            if newParent and (newParent.item == item):
                return errorMessage("You are trying to set\n" + item.name + "\nto be its own parent. This is not supported.")

            # find old parent
            oldParent = item.viewNodes[treeIndex]
            disappeared = False

            # either remove node from tree
            if oldParent and not newParent:

                # question to user: Really remove?
                message = "Removing node \"" + item.name + "\" from the tree \"" \
                          + self.forest.children[self.currentTree].name +"\".\n\n" \
                          "This will remove all descendants (children, grandchildren, ...). " \
                          "Changes are saved to file immediately and cannot be reverted.\nProceed?"
                msgBox = QtWidgets.QMessageBox()
                msgBox.setText(message)
                msgBox.setWindowTitle("TreeTime Message")
                msgBox.setStandardButtons(
                    QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel)
                result = msgBox.exec()

                # remove if the user has confirmed
                if result == QtWidgets.QMessageBox.StandardButton.Ok:

                    # recursive function to collect all child nodes in the trees
                    def collect_children(item, item_list, tree):
                        if item not in item_list:
                            item_list += [item]
                            node = item.viewNodes[tree]
                            if node:
                                for child in node.children:
                                    collect_children(child.item, item_list, tree)

                    # find unique list of children to remove, and remove them
                    to_remove = []
                    collect_children(self.currentItem, to_remove, treeIndex)
                    for i in to_remove:

                        # remove single item
                        i.removeFromTree(treeIndex)

                        # delete if orphaned
                        if not sum([len(t) for t in i.trees]):
                            self.forest.itemPool.deleteItem(i)

                    if treeIndex == self.currentTree:
                        disappeared = True     # remember that we've removed a node from the current tree
                else:
                    return

            # or move node within the tree
            elif oldParent and newParent and newParent.item:
                item.moveInTree(treeIndex, newParent.item.trees[treeIndex])
            elif oldParent and newParent and not newParent.item:
                item.moveInTree(treeIndex, [])

            # or add node to tree
            elif not oldParent and newParent:
                tree = self.forest.children[treeIndex]
                node = newParent.addItemAsChild(item)
                newQNode = QNode(node, tree.fieldOrder)
                newParent.viewNode.addChild(newQNode)
            
            # save change
            item.notifyFieldChange('')

            # select new item after moving, if it is still part of the current tree
            if not disappeared:
                treeWidget.setCurrentItem(item.viewNodes[self.currentTree].viewNode)

            # clear table widget to prevent segfault crash in qt lib
            self.locked = False
            self.gridInitialised = False
            self.tableWidget.clear()

            # update table and write to file
            self.itemSelectionChanged()
            self.delayedWriteToFile()
        return True


class TreeTime:
    
    def __init__(self, filename=None):
        
        app = QtWidgets.QApplication(sys.argv)
        if platform.system() == "Windows":
            app.setStyle("Fusion")

        # start main window
        main_window = TreeTimeWindow(filename=filename)

        # start web server and prepare its end
        logo = ApplicationLogo()
        main_window.startWebServer(logo.png)
        app.lastWindowClosed.connect(main_window.stopWebServer)

        # add application icon; test several options, different deploy methods are doing different things with the
        # resources, unfortunately
        main_window.setWindowIcon(logo.icon)

        # run
        main_window.show()
        sys.exit(app.exec())


class ApplicationLogo:

    _data = ("iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAMAAABrrFhUAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9"
             "kT1Iw0AcxV8/pCKVDnaQ4pChCopdVMRRq1CECqFWaNXB5NIvaNKQpLg4Cq4FBz8Wqw4uzro6uAqC"
             "4AeIq4uToouU+L+k0CLGg+N+vLv3uHsH+JtVpprBWUDVLCOTSgq5/KoQekUQMUQwhlGJmfqcKKbh"
             "Ob7u4ePrXYJneZ/7c/QrBZMBPoF4lumGRbxBPL1p6Zz3iaOsLCnE58TjBl2Q+JHrsstvnEsO+3lm"
             "1Mhm5omjxEKpi+UuZmVDJZ4ijiuqRvn+nMsK5y3OarXO2vfkLwwXtJVlrtMcQgqLWIIIATLqqKAK"
             "CwlaNVJMZGg/6eGPOX6RXDK5KmDkWEANKiTHD/4Hv7s1i5MTblI4CfS82PbHMBDaBVoN2/4+tu3W"
             "CRB4Bq60jr/WBGY+SW90tPgRENkGLq47mrwHXO4Ag0+6ZEiOFKDpLxaB9zP6pjwwcAv0rbm9tfdx"
             "+gBkqav0DXBwCIyUKHvd49293b39e6bd3w/a8HLQYu1JmQAAAwBQTFRFfwAAgQIAgwQBhAYDhQgE"
             "hQkMiA4OixIQgxUMhhgOhhgUiRwWih0cjB4YhiIcjSAfjyEaiCQdkCMhiiYfiiYkkiYjii0mii4s"
             "kS0pjDAtjTEulDAsizYvkjUyjDc1mTQvjTg2jjk3mzYxkDoznDc3kDs4jj85mTw3j0A6j0FAnD45"
             "l0E+kUNCkEdDokM+o0VFlExMoElFm0tEkVBNl09PmlBMpU5JqFBMlFhTqlJTlFlZm1hWlltbqVhV"
             "mV1dlV9dl2Ffp11crlxZmWNhsV9comVfmGhjrmJdo2ZmmWpqoWtom21trmlmmXButGlom3JwoHFx"
             "p3BtnXRytG5rnnVzm3l1p3ZwsnR0p3h4nnx3u3Vxnn1+s3t4unp0nYKBpIF9qYB+n4SDpoWGoYeG"
             "pImIwYKBvISAoIyJw4SDuIiIo4+LpZCNr42Oo5GTtoyKwomGrJGQxIuHvo2NqZSRopaXx46KspaV"
             "ppqauJWWypGNo56dr5uYqZ2eyJWPwpeUpaCfyJeXv5ydqKOiy5mZwp6Zq6alzpybvqKhp6mm0J6e"
             "uKWns6enxqKdrqmoqauoqqypq62qzqOgtqqqyKWmrK6rra+s0aWiua2urrGtr7Kux6uq1KimsLOv"
             "sbSws7Wy16ypzLCvtbe0tri1wbW2t7m2uLq317Ktubu4zLe02bSvu726xrq62LW207e2vL67wr27"
             "w768vcC83Li5wMK/2Ly71L6737u84Ly9w8XC1MDD3L++z8PExcfE3sHAxsnGyMrH08fH4cTDz8rI"
             "yczIzM7L5MfG0s3M2MzMzdDMztHO58rJ0NLP0dPQ487K19LQ0tTR3dHR09bS1dfU6NLO4NTU29bU"
             "1tnV6NTX3djW2NrX69XR7dbT2t3a493c3d/c8NrW3uDd6d3d3+He4OPg7ODg4uXh4+bi8OPj5efk"
             "5+nm8+bm6Orn6evo6u3q9+rr7O/r7vDt9O/u8PLv8fTw+PPx9Pbz+vXz9fj0/Pf29vn2/vn4+fv4"
             "+/36//z6/P/7////RZ/1PwAAAAF0Uk5TAEDm2GYAAAABYktHRACIBR1IAAAACXBIWXMAAA9hAAAP"
             "YQGoP6dpAAAAB3RJTUUH5wQaDzUmdtGQ1wAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lN"
             "UFeBDhcAABB3SURBVHja7Z17cBXVHcezExLShIcgSIlAARUBxUrrk6JRo7RAAy1CWwRT8QUMptSK"
             "RWwVDdWe0vYmSi6dVCYoJgRT3HZCeShhIp1QMiYYEzRthhSWTG7MJRlv4BLicONOz9m9z3Dv7jnZ"
             "H2d3b/j+m5vd/X727OM8vvtLSNCWbF8lGJdsdw1s98YYhG1CsK/6jSAu3IcY9Nu+EB9iRBBv9lkR"
             "xKF9FQGbfyH+JA9s+3QI4tu/PoF4969HIO7tEw10/1oEePtPnZG5aMWanJw12QszpqWZT4Cv/2EZ"
             "a1CE1s8ZZS4Brv4nZaMoWjPVRAI8bwCjVgQsO8XK6rrGpqbGuuqKMidCK9PNBcDHf8ZrqvvCikYp"
             "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"
             "RVekrf8DQpnvQNJVVVIAAAAASUVORK5CYII=")

    def __init__(self):
        self.png = base64.b64decode(self._data)
        self.pixmap = QtGui.QPixmap()
        self.pixmap.loadFromData(self.png, format='png')
        self.icon = QIcon(self.pixmap)
