# GNU Enterprise Forms - wx 2.6 UI Driver - Form widget
#
# Copyright 2001-2009 Free Software Foundation
#
# This file is part of GNU Enterprise
#
# GNU Enterprise is free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation; either
# version 3, or (at your option) any later version.
#
# GNU Enterprise 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public
# License along with program; see the file COPYING. If not,
# write to the Free Software Foundation, Inc., 59 Temple Place
# - Suite 330, Boston, MA 02111-1307, USA.
#
# $Id: form.py 10017 2009-10-28 14:59:53Z reinhard $
"""
Implementation of the UI layer for the <form> and <dialog> tag.
"""

import time
import os.path
import wx

from gnue.common.apps import GConfig
from gnue.forms import VERSION
from gnue.forms.uidrivers.wx26 import dialogs
from gnue.forms.uidrivers.wx26.widgets._base import UIHelper

__all__ = ['UIForm']

_MBOX_KIND = {'info'    : {'type'   : wx.ICON_INFORMATION,
                           'buttons': wx.OK,
                           'title'  : u_("Information")},
              'warning' : {'type'   : wx.ICON_EXCLAMATION,
                           'buttons': wx.OK,
                           'title'  : u_("Warning")},
              'question': {'type'   : wx.ICON_QUESTION,
                           'buttons': wx.YES_NO,
                           'title'  : u_("Question")},
              'error'   : {'type'   : wx.ICON_ERROR,
                           'buttons': wx.OK,
                           'title'  : u_("Error")}}

_RESPONSE = {wx.ID_OK    : True,
             wx.ID_YES   : True,
             wx.ID_NO    : False,
             wx.ID_CANCEL: None }

# =============================================================================
# Interface implementation of a form widget
# =============================================================================

class UIForm(UIHelper):
    """
    Interface implementation of form widgets (forms and dialogs)

    @ivar pages: list of page panels, one per <page> tag.  The page panel is a
        wx.Panel instance which is the parent of all wx widgets for that given
        page.
    @ivar sizing_enabled: if True, handling of wx.EVT_SIZE event of <grid> tags
        is activated.  In that case newly available space will be used to
        create/show new grid-rows, and less available space will cause
        grid-rows to be hidden.  On some platforms it is neccessary to disable
        the wx.EVT_SIZE event before closing the application in order to avoid
        core dumps.
    @ivar main_window: the wx.Frame or wx.Dialog instance representing this
        form.
    """

    _TAB_STYLE = {'left': wx.NB_LEFT,
                  'right': wx.NB_RIGHT,
                  'bottom': wx.NB_BOTTOM,
                  'top': wx.NB_TOP}

    # -------------------------------------------------------------------------
    # Constructor
    # -------------------------------------------------------------------------

    def __init__(self, event):

        UIHelper.__init__(self, event)

        self.pages = []
        self.sizing_enabled = False

        self._inits.extend([self.__update_sizer])

        self.main_window = None

        self._form = None
        self.__status_bar = None
        self.__master_sizer = None

        # Is this form embedded?
        self.__embedded = False

        # Set to True to block focus events
        self.block_focus_events = False


    # -------------------------------------------------------------------------
    # Create a new wx frame widget
    # -------------------------------------------------------------------------

    def _create_widget_(self, event, spacer):
        """
        Create the real wx.Frame or wx.Dialog.  The spacer is ignored for form
        tags.

        @param event: the creation-event instance carrying information like
            container (parent-widget)
        @param spacer: not used for forms and dialogs

        @returns: the wx.Frame or wx.Dialog instance for this form
        """

        if self._form.style != 'dialog':
            embed = self._uiDriver._parentContainer
            if embed is not None:
                self.main_window = embed['frame']
                parent = embed['parent']
                self.__embedded = embed['navigator']
            else:
                parent = self.main_window = wx.Frame(None, -1)
        else:
            parent = self.main_window = wx.Dialog(None, -1,
                    style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)

        self.main_window.SetTitle(self._form.title)
        self.main_window.SetIcons(self.__load_icons())
        self._menubar_ = None
        if not self.__embedded:
            self.main_window.Bind(wx.EVT_CLOSE, self.__on_close,
                    self.main_window)

        if self.__embedded:
            self.__master_sizer = parent.GetSizer()
        else:
            self.__master_sizer = wx.BoxSizer(wx.VERTICAL)

        # The base panel is needed on wxMSW to get a uniform background of the
        # frame.
        self.__base_panel = wx.Panel(parent, -1)
        base_sizer = wx.BoxSizer(wx.VERTICAL)
        self.__base_panel.SetSizer(base_sizer)

        self.__master_sizer.Add(self.__base_panel, 1, wx.EXPAND)

        # Add Statusbar, Toolbar and Menubar as requested and/or allowed
        if self._form.style != 'dialog':
            if not self._form._features['GUI:STATUSBAR:SUPPRESS']:
                self.__status_bar = self.main_window.CreateStatusBar()
                self.__status_bar.SetFieldsCount(5)
                self.__status_bar.SetStatusWidths([-1, 50, 50, 75, 75])

        # If the form is using tabs, we need to create a Notebook control as
        # page container, otherwise we can just use the base panel
        if self._form._layout.tabbed != 'none':
            style = self._TAB_STYLE[self._form._layout.tabbed]
            self._container = wx.Notebook(self.__base_panel, -1, style = style)
            # The border between the edge of the form and the page is 12 pixel
            # according to the GNOME Human Interface Guidlines.
            base_sizer.Add(self._container, 1, wx.EXPAND | wx.ALL, 12)
        else:
            self._container = self.__base_panel

        return self.main_window


    # -------------------------------------------------------------------------
    # Load an icon bundle for this form
    # -------------------------------------------------------------------------

    def __load_icons(self):

        idir = GConfig.getInstalledBase('forms_images', 'common_images')
        iconBundle = wx.IconBundle()

        for size in [16, 32, 64]:
            fname = os.path.join(idir, 'forms', 'default',
                    'gnue-%sx%s.png' % (size, size))
            if os.path.exists(fname):
                # On wxMSW loading an icon directly from a file doesn't work, so
                # we need to copy the icons from a bitmap on the fly
                icon = wx.Icon(fname, wx.BITMAP_TYPE_PNG)
                icon.CopyFromBitmap(wx.Image(fname).ConvertToBitmap())

                iconBundle.AddIcon(icon)

        return iconBundle


    # -------------------------------------------------------------------------
    # Set sizers, specify the size hints and fit all child controls
    # -------------------------------------------------------------------------

    def __update_sizer (self):

        # If a menubar has been created, it will be set for the main window.
        # This 'deferred' method is needed so OS X can rearrange some
        # items according to it's HIG.
        if self._menubar_ is not None:
            self.main_window.SetMenuBar(self._menubar_)
            self._menubar_ = None

        if not isinstance(self.main_window, wx.Dialog) and \
                self.main_window.GetToolBar():
            self.main_window.GetToolBar().Realize()

        if isinstance(self._container, wx.Notebook):
            self._container.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED,
                    self.__on_page_changed)

        self.show_page(0, True)
        if not self.__embedded:
            self.main_window.SetSizer(self.__master_sizer)
            self.main_window.Fit()

            self.walk(self.__update_size_hints_walker)

            self.main_window.CenterOnScreen ()


    # -------------------------------------------------------------------------
    # Update the size hints of all UI widgets
    # -------------------------------------------------------------------------

    def __update_size_hints_walker(self, item):

        if item != self:
            item.update_size_hints()


    # -------------------------------------------------------------------------
    # Show the form/dialog
    # -------------------------------------------------------------------------

    def _ui_show_(self, modal):

        self._uiDriver.hide_splash()
        self.main_window.Raise()
        self.sizing_enabled = True

        if modal and isinstance(self.main_window, wx.Dialog):
            self.main_window.ShowModal()
        else:
            self.main_window.Show()


    # -------------------------------------------------------------------------
    # Show the requested page
    # -------------------------------------------------------------------------

    def show_page(self, page_index, initial=False):
        """
        Show a given <page> in the current container.

        @param page_index: the zero-based index of the page to show
        """

        if isinstance (self._container, wx.Notebook):
            self.block_focus_events = True
            try:
                self._container.SetSelection(page_index)
            finally:
                self.block_focus_events = False
        else:
            # TODO: on wx.MSW the screen does not refresh automatically
            self.main_window.Freeze()
            try:
                for (index, widget) in enumerate(self.pages):
                    if index == page_index:
                        widget.Show()
                    else:
                        widget.Hide()

                if not initial:
                    self.main_window.Layout()
                    if not self.__embedded:
                        self.main_window.SetSizerAndFit(self.__master_sizer,
                                True)

            finally:
                self.main_window.Thaw()
                self.main_window.Refresh()


    # -------------------------------------------------------------------------
    # Event-handler
    # -------------------------------------------------------------------------

    def __on_close(self, event):

        if event.CanVeto():
            self._form.close()
            event.Veto()
        else:
            self.sizing_enabled = False
            # Actually, event.Skip() should call the standard event handler for
            # this event, meaning the form is being destroyed. For some reason,
            # this doesn't work, so we destroy the form manually.
            # event.Skip()

            # FIXME: Workaround for issue132: Destroying Windows that are not
            # visible at the time of being destroyed does not work in wx2.6.x.
            # This workaround shoud be obsolete with wx2.7+
            event.GetEventObject().Show()

            event.GetEventObject().Destroy()

            # FIXME: Workaround for issue131: Calling wx.EndBusyCursor()
            # at a time when all forms have been destroyed generates a segfault
            # if the last form that has been shown is open on a non-first page.
            # So we set main_window to None when destroying the main window and
            # don't call wx.EndBusyCursor() if main_window is None.
            self.main_window = None

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

    def __on_page_changed(self, event):

        # wx sets the focus to nowhere land *after* this code has run, so we
        # must do our focus handling in a CallAfter to get the focus to where
        # we want it.
        if not self.block_focus_events:
            wx.CallAfter(self._form._event_page_changed, event.GetSelection())
        event.Skip()


    # -------------------------------------------------------------------------
    # User feedback functions
    # -------------------------------------------------------------------------

    def _ui_begin_wait_(self):
        """
        Display the hourglass cursor on all windows of the application.
        """
        wx.BeginBusyCursor()

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

    def _ui_end_wait_(self):
        """
        Display the normal mouse cursor on all windows of the application.
        """
        # FIXME: Workaround for issue131: Calling wx.EndBusyCursor()
        # at a time when all forms have been destroyed generates a segfault
        # if the last form that has been shown is open on a non-first page.
        # So we set main_window to None when destroying the main window and
        # don't call wx.EndBusyCursor() if main_window is None.
        if self.main_window:
            wx.EndBusyCursor()


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

    def _ui_beep_(self):
        """
        Ring the system bell.
        """
        wx.Bell()

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

    def _ui_update_status_(self, tip, record_status, insert_status,
            record_number, record_count, page_number, page_count):
        """
        Update the apropriate section of the status bar with the given
        information.

        @param tip: message to be shown in the first section
        @param record_status: message for the second section
        @param insert_status: message for the third section
        @param record_number: number of the current record
        @param record_count: number of available records.  Together with
            record_number this will be set into the 4th section
        @param page_number: number of the current page
        @param page_count: number of available pages.  Together with the
            page_number this will be set into the 5th section
        """
        if not self.__status_bar:
            return

        if tip is not None:
            self.__status_bar.SetStatusText(tip, 0)

        if record_status is not None:
            self.__status_bar.SetStatusText(record_status, 1)

        if insert_status is not None:
            self.__status_bar.SetStatusText(insert_status, 2)

        if record_number is not None and record_count is not None:
            if record_number == 0 or record_count == 0:
                self.__status_bar.SetStatusText("", 3)
            else:
                self.__status_bar.SetStatusText(
                        "%s/%s" % (record_number, record_count), 3)

        if page_number is not None and page_count is not None:
            if page_number == 0 or page_count == 0:
                self.__status_bar.SetStatusText("", 4)
            else:
                self.__status_bar.SetStatusText(
                        "%s/%s" % (page_number, page_count), 4)

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

    def _ui_show_message_(self, message, kind, title, cancel):
        """
        This function creates a message box of a given kind and returns True,
        False or None depending on the button pressed.

        @param message: the text of the messagebox
        @param kind: type of the message box. Valid types are 'Info',
            'Warning', 'Question', 'Error'
        @param title: title of the message box
        @param cancel: If True a cancel button will be added to the dialog

        @return: True if the Ok-, Close-, or Yes-button was pressed, False if
            the No-button was pressed or None if the Cancel-button was pressed.
        """

        mbRec  = _MBOX_KIND.get(kind.lower())
        flags  = mbRec['type'] | mbRec['buttons']
        if cancel:
            flags |= wx.CANCEL
        title  = title and title or mbRec['title']

        dialog = wx.MessageDialog(self.main_window, message, title, flags)
        try:
            result = dialog.ShowModal()
        finally:
            dialog.Destroy()

        return _RESPONSE[result]

    # -------------------------------------------------------------------------
    # Show a file selection dialog
    # -------------------------------------------------------------------------

    def _ui_select_files_(self, title, default_dir, default_file, wildcard,
            mode, multiple, overwrite_prompt, file_must_exist):
        """
        Bring up a dialog for selecting filenames.

        @param title: Message to show on the dialog
        @param default_dir: the default directory, or the empty string
        @param default_file: the default filename, or the empty string
        @param wildcard: a list of tuples describing the filters used by the
            dialog.  Such a tuple constists of a description and a fileter.
            Example: [('PNG Files', '*.png'), ('JPEG Files', '*.jpg')]
            If no wildcard is given, all files will match (*.*)
        @param mode: Is this dialog an open- or a save-dialog.  If mode is
            'save' it is a save dialog, everything else would be an
            open-dialog.
        @param multiple: for open-dialog only: if True, allows selecting
            multiple files
        @param overwrite_prompt: for save-dialog only: if True, prompt for a
            confirmation if a file will be overwritten
        @param file_must_exist: if True, the user may only select files that
            actually exist

        @returns: a sequence of filenames or None if the dialog has been
            cancelled.
        """

        wst = '|'.join(['%s|%s' % (descr, filt) for (descr, filt) in wildcard])
        if not wst:
            wst = '%s (*.*)|*.*' % u_('All files')

        if mode.lower().startswith('save'):
            flags = wx.SAVE
            if overwrite_prompt:
                flags |= wx.OVERWRITE_PROMPT
        else:
            flags = wx.OPEN
            if multiple:
                flags |= wx.MULTIPLE

        if file_must_exist:
            flags |= wx.FILE_MUST_EXIST

        dlg = wx.FileDialog(self.main_window, title, default_dir, default_file,
                wst, flags)
        try:
            result = None
            mres = dlg.ShowModal()
            if mres == wx.ID_OK:
                if multiple:
                    result = dlg.GetPaths()
                else:
                    result = [dlg.GetPath()]
        finally:
            dlg.Destroy()

        return result

    # -------------------------------------------------------------------------
    # Select a directory
    # -------------------------------------------------------------------------

    def _ui_select_dir_(self, title, default_dir, new_dir):
        """
        Bring up a dialog for selecting a directory path.

        @param title: Message to show on the dialog
        @param default_dir: the default directory, or the empty string
        @param new_dir: If true, add "Create new directory" button and allow
            directory names to be editable. On Windows the new directory button
            is only available with recent versions of the common dialogs.

        @returns: a path or None if the dialog has been cancelled.
        """
        
        if new_dir:
            flags = wx.DD_NEW_DIR_BUTTON
        else:
            flags = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER

        dlg = wx.DirDialog(self.main_window, title, default_dir, flags)
        try:
            result = None
            mres = dlg.ShowModal()
            if mres == wx.ID_OK:
                result = dlg.GetPath()

        finally:
            dlg.Destroy()
            
        return result


    # -------------------------------------------------------------------------
    # Set title of form
    # -------------------------------------------------------------------------

    def _ui_set_title_(self, title):

      self.main_window.SetTitle(title)


    # -------------------------------------------------------------------------
    # Change the page
    # -------------------------------------------------------------------------

    def _ui_goto_page_(self, page):
        """
        Change the page shown in the form.

        @param page: UIPage instance of the page to be displayed
        """
        self.show_page(page.page_index)


    # -------------------------------------------------------------------------
    # Display an about box
    # -------------------------------------------------------------------------

    def _ui_show_about_(self, name, version, author, description):

        idir = GConfig.getInstalledBase('forms_images', 'common_images')
        icon = os.path.join(idir, 'gnue-icon.png')
    
        dlg = dialogs.AboutBox(self.main_window, name, version, author,
                description, icon)
        try:
            dlg.ShowModal()
        finally:
            dlg.Destroy()


    # -------------------------------------------------------------------------
    # Print form screenshot
    # -------------------------------------------------------------------------

    def _ui_printout_(self, title, subtitle, user):

        # Store the content of the form in a bitmap DC, because later parts of
        # the form are hidden behind the print dialog, which breaks the form
        # DC.
        window_dc = wx.ClientDC(self.main_window)
        w, h = self.main_window.GetClientSizeTuple()
        # We have to take the different origin of the client area into account.
        # On wxMSW the ClientDC contains the ToolBar.
        (offsx, offsy) = self.main_window.GetClientAreaOrigin()

        bitmap = wx.EmptyBitmap(w, h)
        form_dc = wx.MemoryDC()
        form_dc.SelectObject(bitmap)
        form_dc.Blit(0,0, w, h, window_dc, offsx, offsy)

        printout = _Printout(title, subtitle, user, bitmap)
        wx.Printer().Print(self.main_window, printout)


    # -------------------------------------------------------------------------
    # Close the window (actually only hide it)
    # -------------------------------------------------------------------------

    def _ui_close_(self):

        if isinstance(self.main_window, wx.Dialog) \
                and self.main_window.IsModal():
            self.main_window.EndModal(-1)
        elif self.__embedded:
            if self.main_window.GetToolBar():
                self.main_window.GetToolBar().Hide()
                self.main_window.SetToolBar(None)
            if self.main_window.GetStatusBar():
                self.main_window.GetStatusBar().Hide()
                self.main_window.SetStatusBar(None)
            self.__base_panel.Hide()
            self.__embedded.event_form_closed()
        else:
            self.main_window.Hide()


# =============================================================================
# Helper class for screen dump printing
# =============================================================================

class _Printout(wx.Printout):

    # -------------------------------------------------------------------------
    # Constructor
    # -------------------------------------------------------------------------

    def __init__(self, title, subtitle, login, bitmap):
        self.__title = title
        self.__subtitle = subtitle or ''
        self.__login = login
        self.__bitmap = bitmap
        wx.Printout.__init__(self, title)


    # -------------------------------------------------------------------------
    # Return info about number of pages in the document (always 1 here)
    # -------------------------------------------------------------------------

    def GetPageInfo(self):

        return (1, 1, 1, 1)


    # -------------------------------------------------------------------------
    # Print the page
    # -------------------------------------------------------------------------

    def OnPrintPage(self, page):

        # Prepare DC to paint on
        dc = self.GetDC()
        # dc.SetFont(wx.NORMAL_FONT)
        dc.SetPen(wx.BLACK_PEN)
        dc.SetBrush(wx.TRANSPARENT_BRUSH)

        # Scale from screen to printer
        screen_ppi_x, screen_ppi_y = self.GetPPIScreen()
        printer_ppi_x, printer_ppi_y = self.GetPPIPrinter()
        scale_x = float(printer_ppi_x) / screen_ppi_x
        scale_y = float(printer_ppi_y) / screen_ppi_y

        # Calculate page margins
        page_width, page_height = self.GetPageSizePixels()
        page_margin_left = page_margin_right  = int(printer_ppi_x*.75+.5)
        page_margin_top  = page_margin_bottom = int(printer_ppi_y*.75+.5)
        page_left   = page_margin_left
        page_top    = page_margin_top
        page_right  = page_width - page_margin_right
        page_bottom = page_height - page_margin_bottom

        # Page header, left
        y = self.draw_text(dc, page_left, page_top, self.__title)
        y = self.draw_text(dc, page_left, y, self.__subtitle)

        # Page header, right
        timestamp = time.strftime(
                "%Y-%m-%d %H:%M:%S",time.localtime(time.time()))
        y = self.draw_text(dc, page_right, page_top, timestamp,
                align_right=True)
        y = self.draw_text(dc, page_right, y, u_('Login: ') + self.__login,
                align_right=True)

        # Page header, line
        dc.DrawLine(page_left, y, page_right, y)
        canvas_top = y + int(printer_ppi_y * 0.25 + 0.5)

        # Page footer, left
        y = self.draw_text(dc, page_left, page_bottom,
                'GNUe Forms %s' % VERSION, align_bottom=True)

        # Page footer, line
        dc.DrawLine(page_left, y, page_right, y)
        canvas_bottom = y - int(printer_ppi_y * 0.25 + 0.5)

        # Space where we can paint the screenshot
        canvas_w = page_right - page_left
        canvas_h = canvas_bottom - canvas_top

        # If necessary, adjust scale factor to fit on page
        w = self.__bitmap.GetWidth()
        h = self.__bitmap.GetHeight()
        if w * scale_x > canvas_w:
            scale_y = float(scale_y) / (w * scale_x / canvas_w)
            scale_x = float(canvas_w) / w
        if h * scale_y > canvas_h:
            scale_x = float(scale_x) / (h * scale_y / canvas_h)
            scale_y = float(canvas_h) / h

        # the actual screenshot with border
        dc.SetUserScale(scale_x, scale_y)
        x = (page_left + canvas_w / 2) / scale_x - w / 2
        y = (canvas_top + canvas_h / 2) / scale_y - h / 2
        dc.DrawBitmap(self.__bitmap, x, y)
        dc.DrawRectangle(x, y, w, h)
        dc.SetUserScale(1, 1)


    # -------------------------------------------------------------------------
    # Draw text and calculate new y position
    # -------------------------------------------------------------------------

    def draw_text(self, dc, x, y, text, align_right=False, align_bottom=False):

        w, h = dc.GetTextExtent(text)

        _x = x
        _y = y

        if align_right:
            _x -= w
        if align_bottom:
            _y -= h

        dc.DrawText(text, _x, _y)

        if align_bottom:
            new_y = y - h * 1.3
        else:
            new_y = y + h * 1.3

        return new_y


# =============================================================================
# Widget configuration
# =============================================================================

configuration = {
    'baseClass': UIForm,
    'provides' : 'GFForm',
    'container': 1,
}
