# Copyright (c) 2003 by Intevation GmbH
# Authors:
# Jonathan Coles <jonathan@intevation.de>
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with Thuban for details.

"""Classes for creating dockable windows.

The DockFrame is the main window that will be the parent for the
dockable windows.

The DockPanel is a panel that all windows that wish to be dockable
should derive from.
"""

__version__ = "$Revision: 2877 $"

import resource

from Thuban import _

import wx

from Thuban.Lib.connector import Publisher

from dialogs import NonModalDialog

from messages import DOCKABLE_DOCKED, DOCKABLE_UNDOCKED, DOCKABLE_CLOSED

# Commented ID's because ID's are the same as the ID's of the legend buttons ...
#ID_BUTTON_DOCK = 4001
#ID_BUTTON_CLOSE = 4002
ID_BUTTON_DOCK = wx.ID_ANY
ID_BUTTON_CLOSE = wx.ID_ANY

PANEL_ID = 3141

DOCK_BMP   = "dock_12"
UNDOCK_BMP = "undock_12"
CLOSE_BMP  = "close_12"

class DockPanel(wx.Panel):
    """A DockPanel is a panel that should be derived from to create panels
    that can be docked in a DockFrame.
    """

    def __init__(self, parent, id):
        """parent should be a DockableWindow created from 
        DockFrame.CreateDock().
        """

        if not isinstance(parent, DockableWindow):
            raise TypeError("")

        wx.Panel.__init__(self, parent.GetCurrentParent(), id)

        self.parent = parent

        self.SetDockParent(parent)
        #parent.SetPanel(self)

    def Create(self):
        self.parent.SetPanel(self)

    def SetDockParent(self, parent):
        self.__dockParent = parent

    def GetDockParent(self):
        return self.__dockParent

    def SetDock(self, dock):
        """Specifically set the docked state of the panel 
        to dock (True or False).
        """
        #if dock == self.IsDocked(): return

        if dock:
            self.Dock()
        else:
            self.UnDock()

    def Dock(self):
        """Dock the panel in the DockFrame."""
        self.GetDockParent().Dock()

    def UnDock(self):
        """Undock the panel in the DockFrame."""
        self.GetDockParent().UnDock()

    def IsDocked(self):
        """Return True if the panel is docked."""
        return self.GetDockParent().IsDocked()

class DockableWindow(Publisher):

    def __getattr__(self, attr):
        return getattr(self.__topWindow, attr)    

    def __init__(self, parent, id, name, title, dockWindow, orient):
        """Create the dockable window.

        Initially, the window is hidden, but in an undocked state.
        """

        if not isinstance(parent, DockFrame): raise TypeError("")

        self.__parent = parent
        self.__id     = id
        self.__name   = name

        self.__orientation = orient

        self.__dockWindow  = dockWindow
        self.__floatWindow = wx.Frame(parent, id, title)

        self.__docked      = False
        if self.__docked:
            self.__topWindow = self.__dockWindow
        else:
            self.__topWindow = self.__floatWindow

        
        self.__floatSize     = None
        self.__floatPosition = None

        self.__dockPanel  = None
        self.__panel = None

        self.__dockWindow.Hide()
        self.__floatWindow.Hide()

        self.Bind(wx.EVT_CLOSE, self._OnClose)

    ##
    # Public methods
    #

    def GetName(self):
        return self.__name

    def SetPanel(self, panel):

        if not isinstance(panel, DockPanel):
            raise TypeError("")

        self.__panel = panel
        self.__panel.SetDockParent(self)
        self.__CreateBorder()

        self.SetDock(self.__docked)

    def GetPanel(self):
        return self.__panel

    def GetCurrentParent(self):
        return self.__topWindow

    def SetDock(self, dock):

        self.__CheckAllGood()

        if dock:
            self.Dock()
        else:
            self.UnDock()

    def Dock(self):
        """Dock the window."""
        self.__CheckAllGood()

        wasVisible = self.IsShown()

        if wasVisible: self.Show(False)

        self.__docked = True

        #
        # save window information
        #
        self.__floatSize = self.GetSize()
        self.__floatPosition = self.GetPosition()

        #
        # reparent
        #
        self.__topWindow = self.__dockWindow
        self.__dockPanel.Reparent(self.__topWindow)

        self.__dockButton.SetBitmapLabel(self.__bmpUnDock)
        self.__dockButton.SetBitmapFocus(self.__bmpUnDock)
        self.__dockButton.SetBitmapSelected(self.__bmpUnDock)
        self.__dockButton.SetBitmapDisabled(self.__bmpUnDock)
        self.__dockButton.SetToolTip(wx.ToolTip(_("Undock")))

        self.SetDockSize(self.__dockWindow.GetSize())

        if wasVisible: self.Show(True)
        
        # rebind the events to the correct window
        # FIXME : should we remove the previous bindings ?
        self.__topWindow.Bind(wx.EVT_BUTTON, self._OnToggleDock, self.__dockButton)
        self.__topWindow.Bind(wx.EVT_BUTTON, self._OnButtonClose, self.__closeX) 

        self.issue(DOCKABLE_DOCKED, self.__id, self)

    def UnDock(self):
        """Undock the window."""
        self.__CheckAllGood()

        wasVisible = self.IsShown()

        if wasVisible: self.Show(False)

        self.__docked = False

        #
        # reparent
        #
        self.__topWindow = self.__floatWindow
        self.__dockPanel.Reparent(self.__topWindow)

        self.__dockButton.SetBitmapLabel(self.__bmpDock)
        self.__dockButton.SetBitmapFocus(self.__bmpDock)
        self.__dockButton.SetBitmapSelected(self.__bmpDock)
        self.__dockButton.SetBitmapDisabled(self.__bmpDock)
        self.__dockButton.SetToolTip(wx.ToolTip(_("Dock")))

        if wasVisible: self.Show()

        #
        # restore window information
        #
        if self.__floatPosition is not None:
            self.SetPosition(self.__floatPosition)
        if self.__floatSize is not None:
            self.SetSize(self.__floatSize)
            
        # rebind the events to the correct window
        # FIXME : should we remove the previous bindings ?
        self.__topWindow.Bind(wx.EVT_BUTTON, self._OnToggleDock, self.__dockButton)
        self.__topWindow.Bind(wx.EVT_BUTTON, self._OnButtonClose, self.__closeX) 

        self.__dockPanel.SetSize(self.__topWindow.GetClientSize())

        self.issue(DOCKABLE_UNDOCKED, self.__id, self)

    def IsDocked(self):
        self.__CheckAllGood()
        return self.__docked

    def Show(self, show = True):
        """Show or hide the window."""
        if show:
            self.__DoShow()
        else:
            self.__DoHide()

    def SetDockSize(self, rect = None):
        """Set the size of the dock window to rect."""

        dockSize = self.__dockPanel.GetBestSize()
        panelSize = self.__panel.GetBestSize()
        
        panelSize.IncTo(dockSize)
        w = panelSize.GetWidth()
        h = panelSize.GetHeight()
        
        if rect is not None:
            rw = rect.width
            rh = rect.height
            if rw < w: rw = w
            if rh < h: rh = h
        else:
            rw = w
            rh = h

        # these are to account for the border?!!?
        rw += 8 # XXX: without this the sash isn't visible!?!?!?!
        rh += 8 # XXX: without this the sash isn't visible!?!?!?!

        self.__dockWindow.SetDefaultSize(wx.Size(rw, rh))


    def Destroy(self):
        self.__panel.Destroy()
        self.__floatWindow.Destroy()
        self.__dockWindow.Destroy()
        self.__parent.OnDockDestroy(self)

    ##
    # Event handlers
    #

    def _OnButtonClose(self, event):
        self.Close()
        self.Show(False)

    def _OnClose(self, force = False):
        self.Show(False)

    def _OnToggleDock(self, event):
        self.__CheckAllGood()

        self.SetDock(not self.IsDocked())

    ##
    # Private methods
    #

    def __CheckAllGood(self):
        if self.__panel is None:
            raise TypeError("")

    def __DoShow(self):
        if self.IsShown(): return

        self.__topWindow.Show()

        if self.__topWindow is self.__dockWindow:
            self.__parent._UpdateDocks()

    def __DoHide(self):
        if not self.IsShown(): return

        self.__topWindow.Show(False)

        if self.__topWindow is self.__dockWindow:
            self.__parent._UpdateDocks()

    def __CreateBorder(self):

        self.__dockPanel = wx.Panel(self.__topWindow, -1, style=wx.SUNKEN_BORDER)

        self.__panel.Reparent(self.__dockPanel)
        self.__panel.SetId(PANEL_ID)

        if self.__orientation == wx.LAYOUT_VERTICAL:
            sizer = wx.BoxSizer(wx.VERTICAL)
            headerBoxOrient = wx.HORIZONTAL
        else:
            sizer = wx.BoxSizer(wx.HORIZONTAL)
            headerBoxOrient = wx.VERTICAL


        headerBox = wx.StaticBoxSizer(
                        wx.StaticBox(self.__dockPanel, -1, ""), headerBoxOrient)

        #
        # ideally, we should be able to rotate this text depending on
        # our orientation
        #
        text = wx.StaticText(self.__dockPanel, -1, self.GetTitle(),
                             style = wx.ALIGN_CENTRE)
        font = text.GetFont()
        font.SetPointSize(10)
        text.SetFont(font)

        #
        # load the dock/undock/close bitmaps
        # and create the buttons
        #
        self.__bmpDock   = \
            resource.GetBitmapResource(DOCK_BMP, wx.BITMAP_TYPE_XPM)
        self.__bmpUnDock = \
            resource.GetBitmapResource(UNDOCK_BMP, wx.BITMAP_TYPE_XPM)

        if self.__docked:
            bmp = self.__bmpDock
        else:
            bmp = self.__bmpUnDock

        self.__dockButton = wx.BitmapButton(
            self.__dockPanel, ID_BUTTON_DOCK,
            bmp,
            size = wx.Size(bmp.GetWidth() + 4, bmp.GetHeight() + 4),
            style = wx.BU_EXACTFIT | wx.ADJUST_MINSIZE)

        bmp = resource.GetBitmapResource(CLOSE_BMP, wx.BITMAP_TYPE_XPM)

        self.__closeX = wx.BitmapButton(self.__dockPanel, ID_BUTTON_CLOSE, bmp,
                          size = wx.Size(bmp.GetWidth() + 4,
                                        bmp.GetHeight() + 4),
                          style = wx.BU_EXACTFIT | wx.ADJUST_MINSIZE)
        self.__closeX.SetToolTip(wx.ToolTip(_("Close")))

        #
        # fill in the sizer in an order appropriate to the orientation
        #
        if self.__orientation == wx.LAYOUT_VERTICAL:
            headerBox.Add(text, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTRE_VERTICAL, 0)
            headerBox.Add((1, 5), 1, wx.GROW)
            headerBox.Add(self.__dockButton, 0, wx.ALIGN_RIGHT, 0)
            headerBox.Add(self.__closeX, 0, wx.ALIGN_RIGHT | wx.LEFT, 4)
        else:
            headerBox.Add(self.__closeX, 0, wx.ALIGN_RIGHT | wx.BOTTOM, 4)
            headerBox.Add(self.__dockButton, 0, wx.ALIGN_RIGHT, 0)
            headerBox.Add(text, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTRE_VERTICAL, 0)

        sizer.Add(headerBox, 0, wx.GROW, 0)
        sizer.Add(self.__panel, 1, wx.GROW, 0)


        self.__dockPanel.SetAutoLayout(True)
        self.__dockPanel.SetSizer(sizer)
        sizer.SetSizeHints(self.__dockPanel)
        sizer.SetSizeHints(self.__floatWindow)

        self.__topWindow.Bind(wx.EVT_BUTTON, self._OnToggleDock, self.__dockButton)
        self.__topWindow.Bind(wx.EVT_BUTTON, self._OnButtonClose, self.__closeX)


class DockFrame(wx.Frame):
    """A DockFrame is a frame that will contain dockable panels."""

    def __init__(self, parent, id, title, position, size):
        wx.Frame.__init__(self, parent, id, title, position, size)

        self.openWindows = {}

        self.__update_lock = 0

        self.SetMainWindow(None)


        self.Bind(wx.EVT_SIZE, self._OnSashSize)
        self.Bind(wx.EVT_CLOSE, self.OnClose)

    layout2oppSash = {
            wx.LAYOUT_NONE   : wx.SASH_NONE,
            wx.LAYOUT_TOP    : wx.SASH_BOTTOM,
            wx.LAYOUT_LEFT   : wx.SASH_RIGHT,
            wx.LAYOUT_RIGHT  : wx.SASH_LEFT,
            wx.LAYOUT_BOTTOM : wx.SASH_TOP }


    def OnClose(self, event):

        self.__update_lock += 1

        #
        # child windows are not notified when the parent is destroyed
        # as of v2.4.0.3 so we need to interate over our children
        # and tell them to go away.
        #
        for key in self.openWindows.keys():
            win = self.openWindows[key]
            win.Destroy()

        self.__update_lock -= 1

        # should really call _UpdateDocks() here but we don't need to
        # since we're going away

    def CreateDock(self, name, id, title, align):
        """Create a new dock. align specifies where the dock will
        be placed. It can be one of wxLAYOUT_NONE, wxLAYOUT_LEFT, 
        wxLAYOUT_RIGHT.
        """

        if align in (wx.LAYOUT_NONE, wx.LAYOUT_LEFT, wx.LAYOUT_RIGHT):
            orient = wx.LAYOUT_VERTICAL
        else:
            orient = wx.LAYOUT_HORIZONTAL

        sash = wx.SashLayoutWindow(self, id, style=wx.NO_BORDER|wx.SW_3D)
        sash.SetOrientation(orient)
        sash.SetAlignment(align)
        sash.SetSashVisible(DockFrame.layout2oppSash[align], True)
        
        win = DockableWindow(self, id, name, title, sash, orient)

        self.__RegisterDock(name, win)
        self.Bind(wx.EVT_SASH_DRAGGED, self._OnSashDragged, id=id)

        return win

    def FindRegisteredDock(self, name):
        """Return a reference to a dock that has name."""
        return self.openWindows.get(name)

    def OnDockDestroy(self, win):
        del self.openWindows[win.GetName()]
        self._UpdateDocks()

    def SetMainWindow(self, main):
        self.__mainWindow = main
        self._UpdateDocks()

    def _UpdateDocks(self):
        if self.__update_lock == 0:
            wx.LayoutAlgorithm().LayoutWindow(self, self.__mainWindow)

    def _OnSashDragged(self, event):
        if event.GetDragStatus() == wx.SASH_STATUS_OUT_OF_RANGE:
            return

        id = event.GetId()
        sash = self.FindWindowById(id)
        #assert(isinstance(win, wxPanel))
        dockPanel = sash.GetChildren()[0]
        panel = dockPanel.FindWindowById(PANEL_ID)
        assert isinstance(panel, DockPanel)
        win = panel.GetDockParent()
        assert isinstance(win, DockableWindow)

        assert win.IsDocked()

        rect = event.GetDragRect()

        win.SetDockSize(rect)

        self._UpdateDocks()

    def _OnSashSize(self, event):
        self._UpdateDocks()

    def __RegisterDock(self, name, win):
        if self.FindRegisteredDock(name) is not None:
            raise ValueError(
                "A dockable window is already registered under the name '%s'" % name)

        self.openWindows[name] = win

