#===============================================================================
# Attention Disclaimer!
# This piece of Python source code is for test purpose only.
# It is kindly given to you by Animal Logic Pty Ltd
# Animal Logic will not endorse any responsibility for the use and/or misuse of this software.
# Prerequisites:
# XSI version 5.11 or 6
# WinXP 32 (win2k supported)
# Python (2.5 recommended, 2.4 supported): http://www.python.org/download/
# pywin32  210: https://sourceforge.net/projects/pywin32/
# wxPython (2.8.0.1 recommended, older supported): http://wxpython.org/download.php#binaries
#
#  Install:
# Copy this file in the Plugins directory of your XSI user location or workgroup.
#===============================================================================



import os
import wx
import wx.lib.scrolledpanel as scrolled
import traceback
import win32gui, win32process, pythoncom
from win32com.client import constants as c

xsi = Application
log = xsi.LogMessage

def XSILoadPlugin(in_reg):
    in_reg.Author = "Aloys Baillet for Animal Logic"
    in_reg.Name = "wxPythonXSIExplorerPlugin"
    in_reg.Email = "aloys.baillet@gmail.com"
    in_reg.URL = "www.animallogic.com"
    in_reg.Major = 1
    in_reg.Minor = 0
    in_reg.Categories = 'AnimalLogic,Test'

    in_reg.RegisterCommand("wxPythonXSIExplorer", "wxPythonXSIExplorer")
    in_reg.RegisterMenu(c.siMenuTbGetPropertyID, "wxPythonXSIExplorer_Menu", False, False)
    in_reg.RegisterEvent("wxPythonXSIExplorerPlugin_OnSelectionChange", c.siOnSelectionChange)

    return True

def wxPythonXSIExplorerPlugin_OnSelectionChange_OnEvent(ctxt):
    XSIObjectExplorerPanel.onXSISelectionChangeEvent()

def wxPythonXSIExplorer_Init(in_ctxt):
    oCmd = in_ctxt.Source
    oCmd.Description = ""
    oCmd.ReturnValue = True
    return True

def wxPythonXSIExplorer_Execute():
    XSIExplorerFrame.create()
    return True

def wxPythonXSIExplorer_Menu_Init(in_ctxt):
    oMenu = in_ctxt.Source
    oMenu.AddCommandItem("Open a wxPython Explorer", "wxPythonXSIExplorer")
    return True

#----------------------------
# That's the interesting bit:
# This class can be put in a separate module and imported in the plugin for exemple.
class XSISubFrame(wx.Frame):
    """
    XSI wx SubFrame with special event bound to the close.
    Please inherit from this one to create a custom frame in XSI.
    In order to ensure a clean exit, please call the XSISubFrame.Close method to generate a EVT_CLOSE event.
    Don't forget to call the XSISubFrame.__init__ in your __init__!
    And don't forget to define the self.panel at the end of your .__init__!
    @author: Aloys Baillet
    """
    _topLevelXSIWindowHandle = None
    @classmethod
    def create(cls, *args, **kw):
        """
        Call this static class method to create a new instance of your subframe.
        This class method will use the XSI Top Level window as a parent for the current frame.
        """
        app = wx.GetApp()
        if app is None:
            app = wx.App(redirect=False)
        topHandle = XSISubFrame._getXSITopLevelWindow()
        top = wx.PreFrame()
        top.AssociateHandle(topHandle)
        top.PostCreate(top)
        app.SetTopWindow(top)
        try:
            frame = cls(top, app, *args, **kw)
            frame.Show(True)
        except:
            log('An error occured during the instanciation of the %s frame class: %s'%(cls.__name__, traceback.format_exc()), c.siError)
            frame = None
        top.DissociateHandle()
        return frame


    @staticmethod
    def _getXSITopLevelWindow():
        """
        Returns the handle to the XSI top-level window
        """
        if XSISubFrame._topLevelXSIWindowHandle is not None:
            return XSISubFrame._topLevelXSIWindowHandle
        def callback(handle, winList):
            winList.append(handle)
            return True
        wins = []
        win32gui.EnumWindows(callback, wins)
        currentId = os.getpid()
        for handle in wins:
            tid, pid = win32process.GetWindowThreadProcessId(handle)
            if pid == currentId:
                title = win32gui.GetWindowText(handle)
                if title.startswith('SOFTIMAGE'):
                    XSISubFrame._topLevelXSIWindowHandle = handle
                    return handle
        return None


    def __init__(self, parent, app, id, title,
                 pos=(150, 150), size=(350, 200),
                 style=wx.DEFAULT_FRAME_STYLE|wx.FRAME_FLOAT_ON_PARENT|wx.FRAME_NO_TASKBAR|wx.FULL_REPAINT_ON_RESIZE,
                 name='frame'):
        wx.Frame.__init__(self, parent, id, title, pos, size, style, name)
        self.app = app
        self.panel = None
        self._runningModal = False
        self.SetBackgroundStyle(wx.BG_STYLE_COLOUR)
        self.SetBackgroundColour(wx.Color(171, 168, 166))
        self.Bind(wx.EVT_CLOSE, self.OnClose)
        self.Bind(wx.EVT_ACTIVATE, self.OnActivate)
        icon = wx.Icon('xsiIcon', wx.BITMAP_TYPE_ICO, 16, 16)
        icon.LoadFile(xsi.InstallationPath(c.siFactoryPath)+'\\xsi.ico', wx.BITMAP_TYPE_ICO)
        self.SetIcon(icon)

    def OnActivate(self, evt):
        """Event handler for activation. Used to detect modality in XSI"""
        if self._runningModal:
            return
        self.panel.Enable(win32gui.IsWindowEnabled(XSISubFrame._topLevelXSIWindowHandle))
        evt.Skip()

    def OnClose(self, evt):
        """Event handler for EVT_CLOSE event."""
        self.Show(False)
        self.Destroy()
        # This is because the regular wx Destroy waits for the application to destroy the window
        # But there is no wxApp running, so we do it ourselves
        win32gui.DestroyWindow(self.GetHandle())
        win32gui.SetFocus(XSISubFrame._topLevelXSIWindowHandle)

    def showModalDialog(self, dlg):
        """
        Method to display a modal dialog that disactivate XSI window.
        """
        self._runningModal = True
        top = XSISubFrame._topLevelXSIWindowHandle
        win32gui.EnableWindow(top, False)
        dlgReturn = dlg.ShowModal()
        win32gui.EnableWindow(top, True)
        self._runningModal = False
        return dlgReturn

    def __del__(self):
        pass
# End of the interesting bit
#-----------------

#-----------------
# Now that's an example of how to use the interesting bit!
class XSIExplorerFrame(XSISubFrame):
    """
    The Frame for the XSI Explorer.
    """
    def __init__(self, parent, app):
        XSISubFrame.__init__(self, parent, app, -1, 'XSI wxPython Explorer', size=(500, 300))
        self.panel = XSIObjectExplorerPanel(self)

    def OnClose(self, event):
        """
        This method is bound by XSISubFrame to the EVT_CLOSE event.
        It shows a modal dialog before closing.
        Notice the win32gui hack to disable the XSI top-level window during the modal time.
        """
        dlg = wx.MessageDialog(None, 'Are you sure?', 'Sure?')
        if self.showModalDialog(dlg) == wx.ID_OK:
            self.panel.onWindowClose()
            XSISubFrame.OnClose(self, event)


class XSITextCtrl(wx.TextCtrl):
    """
    Custom Text control for XSI, the regular wx.TextCtrl has been "hacked" by XSI
    So we need to use the wx.TE_RICH2 control to have a correct behavior
    """
    def __init__(self, parent, id, value='', pos=(-1, -1), size=(-1, -1), style=0, validator=wx.DefaultValidator, name=''):
        style += wx.TE_RICH2
        wx.TextCtrl.__init__(self, parent, id, value, pos, size, style, validator, name)
        self.SetBackgroundStyle(wx.BG_STYLE_COLOUR)
        self.SetBackgroundColour(wx.Color(54, 51, 51))
        self.SetForegroundColour(wx.Color(255, 255, 255))



class XSIObjectInfoPanel(scrolled.ScrolledPanel):
    """
    XSI Object Info Panel: list all editable Parameters
    """
    def __init__(self, parent):
        scrolled.ScrolledPanel.__init__(self, parent, -1, style = wx.TAB_TRAVERSAL|wx.SUNKEN_BORDER)
        sizer = wx.BoxSizer(wx.VERTICAL)
        text = wx.StaticText(self, -1, 'No object selected...')
        sizer.Add(text, 0, wx.ALL)
        self.SetSizer(sizer)
        self.Layout()
        self.SetAutoLayout(True)
        self.SetupScrolling()
        self._params = {}


    def updateFromObject(self, obj):
        rowSize = 26
        self.DestroyChildren()
        self.Show(False)
        self._params = {}
        sizer = wx.FlexGridSizer(cols=2, vgap=4, hgap=4)
        text = wx.StaticText(self, -1, 'Parameters of "%s":'%obj.FullName)
        sizer.Add(text)
        sizer.AddSpacer(rowSize)

        for param in obj.Parameters:
            if param.ValueType not in (c.siBool, c.siInt4, c.siFloat, c.siString, c.siWStr, c.siDouble):
                continue
            name = param.FullName
            self._params[name] = param
            try:
                val = param.Value
            except pythoncom.com_error:
                continue
            label = wx.StaticText(self, -1, param.ScriptName + ':', size=(-1, rowSize))
            sizer.Add(label)
            if param.ValueType in (c.siInt4, c.siFloat, c.siString, c.siDouble, c.siWStr):
                widget = XSITextCtrl(self, -1, str(param.Value), size=(-1, 20))
                widget.Bind(wx.EVT_TEXT_ENTER, self.OnValueChange)
                widget.Bind(wx.EVT_KILL_FOCUS, self.OnValueChange)
            elif param.ValueType == c.siBool:
                widget = wx.CheckBox(self, -1, '', size=(-1, rowSize))
                widget.SetValue(param.Value)
                widget.Bind(wx.EVT_CHECKBOX, self.OnValueChange)
            widget.SetName(name)
            sizer.Add(widget)

        self.SetSizer(sizer)
        self.Layout()
        self.Show(True)
        self.SetupScrolling()

    def OnValueChange(self, event):
        widget = event.GetEventObject()
        param = self._params[widget.GetName()]
        if param.ValueType == c.siInt4:
            try:
                param.Value = int(widget.GetValue())
            except:
                widget.SetValue(param.Value)
        elif param.ValueType in (c.siFloat, c.siDouble):
            try:
                param.Value = float(widget.GetValue())
            except:
                widget.SetValue(param.Value)
        elif param.ValueType == c.siBool:
            param.Value = widget.GetValue()
        elif param.ValueType in (c.siString, c.siWStr):
            param.Value = widget.GetValue()



class XSIObjectExplorerPanel(wx.Panel):
    """
    Test Object Explorer Panel
    """
    _runningInstances = []
    def __init__(self, parent):
        wx.Panel.__init__(self, parent, -1)
        self.frame = parent

        text = wx.StaticText(self, -1, "Test of a custom Selection Explorer + Property Panel")
        text.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD))
        text.SetSize(text.GetBestSize())
        btn = wx.Button(self, -1, "Close")

        self.Bind(wx.EVT_BUTTON, self.OnClose, btn)
        self.Bind(wx.EVT_CLOSE, self.OnClose)

        tID = wx.NewId()

        self.tree = wx.TreeCtrl(self, -1, wx.DefaultPosition, wx.DefaultSize,
                               wx.TR_HAS_BUTTONS)
        self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelChanged, self.tree)
        self.updateTreeFromSelection()

        self.infoPanel = XSIObjectInfoPanel(self)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(text, 0, wx.ALL)
        treeAndPageSizer = wx.BoxSizer(wx.HORIZONTAL)
        treeAndPageSizer.Add(self.tree, 1, wx.ALL|wx.EXPAND)
        treeAndPageSizer.Add(self.infoPanel, 1, wx.ALL|wx.EXPAND)
        sizer.Add(treeAndPageSizer, 1, wx.ALL|wx.EXPAND)
        sizer.Add(btn, 0, wx.ALL)
        self.SetSizer(sizer)
        self.Layout()
        self.SetSize(sizer.GetSize())
        self.SetAutoLayout(True)
        self.frozen = False
        XSIObjectExplorerPanel._runningInstances.append(self)

    @staticmethod
    def onXSISelectionChangeEvent():
        for inst in XSIObjectExplorerPanel._runningInstances:
            inst.updateTreeFromSelection()

    def onWindowClose(self):
        XSIObjectExplorerPanel._runningInstances.remove(self)

    def updateTreeFromSelection(self):
        self.tree.DeleteAllItems()
        self.root = self.tree.AddRoot("Selection")
        objs = {}
        for obj in xsi.Selection:
            if obj.IsClassOf(c.siX3DObjectID):
                objs[obj.FullName] = obj
            else:
                obj = obj.Parent3DObject
                objs[obj.FullName] = obj
        for objFullName, obj in objs.iteritems():
            child = self.tree.AppendItem(self.root, objFullName)
            self.tree.SetPyData(child, obj)
            primItem = self.tree.AppendItem(child, obj.ActivePrimitive.Type)
            self.tree.SetPyData(primItem, obj.ActivePrimitive)
            for prop in obj.Properties:
                propItem = self.tree.AppendItem(child, prop.FullName)
                self.tree.SetPyData(propItem, prop)
                if prop.IsClassOf(c.siKinematicsID):
                    localItem = self.tree.AppendItem(propItem, 'local')
                    self.tree.SetPyData(localItem, prop.Local)
                    globalItem = self.tree.AppendItem(propItem, 'global')
                    self.tree.SetPyData(globalItem, prop.Global)
        self.tree.Expand(self.root)


    def OnClose(self, evt):
        """Event handler for the button click."""
        self.frame.Close()


    def OnSelChanged(self, event):
        # Freeze the UI so the refresh is faster and without flicker
        self.Freeze()
        wx.BeginBusyCursor()
        self.item = event.GetItem()
        if self.item:
            try:
                obj = self.tree.GetPyData(self.item)
                if obj is not None:
                    self.infoPanel.updateFromObject(obj)
            except:
                log('Error: %s'%traceback.format_exc())
        wx.EndBusyCursor()
        self.Thaw()


