﻿#
# This file is part of Dragonfly.
# (c) Copyright 2007, 2008 by Christo Butcher
# Licensed under the LGPL.
#
#   Dragonfly is free software: you can redistribute it and/or modify it 
#   under the terms of the GNU Lesser General Public License as published 
#   by the Free Software Foundation, either version 3 of the License, or 
#   (at your option) any later version.
#
#   Dragonfly 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 
#   Lesser General Public License for more details.
#
#   You should have received a copy of the GNU Lesser General Public 
#   License along with Dragonfly.  If not, see 
#   <http://www.gnu.org/licenses/>.
#

"""
This file implements a Win32 dialog base class.
"""


#---------------------------------------------------------------------------

import sys
import ctypes
import struct
import winxpgui as win32gui
import win32api
import win32con
import os
from ctypes.wintypes import POINT


#---------------------------------------------------------------------------

class MINMAXINFO(ctypes.Structure):
    _fields_ = [
                ("ptReserved",      POINT),
                ("ptMaxSize",       POINT),
                ("ptMaxPosition",   POINT),
                ("ptMinTrackSize",  POINT),
                ("ptMaxTrackSize",  POINT),
               ]


#---------------------------------------------------------------------------

class DialogBase(object):

    _dialog_registered_windowclass = False
    _dialog_atom = None

    @classmethod
    def _dialog_register_windowclass(cls, instance, name):
        if not cls._dialog_registered_windowclass:
            wc = win32gui.WNDCLASS()
            wc.SetDialogProc()
            wc.hInstance = instance
            wc.lpszClassName = name
            wc.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW
            wc.hCursor = win32gui.LoadCursor( 0, win32con.IDC_ARROW )
            wc.hbrBackground = win32con.COLOR_WINDOW + 1
            wc.lpfnWndProc = {}

            # C code: wc.cbWndExtra = DLGWINDOWEXTRA + sizeof(HBRUSH) + (sizeof(COLORREF));
            wc.cbWndExtra = win32con.DLGWINDOWEXTRA + struct.calcsize("Pi")

            cls._dialog_atom = win32gui.RegisterClass(wc)
            cls._dialog_registered_windowclass = True

        return cls._dialog_atom


    #-----------------------------------------------------------------------

    def __init__(self, classname, title, initial_size, initial_position,
                 min_size, max_size, style, font, modal=False):

        self._dialog_classname = classname
        self._dialog_initial_size = initial_size
        self._dialog_initial_position = initial_position
        self._dialog_min_size = min_size
        self._dialog_max_size = max_size
        self._dialog_title = title
        self._dialog_font = font
        self._dialog_style = style
        self._dialog_modal = modal

        win32gui.InitCommonControls()
        self._hinst = win32gui.dllhandle
        self._dialog_controls = []

        self._dialog_register_windowclass(self._hinst, classname)

    def _dialog_create(self, template, message_map):
        if self._dialog_modal:  function = win32gui.DialogBoxIndirect
        else:                   function = win32gui.CreateDialogIndirect
        function(self._hinst, template, 0, message_map)

    def _dialog_build_template(self):
        # Initialize dialog template with header.
        dialog_template = [self._dialog_build_header()]

        # Create controls and append control entries to the dialog template.
        self._dialog_build_controls()
        w, h = self._dialog_initial_size
        for control in self._dialog_controls:
            dialog_template.append(control.template_entry(w, h))

        print "\n   ".join(str(value) for value in dialog_template)
        return dialog_template

    def _calculate_center(self, size):
        w, h = size
        desktop = win32gui.GetDesktopWindow()
        al, at, ar, ab = win32gui.GetWindowRect(desktop)
        cx = (ar-al)/2; cy = (ab-at)/2
        cx, cy = win32gui.ClientToScreen(desktop, (cx, cy))
        l = cx - w/2; t = cy - h/2
        return l, t


    #-----------------------------------------------------------------------
    # Property access to window info.

    hwnd = property(lambda self: self._hwnd)


    #-----------------------------------------------------------------------
    # Methods that control the creation and content of the window.

    def _dialog_build_header(self):
        """
            Method which creates window controls.

            This method should be overridden by derived window classes
            to create the desired contents.

            http://docs.activestate.com/activepython/2.5/pywin32/Dialog_Header_Tuple.html

        """

        return [
                self._dialog_title,
                tuple(self._dialog_initial_position)
                 + tuple(self._dialog_initial_size),
                self._dialog_style,
                None,
                self._dialog_font,
                None,
                self._dialog_classname,
               ]

    def _dialog_build_controls(self):
        """
            Method which creates window controls.

            This method should be overridden by derived window classes
            to create the desired contents.

        """

        pass

    def add_control(self, control):
        """
            Register the given *control* as a child of this window.

            This method is generally called by the constructor of
            Control instances.  It is therefore usually not necessary for
            users to call it directly.

        """

        self._dialog_controls.append(control)

    def _dialog_build_message_map(self):
        # Collect all controls which expect callbacks.
        map = {}
        for control in self._dialog_controls:
            for message, callback in control.message_callbacks.items():
                map.setdefault(message, {})[control.id] = callback

        # Create dispatchers for each type of message.
        for message, control_callbacks in map.items():
            def dispatcher(hwnd, msg, wparam, lparam):
                id = win32api.LOWORD(wparam)
                if id in control_callbacks:
                    control_callbacks[id](hwnd, msg, wparam, lparam)
            map[message] = dispatcher

        # Add the top-level callbacks handled by the window itself.
        map.update({
                    win32con.WM_SIZE:           self.on_size,
                    win32con.WM_INITDIALOG:     self.on_init_dialog,
                    win32con.WM_GETMINMAXINFO:  self.on_getminmaxinfo,
                    win32con.WM_CLOSE:          self.on_close,
                    win32con.WM_DESTROY:        self.on_destroy,
                  })
        return map


    #-----------------------------------------------------------------------
    # Message handler methods.

    def _get_edge_sizes(self):
        wl, wt, wr, wb = win32gui.GetWindowRect(self._hwnd)
        cl, ct, cr, cb = win32gui.GetClientRect(self._hwnd)
        il, it = win32gui.ClientToScreen(self._hwnd, (cl, ct))
        ir, ib = win32gui.ClientToScreen(self._hwnd, (cr, cb))
        el = il - wl
        et = it - wt
        er = wr - ir
        eb = wb - ib
        return el, et, er, eb

    def tracer (function):
        if hasattr(function, 'im_func'): name = function.im_func.func_name
        else: name = function.func_name
        def decorated (*a, **k):
            print "call", name, "arguments:", a,k
            return function(*a, **k)
        return decorated

    @tracer
    def on_init_dialog(self, hwnd, msg, wparam, lparam):
        self._hwnd = hwnd

        l, t = self._dialog_initial_position
        el, et, er, eb = self._get_edge_sizes()
        w = self._dialog_initial_size[0] + el + er
        h = self._dialog_initial_size[1] + et + eb
        win32gui.MoveWindow(self._hwnd, l, t, w, h, 0)

        l, t, r, b = win32gui.GetWindowRect(self._hwnd)
        w = r - l - el - er
        h = b - t - et - eb
        self._do_size(w, h, 1)

        self._post_init()

    def _post_init(self):
        """
            Method for performing any processing required after the
            main window has been initialized.

            This method should be overridden by derived window classes
            if they require anything to be done after the main window
            has been created and positioned.  This method is called
            after a WM_INITDIALOG message was received.

        """

        pass

    def on_size(self, hwnd, msg, wparam, lparam):
        width  = win32api.LOWORD(lparam)
        height = win32api.HIWORD(lparam)
        self._do_size(width, height)
        return 1

    def _do_size(self, width, height, repaint=1):
        for control in self._dialog_controls:
            l, t, w, h = control.calculate_size(width, height)
            win32gui.MoveWindow(control.handle, l, t, w, h, repaint)

    def on_getminmaxinfo(self, hwnd, msg, wparam, lparam):
        info = ctypes.cast(lparam, ctypes.POINTER(MINMAXINFO)).contents
        el, et, er, eb = self._get_edge_sizes()
        if self._dialog_max_size:
            width  = self._dialog_max_size[0] + el + er
            height = self._dialog_max_size[1] + et + eb
            point  = POINT(width, height)
            info.ptMaxSize       = point
            info.ptMaxTrackSize  = point
        if self._dialog_min_size:
            width  = self._dialog_min_size[0] + el + er
            height = self._dialog_min_size[1] + et + eb
            point  = POINT(width, height)
            info.ptMinTrackSize  = point

    def close(self):
        if self._dialog_modal:  win32gui.EndDialog(self._hwnd, 0)
        else:                   win32gui.DestroyWindow(self._hwnd)

    @tracer
    def on_close(self, hwnd, msg, wparam, lparam):
            self.close()

    @tracer
    def on_destroy(self, hwnd, msg, wparam, lparam):
        if not self._dialog_modal:
            win32gui.PostQuitMessage(0)


#---------------------------------------------------------------------------

class BasicDialog(DialogBase):

    def __init__(self, title, size, min_size=None, max_size=None, modal=False):
        classname  = "dragonfly.BasicDialog"
        position   = self._calculate_center(size)
        font       = (8, "MS Sans Serif")
        style      = ( win32con.WS_THICKFRAME
                     | win32con.WS_POPUP
                     | win32con.WS_VISIBLE
                     | win32con.WS_CAPTION
                     | win32con.WS_SYSMENU
                     | win32con.DS_SETFONT
                     | win32con.WS_MINIMIZEBOX)

        DialogBase.__init__(
                            self,
                            classname=classname,
                            title=title,
                            initial_size=size,
                            initial_position=position,
                            min_size=min_size,
                            max_size=max_size,
                            style=style,
                            font=font,
                            modal=modal,
                           )

        template = self._dialog_build_template()
        message_map = self._dialog_build_message_map()
        self._dialog_create(template, message_map)

