# A core, data-driven dialog.
# Driven completely by "Control Processor" objects.

# This module is part of the spambayes project, which is Copyright 2003
# The Python Software Foundation and is covered by the Python Software
# Foundation license.

import win32gui, win32api, win32con
import commctrl
import struct, array

from dlgutils import *

# Isolate the nasty stuff for tooltips somewhere.
class TooltipManager:
    def __init__(self, dialog):
        self.dialog = dialog
        self.hwnd_tooltip = None
        self.tooltip_tools = {}

    def HideTooltip(self):
        if self.hwnd_tooltip is not None:
            win32gui.SendMessage(self.hwnd_tooltip, commctrl.TTM_TRACKACTIVATE, 0, 0)

    def ShowTooltipForControl(self, control_id, text):
        # Note sure this tooltip stuff is quite right!
        # Hide an existing one, so the new one gets created.
        # (new one empty is no big deal, but hiding the old one is, so
        # we get re-queried for the text.
        hwnd_dialog = self.dialog.hwnd
        self.HideTooltip()
        if self.hwnd_tooltip is None:
            TTS_BALLOON = 0x40
            self.hwnd_tooltip = win32gui.CreateWindowEx(0, "tooltips_class32", None,
                                    win32con.WS_POPUP | TTS_BALLOON,
                                    win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT,
                                    win32con.CW_USEDEFAULT, win32con.CW_USEDEFAULT,
                                    hwnd_dialog, 0, 0, None)
            # 80 chars max for our tooltip
            # hrm - how to measure this in pixels!
            win32gui.SendMessage(self.hwnd_tooltip,
                                 commctrl.TTM_SETMAXTIPWIDTH,
                                 0, 300)

        format = "iiiiiiiiiii"
        tt_size = struct.calcsize(format)
        buffer = array.array("c", text + "\0")
        text_address, size = buffer.buffer_info()
        uID = control_id
        flags = commctrl.TTF_TRACK | commctrl.TTF_ABSOLUTE
        data = struct.pack(format, tt_size, flags, hwnd_dialog, uID, 0,0,0,0, 0, text_address, 0)

        # Add a tool for this control only if we haven't already
        if control_id not in self.tooltip_tools:
            win32gui.SendMessage(self.hwnd_tooltip,
                                 commctrl.TTM_ADDTOOL,
                                 0, data)
            self.tooltip_tools[control_id] = 1

        control = win32gui.GetDlgItem(hwnd_dialog, control_id)
        child_rect = win32gui.GetWindowRect(control)
        xOff = yOff = 15 # just below and right of the control

        win32gui.SendMessage(self.hwnd_tooltip,
                             commctrl.TTM_TRACKPOSITION,
                             0,
                             MAKELONG(child_rect[0]+xOff, child_rect[1]+yOff))
        win32gui.SendMessage(self.hwnd_tooltip,
                             commctrl.TTM_TRACKACTIVATE,
                             1,data)

# A base dialog class, that loads from resources.  Has no real smarts.
class Dialog:
    def __init__(self, parent, parser, idd):
        win32gui.InitCommonControls()
        self.hinst = win32api.GetModuleHandle(None)
        self.parent = parent
        self.dialog_parser = parser
        self.template = parser.dialogs[idd]

    def _GetIDName(self, cid):
        return self.dialog_parser.names.get(cid, str(cid))

    def CreateWindow(self):
        return self._DoCreate(win32gui.CreateDialogIndirect)

    def DoModal(self):
        return self._DoCreate(win32gui.DialogBoxIndirect)

    def GetMessageMap(self):
        ret = {
            #win32con.WM_SIZE: self.OnSize,
            win32con.WM_COMMAND: self.OnCommand,
            win32con.WM_NOTIFY: self.OnNotify,
            win32con.WM_INITDIALOG: self.OnInitDialog,
            win32con.WM_CLOSE: self.OnClose,
            win32con.WM_DESTROY: self.OnDestroy,
            win32con.WM_RBUTTONUP: self.OnRButtonUp,
        }
        return ret

    def DoInitialPosition(self):
        # centre the dialog
        desktop = win32gui.GetDesktopWindow()
        l,t,r,b = win32gui.GetWindowRect(self.hwnd)
        w = r-l
        h = b-t
        dt_l, dt_t, dt_r, dt_b = win32gui.GetWindowRect(desktop)
        centre_x, centre_y = win32gui.ClientToScreen( desktop, ( (dt_r-dt_l)/2, (dt_b-dt_t)/2) )
        win32gui.MoveWindow(self.hwnd, centre_x-(w/2), centre_y-(h/2), w, h, 0)

    def OnInitDialog(self, hwnd, msg, wparam, lparam):
        self.hwnd = hwnd
        self.DoInitialPosition()

    def OnCommand(self, hwnd, msg, wparam, lparam):
        pass
    def OnNotify(self, hwnd, msg, wparam, lparam):
        pass
    def OnClose(self, hwnd, msg, wparam, lparam):
        pass
    def OnDestroy(self, hwnd, msg, wparam, lparam):
        pass
    def OnRButtonUp(self, hwnd, msg, wparam, lparam):
        pass

    def _DoCreate(self, fn):
        message_map = self.GetMessageMap()
        return win32gui.DialogBoxIndirect(self.hinst, self.template, self.parent, message_map)
    # A couple of helpers
    def GetDlgItem(self, id):
        if type(id)==type(''):
            id = self.dialog_parser.ids[id]
        return win32gui.GetDlgItem(self.hwnd, id)
    def SetDlgItemText(self, id, text):
        hchild = self.GetDlgItem(id)
        win32gui.SendMessage(hchild, win32con.WM_SETTEXT, 0, text)

# A dialog with a tooltip manager
class TooltipDialog(Dialog):
    def __init__(self, parent, parser, idd):
        Dialog.__init__(self, parent, parser, idd)
        self.tt = TooltipManager(self)

    def GetMessageMap(self):
        ret = Dialog.GetMessageMap(self)
        ret.update( {
            win32con.WM_HELP: self.OnHelp,
            win32con.WM_LBUTTONDOWN: self.OnLButtonDown,
            win32con.WM_ACTIVATE: self.OnActivate,
        })
        return ret

    def OnLButtonDown(self, hwnd, msg, wparam, lparam):
        self.tt.HideTooltip()

    def OnActivate(self, hwnd, msg, wparam, lparam):
        self.tt.HideTooltip()

    def OnDestroy(self, hwnd, msg, wparam, lparam):
        self.tt.HideTooltip()

    def OnHelp(self, hwnd, msg, wparam, lparam):
        format = "iiiiiii"
        buf = win32gui.PyMakeBuffer(struct.calcsize(format), lparam)
        cbSize, iContextType, iCtrlId, hItemHandle, dwContextID, x, y = \
                struct.unpack(format, buf)
        #print "OnHelp", cbSize, iContextType, iCtrlId, hItemHandle, dwContextID, x, y
        tt_text = self.GetPopupHelpText(iCtrlId)
        if tt_text:
            self.tt.ShowTooltipForControl(iCtrlId, tt_text)
        else:
            self.tt.HideTooltip()
        return 1

    def GetPopupHelpText(self, control_id):
        return None

# A "Processor Dialog" works with Command Processors, to link SpamBayes
# options with control IDS, giving a "data driven" dialog.
class ProcessorDialog(TooltipDialog):
    def __init__(self, parent, manager, config, idd, option_handlers):
        TooltipDialog.__init__(self, parent, manager.dialog_parser, idd)
        parser = manager.dialog_parser
        self.manager = manager
        self.config = config
        self.command_processors = {}
        self.processor_message_map = {} # WM_MESSAGE : [processors_who_want_it]
        self.all_processors = []
        for data in option_handlers:
            klass = data[0]
            id_names = data[1]
            rest = data[2:]
            ids = id_names.split()
            int_ids = [ parser.ids[id] for id in ids]
            instance = klass(self,int_ids, *rest)
            self.all_processors.append(instance)
            for int_id in int_ids:
                self.command_processors[int_id] = instance
            for message in instance.GetMessages():
                existing = self.processor_message_map.setdefault(message, [])
                existing.append(instance)

    def GetMessageMap(self):
        ret = TooltipDialog.GetMessageMap(self)
        for key in self.processor_message_map.keys():
            if key in ret:
                print "*** WARNING: Overwriting message!!!"
            ret[key] = self.OnCommandProcessorMessage
        return ret

    def OnInitDialog(self, hwnd, msg, wparam, lparam):
        TooltipDialog.OnInitDialog(self, hwnd, msg, wparam, lparam)
        if __debug__: # this is just a debugging aid
            for int_id in self.command_processors:
                try:
                    self.GetDlgItem(int_id)
                except win32gui.error:
                    print "ERROR: Dialog item %s refers to an invalid control" % \
                          self._GetIDName(int_id)
        self.LoadAllControls()

    def GetPopupHelpText(self, iCtrlId):
        cp = self.command_processors.get(iCtrlId)
        tt_text = None
        if cp is not None:
            return cp.GetPopupHelpText(iCtrlId)

        print "Can not get command processor for", self._GetIDName(iCtrlId)
        return None
    def OnRButtonUp(self, hwnd, msg, wparam, lparam):
        for cp in self.command_processors.values():
            cp.OnRButtonUp(wparam,lparam)

    def OnCommandProcessorMessage(self, hwnd, msg, wparam, lparam):
        for p in self.processor_message_map[msg]:
            p.OnMessage(msg, wparam, lparam)

    # Called back by a processor when it changes an option.  We tell all other
    # options on our page that the value changed.
    def OnOptionChanged(self, changed_by, option):
        for p in self.all_processors:
            if p is not changed_by:
                p.OnOptionChanged(option)


    def OnDestroy(self, hwnd, msg, wparam, lparam):
        #print "OnDestroy"
        for p in self.all_processors:
            p.Term()
        TooltipDialog.OnDestroy(self, hwnd, msg, wparam, lparam)
        self.command_processors = None
        self.all_processors = None
        self.processor_message_map = None

    def LoadAllControls(self):
        for p in self.all_processors:
            p.Init()

    def ApplyHandlingOptionValueError(self, func, *args):
        try:
            return func(*args)
        except ValueError, why:
            mb_flags = win32con.MB_ICONEXCLAMATION | win32con.MB_OK
            win32gui.MessageBox(self.hwnd, str(why), "SpamBayes", mb_flags)
            return False

    def SaveAllControls(self):
        for p in self.all_processors:
            if not self.ApplyHandlingOptionValueError(p.Done):
                win32gui.SetFocus(p.GetControl())
                return False
        return True

    def OnClose(self, hwnd, msg, wparam, lparam):
        if TooltipDialog.OnClose(self, hwnd, msg, wparam, lparam):
            return 1
        if not self.SaveAllControls():
            return 1
        win32gui.EndDialog(hwnd, 0)

    def OnNotify(self, hwnd, msg, wparam, lparam):
        #print "OnNotify", hwnd, msg, wparam, lparam
        # Parse the NMHDR
        TooltipDialog.OnNotify(self, hwnd, msg, wparam, lparam)
        format = "iii"
        buf = win32gui.PyMakeBuffer(struct.calcsize(format), lparam)
        hwndFrom, idFrom, code = struct.unpack(format, buf)
        code += 0x4f0000 # hrm - wtf - commctrl uses this, and it works with mfc.  *sigh*
        # delegate rest to our commands.
        handler = self.command_processors.get(idFrom)
        if handler is None:
            print "Ignoring OnNotify for", self._GetIDName(idFrom)
            return
        return handler.OnNotify( (hwndFrom, idFrom, code), wparam, lparam)

    def OnCommand(self, hwnd, msg, wparam, lparam):
        TooltipDialog.OnCommand(self, hwnd, msg, wparam, lparam)
        id = win32api.LOWORD(wparam)
        # Sometimes called after OnDestroy???
        if self.command_processors is None:
            print "Ignoring OnCommand for", self._GetIDName(id)
            return
        else:
            handler = self.command_processors.get(id)
            if handler is None:
                print "Ignoring OnCommand for", self._GetIDName(id)
                return

        self.ApplyHandlingOptionValueError(handler.OnCommand, wparam, lparam)

class ProcessorPage(ProcessorDialog):
    def __init__(self, parent, manager, config, idd, option_handlers, yoffset):
        ProcessorDialog.__init__(self, parent, manager, config, idd,option_handlers)
        self.yoffset = yoffset
    def DoInitialPosition(self):
        # The hardcoded values are a bit of a hack.
        win32gui.SetWindowPos(self.hwnd, win32con.HWND_TOP, 1, self.yoffset, 0, 0, win32con.SWP_NOSIZE)
    def CreateWindow(self):
        # modeless. Pages should have the WS_CHILD window style
        message_map = self.GetMessageMap()
        # remove frame from dialog and make sure it is a child
        self.template[0][2] = self.template[0][2] & ~(win32con.DS_MODALFRAME|win32con.WS_POPUP|win32con.WS_OVERLAPPED|win32con.WS_CAPTION)
        self.template[0][2] = self.template[0][2] | win32con.WS_CHILD
        return win32gui.CreateDialogIndirect(self.hinst, self.template, self.parent, message_map)
