# 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: 1.15 $"

import resource

from Thuban import _

from wxPython.wx import *

from Thuban.Lib.connector import Publisher

from dialogs import NonModalDialog

from messages import DOCKABLE_DOCKED, DOCKABLE_UNDOCKED, DOCKABLE_CLOSED

ID_BUTTON_DOCK = 4001
ID_BUTTON_CLOSE = 4002

PANEL_ID = 3141

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

class DockPanel(wxPanel):
    """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("")

        wxPanel.__init__(self, parent.GetCurrentParent(), id)

        self.parent = parent

        #self.SetDockParent(None)
        #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 = wxFrame(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()

        EVT_CLOSE(self, 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(wxToolTip(_("Undock")))

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

        if wasVisible: self.Show(True)

        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(wxToolTip(_("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)

        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."""

        w0, h0 = self.__dockPanel.GetBestSize()
        w, h = self.__panel.GetBestSize()

        if (w, h) < (w0, h0):
            w = w0
            h = h0

        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(wxSize(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 = wxPanel(self.__topWindow, -1, style=wxSUNKEN_BORDER)

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

        if self.__orientation == wxLAYOUT_VERTICAL:
            sizer = wxBoxSizer(wxVERTICAL)
            headerBoxOrient = wxHORIZONTAL
        else:
            sizer = wxBoxSizer(wxHORIZONTAL)
            headerBoxOrient = wxVERTICAL
        

        headerBox = wxStaticBoxSizer(
                        wxStaticBox(self.__dockPanel, -1, ""), headerBoxOrient)

        #
        # ideally, we should be able to rotate this text depending on
        # our orientation
        #
        text = wxStaticText(self.__dockPanel, -1, self.GetTitle(),
                             style = wxALIGN_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, wxBITMAP_TYPE_XPM)
        self.__bmpUnDock = \
            resource.GetBitmapResource(UNDOCK_BMP, wxBITMAP_TYPE_XPM)

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

        self.__dockButton = wxBitmapButton(
            self.__dockPanel, ID_BUTTON_DOCK, 
            bmp, 
            size = wxSize(bmp.GetWidth() + 4, bmp.GetHeight() + 4),
            style = wxBU_EXACTFIT | wxADJUST_MINSIZE)

        bmp = resource.GetBitmapResource(CLOSE_BMP, wxBITMAP_TYPE_XPM)

        closeX = wxBitmapButton(self.__dockPanel, ID_BUTTON_CLOSE, bmp, 
                          size = wxSize(bmp.GetWidth() + 4,
                                        bmp.GetHeight() + 4),
                          style = wxBU_EXACTFIT | wxADJUST_MINSIZE)
        closeX.SetToolTip(wxToolTip(_("Close")))

        #
        # fill in the sizer in an order appropriate to the orientation
        #
        if self.__orientation == wxLAYOUT_VERTICAL:
            headerBox.Add(text, 0, wxALIGN_LEFT | wxALIGN_CENTRE_VERTICAL, 0)
            headerBox.Add(1, 5, 1, wxGROW)
            headerBox.Add(self.__dockButton, 0, wxALIGN_RIGHT, 0)
            headerBox.Add(closeX, 0, wxALIGN_RIGHT | wxLEFT, 4)
        else:
            headerBox.Add(closeX, 0, wxALIGN_RIGHT | wxBOTTOM, 4)
            headerBox.Add(self.__dockButton, 0, wxALIGN_RIGHT, 0)
            headerBox.Add(text, 0, wxALIGN_LEFT | wxALIGN_CENTRE_VERTICAL, 0)

        sizer.Add(headerBox, 0, wxGROW, 0)
        sizer.Add(self.__panel, 1, wxGROW, 0)


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

        EVT_BUTTON(self.__dockPanel, ID_BUTTON_DOCK, self._OnToggleDock)
        EVT_BUTTON(self.__dockPanel, ID_BUTTON_CLOSE, self._OnButtonClose)


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

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

        self.openWindows = {}

        self.__update_lock = 0

        self.SetMainWindow(None)


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

    layout2oppSash = { 
            wxLAYOUT_NONE   : wxSASH_NONE,
            wxLAYOUT_TOP    : wxSASH_BOTTOM,
            wxLAYOUT_LEFT   : wxSASH_RIGHT,
            wxLAYOUT_RIGHT  : wxSASH_LEFT,
            wxLAYOUT_BOTTOM : wxSASH_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 (wxLAYOUT_NONE, wxLAYOUT_LEFT, wxLAYOUT_RIGHT):
            orient = wxLAYOUT_VERTICAL
        else:
            orient = wxLAYOUT_HORIZONTAL

        sash = wxSashLayoutWindow(self, id, style=wxNO_BORDER|wxSW_3D)
        sash.SetOrientation(orient)
        sash.SetAlignment(align)
        sash.SetSashVisible(DockFrame.layout2oppSash[align], True)
        sash.SetSashBorder(DockFrame.layout2oppSash[align], True)

        win = DockableWindow(self, id, name, title, sash, orient)

        self.__RegisterDock(name, win)
        EVT_SASH_DRAGGED(self, id, self._OnSashDragged)

        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:
            wxLayoutAlgorithm().LayoutWindow(self, self.__mainWindow)

    def _OnSashDragged(self, event):
        if event.GetDragStatus() == wxSASH_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

