EventGhost/EventGhost

View on GitHub
eg/Classes/Document.py

Summary

Maintainability
D
2 days
Test Coverage
# -*- coding: utf-8 -*-
#
# This file is part of EventGhost.
# Copyright © 2005-2020 EventGhost Project <http://www.eventghost.net/>
#
# EventGhost 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 2 of the License, or (at your option)
# any later version.
#
# EventGhost 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 EventGhost. If not, see <http://www.gnu.org/licenses/>.

import os
import wx
from tempfile import mkstemp
from threading import Lock
from types import ClassType
from xml.etree import cElementTree as ElementTree

# Local imports
import eg

class Document(object):
    def __init__(self):
        class ItemMixin:
            document = self
            root = None
        self.ItemMixin = ItemMixin
        itemNamespace = {}
        self.XMLTag2ClassDict = {}

        def MakeCls(name):
            baseCls = getattr(eg, name)
            cls = ClassType(name, (ItemMixin, baseCls), itemNamespace)
            self.XMLTag2ClassDict[cls.xmlTag.lower()] = cls
            return cls

        self.TreeLink = eg.TreeLink
#        self.TreeItem = MakeCls("TreeItem")
#        self.ContainerItem = MakeCls("ContainerItem")
        self.EventItem = MakeCls("EventItem")
        self.ActionItem = MakeCls("ActionItem")
        self.PluginItem = MakeCls("PluginItem")
        self.FolderItem = MakeCls("FolderItem")
        self.MacroItem = MakeCls("MacroItem")
        self.RootItem = MakeCls("RootItem")
        self.AutostartItem = MakeCls("AutostartItem")

        self.selection = None
        self.stockUndo = []
        self.stockRedo = []
        self.undoId = 0
        self.undoIdOnSave = 0
        self.undoState = (False, False, "", "")
        self.isDirty = False
        self.filePath = None
        self.root = None
        self.firstVisibleItem = None
        self.frame = None
        self.reentrantLock = Lock()
        self.expandedNodes = set()
        self.visibleLogItem = 0

    def AfterLoad(self):
        if (
            TreeStateData.guid == self.root.guid and
            TreeStateData.time == self.root.time
        ):
            self.SetExpandState(TreeStateData.expanded)
            self.selection = self.FindItemWithPath(TreeStateData.selection)
            self.firstVisibleItem = self.FindItemWithPath(
                TreeStateData.firstVisibleItem
            )
        else:
            self.selection = self.root
            self.firstVisibleItem = self.root

    @eg.LogIt
    def AppendUndoHandler(self, handler):
        stockUndo = self.stockUndo
        if len(stockUndo) >= 20:
            del stockUndo[0]
        stockUndo.append(handler)
        self.undoId += 1
        del self.stockRedo[:]
        self.SetIsDirty()
        self.SetUndoState((True, False, ": " + handler.name, ""))

    def AskFile(self, style):
        fileDialog = wx.FileDialog(
            self.frame,
            message="",
            wildcard="EventGhost Tree (*.egtree; *.xml)|*.egtree;*.xml",
            style=style
        )
        try:
            if fileDialog.ShowModal() == wx.ID_CANCEL:
                return None
            return fileDialog.GetPath()
        finally:
            fileDialog.Destroy()

    @eg.LogItWithReturn
    def CheckFileNeedsSave(self):
        """
        Checks if the file was changed and if necessary asks the user if he
        wants to save it. If the user affirms, calls Save/SaveAs also.

        returns: wx.ID_OK     if no save was needed
                 wx.ID_YES    if file was saved
                 wx.ID_NO     if file was not saved
                 wx.ID_CANCEL if user canceled possible save
        """
        if not self.isDirty:
            return wx.ID_OK
        dialog = SaveChangesDialog(self.frame)
        result = dialog.ShowModal()
        dialog.Destroy()
        if result == wx.ID_CANCEL:
            return wx.ID_CANCEL
        elif result == wx.ID_YES:
            return self.Save()
        else:
            return wx.ID_NO

    def IsDirty(self):
        return self.isDirty

    @eg.LogItWithReturn
    def Close(self):
        try:
            eg.config.hideOnStartup = self.frame is None
            eg.config.autoloadFilePath = self.filePath
            if self.frame is not None:
                frame = self.frame
                self.frame = None
                frame.Destroy()
            TreeStateData.guid = self.root.guid
            TreeStateData.time = self.root.time
            TreeStateData.expanded = self.GetExpandState()
            TreeStateData.selection = self.selection.GetPath()
            TreeStateData.firstVisibleItem = self.firstVisibleItem.GetPath()
        except:
            eg.PrintTraceback()

    @eg.AssertInMainThread
    def CmdAddAction(self, selection=None, action=None):
        if selection is None:
            selection = self.selection
        if not selection.DropTest(eg.ActionItem):
            self.DisplayError("cantAddAction")
            return
        if action is None:
            # let the user choose an action
            result = eg.AddActionDialog.GetModalResult(self.frame)
            # if user canceled the dialog, take a quick exit
            if result is None:
                return
            action = result[0]
        return eg.UndoHandler.NewAction(self).Do(selection, action)

    @eg.AssertInMainThread
    def CmdAddEvent(self):
        if not self.selection.DropTest(eg.EventItem):
            self.DisplayError("cantAddEvent")
            return
        result = eg.AddEventDialog.GetResult(self.frame)
        if result is None:
           return
        return eg.UndoHandler.NewEvent(self).Do(
            self.selection,
            label=result[0]
        )

    @eg.AssertInMainThread
    def CmdAddFolder(self):
        folderNode = eg.UndoHandler.NewFolder(self).Do(self.selection)
        wx.CallAfter(self.frame.treeCtrl.EditNodeLabel, folderNode)

    @eg.AssertInMainThread
    def CmdAddMacro(self):
        return eg.UndoHandler.NewMacro(self).Do(self.selection)

    @eg.AssertInMainThread
    def CmdAddPlugin(self):
        result = eg.AddPluginDialog.GetModalResult(self.frame)
        if result:
            try:
                eg.UndoHandler.NewPlugin(self).Do(result[0])
            except eg.Exceptions.PluginLoadError:
                pass

    @eg.AssertInMainThread
    def CmdConfigure(self, item=None, isFirstConfigure=False):
        if item is None:
            item = self.selection
        if not item.isConfigurable:
            self.DisplayError("cantConfigure")
        else:
            return eg.UndoHandler.Configure(self).Do(item, isFirstConfigure)

    @eg.AssertInMainThread
    def CmdCopy(self):
        self.selection.OnCmdCopy()

    @eg.AssertInMainThread
    def CmdCut(self):
        eg.UndoHandler.Cut(self).Do(self.selection)

    @eg.AssertInMainThread
    def CmdDelete(self):
        eg.UndoHandler.Clear(self).Do(self.selection)

    @eg.AssertInMainThread
    def CmdExecute(self):
        if not self.selection.isExecutable:
            self.DisplayError("cantExecute")
        else:
            self.ExecuteNode(self.selection).SetShouldEnd()

    @eg.AssertInMainThread
    def CmdPaste(self):
        eg.UndoHandler.Paste(self).Do(self.selection)

    @eg.AssertInMainThread
    def CmdPython(self):
        self.selection.OnCmdPython()

    @eg.AssertInMainThread
    def CmdRename(self):
        if not self.selection.isRenameable:
            self.DisplayError("cantRename")
        else:
            self.frame.treeCtrl.EditNodeLabel(self.selection)

    @eg.AssertInMainThread
    def CmdToggleEnable(self):
        if not self.selection.isDeactivatable:
            self.DisplayError("cantDisable")
        else:
            eg.UndoHandler.ToggleEnable(self).Do(self.selection)

    @eg.AssertInMainThread
    def DisplayError(self, ident):
        self.frame.DisplayError(getattr(eg.text.MainFrame.Messages, ident))

    def ExecuteNode(self, node):
        event = eg.EventGhostEvent("OnCmdExecute")
        eg.actionThread.Call(eg.actionThread.ExecuteTreeItem, node, event)
        return event

    def FindItemWithPath(self, path):
        item = self.root
        if path is None:
            return item
        try:
            for pos in path:
                item = item.childs[pos]
        except IndexError:
            item = self.root
        return item

    @eg.LogIt
    def GetExpandState(self):
        expanded = set()
        add = expanded.add
        ContainerItem = eg.ContainerItem

        def Traverse(item, i):
            if isinstance(item, ContainerItem):
                i += 1
                if item in self.expandedNodes:
                    add(i)
                for child in item.childs:
                    i = Traverse(child, i)
            return i
        Traverse(self.root, -1)

        return expanded

    def GetTitle(self):
        if self.filePath is None:
            filename = eg.text.General.unnamedFile
        elif not self.filePath:
            filename = "Example"
        else:
            filename = os.path.splitext(os.path.basename(self.filePath))[0]
        return "EventGhost %s - %s" % (eg.Version.string, filename)

    @eg.LogItWithReturn
    def HideFrame(self):
        # NOTICE:
        # If the program is started through a shortcut with "minimize" option
        # set, we get an iconize event while ShowFrame() is executing.
        # Therefore we have to use this CallLater workaround.
        # TODO: Find a better way. Preferable detect the minimize option
        #       before we create the MainFrame.
        if self.reentrantLock.acquire(False):
            if self.frame is not None:
                if len(self.frame.openDialogs) == 0:
                    logCtrl = self.frame.logCtrl
                    if logCtrl.IsAutoscroll():
                        self.visibleLogItem = 0
                    else:
                        self.visibleLogItem = (
                            logCtrl.GetTopItem() + logCtrl.GetCountPerPage()
                        )

                        if self.visibleLogItem:
                            self.visibleLogItem -= 1

                    self.frame.Destroy()
                    self.frame = eg.mainFrame = None
            self.reentrantLock.release()
        else:
            wx.CallLater(100, self.HideFrame)

    @eg.LogIt
    def Load(self, filePath):
        if filePath is None:
            return self.LoadEmpty()
        self.ResetUndoState()
#        if self.root:
#            self.root.Delete()
        if not filePath:
            filePath = os.path.join(eg.mainDir, "Example.egtree")
            self.SetFilePath(False)
        else:
            self.SetFilePath(filePath)
        eg.TreeLink.StartLoad()
        xmlTree = ElementTree.parse(filePath)
        node = xmlTree.getroot()
        root = self.RootItem(self, node)
        self.ItemMixin.root = root
        self.root = root
        self.selection = root
        eg.TreeLink.StopLoad()
        self.SetIsDirty(False)
        self.AfterLoad()
        wx.CallAfter(eg.Notify, "DocumentNewRoot", root)
        return root

    @eg.LogIt
    def LoadEmpty(self):
        self.ResetUndoState()
#        if self.root:
#            self.root.Delete()
        self.SetFilePath(None)
        eg.TreeLink.StartLoad()
        node = ElementTree.Element("EventGhost")
        root = self.RootItem(self, node)
        self.root = root
        self.selection = root
        self.ItemMixin.root = root
        node = ElementTree.Element("Autostart")
        self.autostartMacro = self.AutostartItem(root, node)
        root.childs.append(self.autostartMacro)
        eg.TreeLink.StopLoad()
        self.SetIsDirty(False)
        wx.CallAfter(eg.Notify, "DocumentNewRoot", root)
        return root

    def New(self):
        if self.CheckFileNeedsSave() == wx.ID_CANCEL:
            return
        self.StartSession(None)

    def OnCmdConfigure(self, node):
        eg.AsTasklet(eg.UndoHandler.Configure(self).Do)(node)

    def Open(self, filePath=None):
        self.ShowFrame()
        if filePath is not None:
            res = wx.MessageBox(
                "Do you really want to load the tree file:\n%s" % filePath,
                eg.APP_NAME,
                wx.YES_NO | wx.CENTRE | wx.ICON_QUESTION,
                parent = self.frame,
            )
            if res == wx.ID_NO:
                return wx.ID_CANCEL
        if self.CheckFileNeedsSave() == wx.ID_CANCEL:
            return wx.ID_CANCEL
        if filePath is None:
            filePath = self.AskFile(wx.OPEN)
        if filePath is None:
            return wx.ID_CANCEL
        self.StartSession(filePath)

    @eg.AssertInMainThread
    @eg.LogIt
    def Redo(self):
        if len(self.stockRedo) == 0:
            return
        handler = self.stockRedo.pop()
        eg.actionThread.Func(handler.Redo)()
        self.undoId += 1
        self.SetIsDirty(self.undoId != self.undoIdOnSave)
        self.stockUndo.append(handler)
        if len(self.stockRedo):
            redoName = ": " + self.stockRedo[-1].name
            hasRedo = True
        else:
            redoName = ""
            hasRedo = False
        self.SetUndoState((True, hasRedo, ": " + handler.name, redoName))

    def ResetUndoState(self):
        del self.stockUndo[:]
        del self.stockRedo[:]
        self.undoId = 0
        self.undoIdOnSave = 0
        self.SetUndoState((False, False, "", ""))

    @eg.AssertInActionThread
    def RestoreItem(self, treePosition, xmlData):
        eg.TreeLink.StartUndo()
        parent, pos = treePosition.GetParentAndPosition()
        node = ElementTree.fromstring(xmlData)
        cls = self.XMLTag2ClassDict[node.tag.lower()]
        item = cls(parent, node)
        parent.AddChild(item, pos)
        eg.TreeLink.StopUndo()
        item.RestoreState()
        return item

    @eg.LogItWithReturn
    def Save(self):
        if not self.filePath:
            return self.SaveAs()
        self.WriteFile(self.filePath)
        return wx.ID_YES

    def SaveAs(self):
        filePath = self.AskFile(style=wx.SAVE | wx.OVERWRITE_PROMPT)
        if filePath is None:
            return wx.ID_CANCEL
        self.WriteFile(filePath)
        self.SetFilePath(filePath)
        return wx.ID_YES

    @eg.LogIt
    def SetExpandState(self, expanded):
        if expanded is None:
            return
        self.expandedNodes.clear()
        ContainerItem = eg.ContainerItem

        def Traverse(item, i):
            if isinstance(item, ContainerItem):
                i += 1
                if i in expanded:
                    self.expandedNodes.add(item)
                for child in item.childs:
                    i = Traverse(child, i)
            return i
        Traverse(self.root, -1)

    def SetFilePath(self, filePath):
        self.filePath = filePath
        eg.Notify("DocumentFileChange", filePath)

    def SetIsDirty(self, flag=True):
        self.isDirty = flag
        eg.Notify("DocumentChange", flag)

    def SetUndoState(self, undoState):
        self.undoState = undoState
        eg.Notify("UndoChange", undoState)

    @eg.LogItWithReturn
    def ShowFrame(self):
        if self.reentrantLock.acquire(False):
            import threading

            event = threading.Event()

            def do():
                if self.frame is None:
                    self.frame = eg.mainFrame = eg.MainFrame(self)
                    self.frame.Show()
                else:
                    self.frame.Raise()
                event.set()

            if not threading.current_thread() == eg.mainThread:
                wx.CallAfter(do)
                event.wait()
            else:
                do()

            self.reentrantLock.release()

    def StartSession(self, filePath):
        eg.eventThread.Func(eg.eventThread.StopSession)()
        eg.eventThread.Call(eg.eventThread.StartSession, filePath)

    @eg.AssertInMainThread
    def Undo(self):
        if len(self.stockUndo) == 0:
            return
        handler = self.stockUndo.pop()
        eg.actionThread.Func(handler.Undo)()
        self.undoId -= 1
        self.SetIsDirty(self.undoId != self.undoIdOnSave)
        self.stockRedo.append(handler)
        if len(self.stockUndo):
            undoName = ": " + self.stockUndo[-1].name
            hasUndo = True
        else:
            undoName = ""
            hasUndo = False
        self.SetUndoState((hasUndo, True, undoName, ": " + handler.name))

    def WriteFile(self, filePath):
        success = False
        tmpFile, tmpPath = mkstemp(".xml", "$", os.path.dirname(filePath))
        os.close(tmpFile)
        try:
            tmpFile = file(tmpPath, "wb+")
            tmpFile.write('<?xml version="1.0" encoding="UTF-8" ?>\r\n')
            self.root.WriteXmlString(tmpFile.write)
            tmpFile.close()
            try:
                os.remove(filePath)
            except:
                pass
            os.rename(tmpPath, filePath)
            self.SetIsDirty(False)
            self.undoIdOnSave = self.undoId
            success = True
        except:
            eg.PrintTraceback("Error while saving file")
        return success


class SaveChangesDialog(wx.Dialog):
    def __init__(self, parent=None):
        text = eg.text.MainFrame.SaveChanges
        wx.Dialog.__init__(self, parent, title=eg.APP_NAME)
        bmp = wx.ArtProvider.GetBitmap(
            wx.ART_WARNING, wx.ART_CMN_DIALOG, (32, 32)
        )
        staticBitmap = wx.StaticBitmap(self, -1, bmp)

        messageCtrl = wx.StaticText(
            self, -1, eg.text.MainFrame.SaveChanges.mesg
        )
        messageCtrl.Wrap(400)
        saveButton = wx.Button(self, wx.ID_YES, text.saveButton)
        saveButton.Bind(wx.EVT_BUTTON, self.OnButton)
        saveButton.SetDefault()
        saveButton.SetFocus()
        dontSaveButton = wx.Button(self, wx.ID_NO, text.dontSaveButton)
        dontSaveButton.Bind(wx.EVT_BUTTON, self.OnButton)
        cancelButton = wx.Button(self, wx.ID_CANCEL, eg.text.General.cancel)
        cancelButton.Bind(wx.EVT_BUTTON, self.OnButton)
        self.SetDefaultItem(saveButton)

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(staticBitmap, 0, wx.ALL, 12)
        sizer.Add(messageCtrl, 0, wx.ALIGN_CENTER | wx.LEFT | wx.TOP | wx.RIGHT, 6)
        buttonSizer = wx.BoxSizer(wx.HORIZONTAL)
        buttonSizer.Add(saveButton, 0, wx.LEFT | wx.RIGHT, 3)
        buttonSizer.Add(dontSaveButton, 0, wx.LEFT | wx.RIGHT, 3)
        buttonSizer.Add(cancelButton, 0, wx.LEFT | wx.RIGHT, 3)
        mainSizer = wx.BoxSizer(wx.VERTICAL)
        mainSizer.Add(sizer)
        mainSizer.Add(buttonSizer, 0, wx.ALIGN_RIGHT | wx.ALL, 12)
        self.SetSizerAndFit(mainSizer)
        if parent:
            self.CenterOnParent()
        else:
            self.Center()

    def OnButton(self, event):
        buttonId = event.GetId()
        self.EndModal(buttonId)
        event.Skip()


class TreeStateData(eg.PersistentData):
    guid = None
    time = None
    expanded = None