# GNU Enterprise Forms - GTK UI Driver - UI specific dialogs
#
# 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: dialogs.py 9956 2009-10-11 18:54:57Z reinhard $

import sys

import gtk
import types
import pango

from gnue.common.apps import i18n, errors

from gnue.forms import VERSION

# =============================================================================
# Exceptions
# =============================================================================

class InvalidFieldTypeError (errors.ApplicationError):
  def __init__ (self, fieldtype):
    msg = u_("%s is not a valid type for an input field") % fieldtype
    errors.ApplicationError.__init__ (self, msg)


# =============================================================================
# Class implementing an about box for GTK2
# =============================================================================

class AboutBox (gtk.Dialog):
  """
  Displays an about dialog for the current application as defined by the given
  arguments.
  """

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

  def __init__ (self, name, version, author, description):
    """
    @param name: name of the application
    @param version: version of the form
    @param author: author of the form
    @param description: text describing the form
    """

    title = u_("About %s") % name
    gtk.Dialog.__init__ (self, title, None, gtk.DIALOG_MODAL,
                         (gtk.STOCK_OK, gtk.RESPONSE_OK))

    hBox = gtk.HBox ()
    box = gtk.Frame (" GNUe Forms ")
    hBox.pack_start (box, padding = 6)

    tbl = gtk.Table (2, 2)
    tbl.set_border_width (8)
    tbl.set_col_spacings (8)

    labels = []
    items  = []

    labels.append (self.__newLabel (u_("Version:"), tbl, 0, 1, 0, 1))
    labels.append (self.__newLabel (u_("Driver:"), tbl, 0, 1, 1, 2))

    items.append (self.__newLabel (VERSION, tbl, 1, 2, 0, 1))
    items.append (self.__newLabel (u'GTK', tbl, 1, 2, 1, 2))

    box.add (tbl)

    self.vbox.pack_start (hBox)

    box = gtk.Frame (u_(" Form Information "))
    hBox = gtk.HBox ()
    hBox.pack_start (box, padding = 6)

    tbl = gtk.Table (4, 2)
    tbl.set_border_width (8)
    tbl.set_col_spacings (8)

    labels.append (self.__newLabel (u_("Name:"),        tbl, 0, 1, 0, 1))
    labels.append (self.__newLabel (u_("Version:"),     tbl, 0, 1, 1, 2))
    labels.append (self.__newLabel (u_("Author:"),      tbl, 0, 1, 2, 3))
    labels.append (self.__newLabel (u_("Description:"), tbl, 0, 1, 3, 4))

    items.append (self.__newLabel (name,    tbl, 1, 2, 0, 1))
    items.append (self.__newLabel (version, tbl, 1, 2, 1, 2))
    items.append (self.__newLabel (author,  tbl, 1, 2, 2, 3))

    l = self.__newLabel (description, tbl, 1, 2, 3, 4)
    l.set_line_wrap (True)
    items.append (l)

    box.add (tbl)

    self.vbox.pack_start (hBox, padding = 6)

    self.__resizeLabels (labels)
    self.__resizeLabels (items)

    self.show_all ()


  # ---------------------------------------------------------------------------
  # Resize all labels in a sequence to the same width
  # ---------------------------------------------------------------------------

  def __resizeLabels (self, labels):

    maxW = max ([item.size_request () [0] for item in labels])

    for item in labels:
      item.set_size_request (maxW, -1)


  # ---------------------------------------------------------------------------
  # Create a new left- and top-aligned label and attach it to a given table
  # ---------------------------------------------------------------------------

  def __newLabel (self, text, table, left, right, top, bottom):

    if isinstance (text, types.StringType):
      text = unicode (text, i18n.encoding)

    result = gtk.Label (text)
    result.set_alignment (0, 0)
    table.attach (result, left, right, top, bottom)

    return result


# =============================================================================
# Class implementing a versatile input dialog
# =============================================================================

class InputDialog (gtk.Dialog):
  """
  Dialog class prompting the user for a given number of fields. These field
  definitions are specified as follows:

  A field definition is a tuple having these elements:
  - fieldlabel: This text will be used as label in the left column
  - fieldname: This is the key in the result-dictionary to contain the value
      entered by the user
  - fieldtype: Currently these types are supported:
      - label: The contents of 'fieldlabel' as static text
      - warning: The contents of 'fieldlabel' as static text, formatted as
          warning
      - string: A text entry control
      - password: A text entry control with obscured characters
      - dropdown: Foreach element given in 'elements' a separate ComboBox
          control will be created, where each one has it's own dictionary of
          allowed values. If a value is selected in one control, all others are
          synchronized to represent the same key-value.
  - default: Default value to use
  - masterfield: Used for 'dropdowns'. This item specifies another field
      definition acting as master field. If this master field is changed, the
      allowedValues of this dropdown will be changed accordingly. If a
      masterfield is specified the 'allowedValues' dictionaries are built like
      {master1: {key: value, key: value, ...}, master2: {key: value, ...}}
  - elements: sequence of input element tuples (label, allowedValues). This is
      used for dropdowns only. 'label' will be used as ToolTip for the control
      and 'allowedValues' gives a dictionary with all valid keys to be selected
      in the dropdown.

  @return: If closed by 'Ok' the result is a dictionary with all values entered
    by the user, where the "fieldname"s will be used as keys. If the user has
    not selected a value from a dropdown (i.e. it has no values to select)
    there will be no such key in the result dictionary. If the dialog is
    canceled ('Cancel'-Button) the result will be None.
  """

  _FIELDTYPES = ['label', 'warning', 'string', 'password', 'dropdown', 'image']

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

  def __init__ (self, title, fields, cancel = True):
    """
    Create a new input dialog

    @param title: Dialog title
    @param fields: sequence of field definition tuples
    @param cancel: If True add a Cancel button to the dialog
    """

    gtk.Dialog.__init__ (self, title, None, gtk.DIALOG_MODAL)

    ok = self.add_button (gtk.STOCK_OK, gtk.RESPONSE_OK)
    if cancel:
      cancel = self.add_button (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)

    self.set_default (ok)

    self.inputData    = {}
    self.__dropdowns  = {}
    self.__tooltips   = gtk.Tooltips ()
    self.__lastWidget = None

    self.table = gtk.Table (len (fields), 2)
    self.table.set_col_spacing (0, 10)

    hbox = gtk.HBox ()
    hbox.pack_start (self.table, padding = 8)

    self.vbox.pack_start (hbox, padding = 8)

    row = 0
    for (label, name, fieldtype, default, master, elements) in fields:
      ftype = fieldtype.lower ()
      if not ftype in self._FIELDTYPES:
        raise InvalidFieldTypeError, fieldtype

      if ftype in ['label', 'warning']:
        self.__add_text (row, label, ftype == 'warning')

      elif ftype == 'image':
        self.__addImage (row, name)

      else:
        self.__addLabel (row, label)

        if ftype == 'string':
          self.__add_string (row, label, name, default, elements)

        elif ftype == 'password':
          self.__add_string (row, label, name, default, elements, True)

        elif ftype == 'dropdown':
          self.__add_dropdown (row, label, name, default, master, elements)

      row += 1

    self.connect ('response', self.__response)
    self.show_all ()


  # ---------------------------------------------------------------------------
  # Add a centered label to the table
  # ---------------------------------------------------------------------------

  def __add_text (self, row, label, isWarning = False):

    gLabel = gtk.Label (label)
    gLabel.set_justify (gtk.JUSTIFY_CENTER)

    if isWarning:
      attr = pango.AttrList ()
      attr.insert (pango.AttrForeground (65535, 0, 0, 0, -1))
      gLabel.set_attributes (attr)

    self.table.attach (gLabel, 0, 2, row, row + 1)


  # ---------------------------------------------------------------------------
  # Add a text control for strings and passwords to the table
  # ---------------------------------------------------------------------------

  def __add_string (self, row, label, name, default, tips, password = False):

    entry = gtk.Entry ()
    entry.set_visibility (not password)
    entry.set_activates_default (True)

    entry.set_text (default or '')
    self.inputData [name] = default or ''

    if tips and tips [0][0]:
      self.__tooltips.set_tip (entry, tips [0][0])

    entry.connect ('changed', self.__entryChanged, name)
    entry.connect ('activate', self._activate)

    self.table.attach (entry, 1, 2, row, row + 1)
    self.__lastWidget = entry


  # ---------------------------------------------------------------------------
  # Add a series of coresponding dropdowns into a row of the table
  # ---------------------------------------------------------------------------

  def __add_dropdown (self, row, label, name, default, master, elements):

    hbox = gtk.HBox ()

    perMaster = self.__dropdowns.setdefault (master, {})
    drops     = perMaster.setdefault (name, [])

    for (tip, allowedValues) in elements:
      # TODO: We could use a single ListStore to keep all possible values,
      # where the key is in the first column and having another column per
      # dropdown with the description.
      (model, defIter) = self.__getModel (master, allowedValues, default)

      combo = gtk.ComboBox (model)
      cell = gtk.CellRendererText ()
      combo.pack_start (cell, True)
      combo.add_attribute (cell, 'text', 1)
      combo._allowedValues = allowedValues
      combo._default       = default

      combo.set_sensitive (model is not None)

      drops.append (combo)

      if defIter:
        combo.set_active_iter (defIter)
        self.inputData [name] = default

      combo.connect ('changed', self.__comboChanged, name, master)

      # Since a ComboBox widget cannot handle tooltips we add it to an EventBox
      cBox = gtk.EventBox ()
      cBox.add (combo)
      if tip:
        self.__tooltips.set_tip (cBox, tip)

      hbox.pack_start (cBox)

    self.table.attach (hbox, 1, 2, row, row + 1, yoptions = 0)
    self.__lastWidget = combo


  # ---------------------------------------------------------------------------
  # Add a label for an input widget to the left column of the table
  # ---------------------------------------------------------------------------

  def __addLabel (self, row, label):

    gLabel = gtk.Label (label)
    gLabel.set_alignment (0, gLabel.get_alignment () [1])

    self.table.attach (gLabel, 0, 1, row, row + 1, gtk.FILL)


  # ---------------------------------------------------------------------------
  # Add an image centered to the table
  # ---------------------------------------------------------------------------

  def __addImage (self, row, imageURL):
    
    bmp = gtk.Image ()
    bmp.set_from_file (imageURL)

    try:
      self.table.attach (bmp, 0, 2, row, row + 1, ypadding = 8)
    except:
      # TODO: remove this when pygtk win32 build accepts ypadding
      # TODO pygtk 2.6.2 fails on Linux too
      self.table.attach (bmp, 0, 2, row, row + 1)


  # ---------------------------------------------------------------------------
  # Handle changes in a drop down
  # ---------------------------------------------------------------------------

  def __comboChanged (self, combo, dataKey, master):

    iterator = combo.get_active_iter ()
    value = combo.get_model ().get_value (iterator, 0)

    self.inputData [dataKey] = value

    self.__updateDeps (combo, master, dataKey, value)


  # ---------------------------------------------------------------------------
  # Handle changes in a string or password entry
  # ---------------------------------------------------------------------------

  def __entryChanged (self, entry, dataKey):

    self.inputData [dataKey] = entry.get_text ()


  # ---------------------------------------------------------------------------
  # Get a tree model using the given master and data dictionary
  # ---------------------------------------------------------------------------

  def __getModel (self, master, dataDict, default = None):

    if master:
      values = dataDict.get (self.inputData.get (master), {})
    else:
      values = dataDict

    if values:
      defaultIterator = None
      result = gtk.ListStore (str, str)
      for (key, description) in values.items ():
        added = result.append (("%s" % key, "%s" % description))
        if default and key == default:
          defaultIterator = added
    else:
      result = defaultIterator = None

    return (result, defaultIterator)


  # ---------------------------------------------------------------------------
  # Synchronize a combo and update all depending controls
  # ---------------------------------------------------------------------------

  def __updateDeps (self, combo, master, element, newKey):
    
    drops = self.__dropdowns [master] [element]

    # First synchronize all dropdowns of the same element
    if len (drops) > 1:
      for item in drops:
        for row in item.get_model ():
          if row [0] == newKey:
            item.set_active_iter (row.iter)

    # If this dropdown is master of others, make sure to keep them in sync too
    if self.__dropdowns.has_key (element):
      self.__updateDepending (combo, element, newKey)



  # ---------------------------------------------------------------------------
  # update all depending controls
  # ---------------------------------------------------------------------------

  def __updateDepending (self, combo, master, masterKey):

    drops = self.__dropdowns [master]

    for (datakey, combos) in drops.items ():
      for combo in combos:
        (model, defIter) = self.__getModel (master, combo._allowedValues,
            combo._default)
        if model or combo.get_model ():
          combo.set_model (model)
        combo.set_sensitive (model is not None)
        if not model:
          if datakey in self.inputData:
            del self.inputData [datakey]
        else:
          if defIter is not None:
            combo.set_active_iter (defIter)

          elif datakey in self.inputData:
            del self.inputData [datakey]


  # ---------------------------------------------------------------------------
  # Default action for text controls
  # ---------------------------------------------------------------------------

  def _activate (self, widget):

    # If the widget is not the last one stop emission of the activate signal,
    # and emit a 'move-focus' signal instead
    if widget != self.__lastWidget:
      widget.stop_emission ('activate')
      self.emit ('move-focus', gtk.DIR_TAB_FORWARD)


  # ---------------------------------------------------------------------------
  # On cancelling the dialog we clear the input data record
  # ---------------------------------------------------------------------------

  def __response (self, dialog, rid):

    if rid != gtk.RESPONSE_OK:
      self.inputData = None


# =============================================================================
# Module self test
# =============================================================================

if __name__ == '__main__':
  desc = "this is a description to the form. it " \
         "could be a much longer text than one would excpet."

  x = AboutBox ('form-name', 'app-version', 'form-version', 'author', desc)
  x.run ()
  x.destroy ()

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

  cname = {'c1': 'demoa', 'c2': 'demob'}
  ckey  = {'c1': 'ck-A', 'c2': 'ck-B'}

  wija = {'c1': {'04': '2004', '05': '2005'},
          'c2': {'24': '2024', '25': '2025', '26': '2026'}}

  codes = {'24': {'241': 'c-24-1', '242': 'c-24-2'},
           '25': {'251': 'c-25-1'}}

  fields = [('Foo!', '/home/tamas/gnue/share/gnue/images/gnue.png', 'image',
             None, None, []),
            ('Username', '_username', 'string', 'frodo', None, \
              [('Name of the user', None)]),
            ('Password', '_password', 'password', 'foo', None, [('yeah',1)]),
            ('Foobar', '_foobar', 'dropdown', 'frob', None, \
                [('single', {'trash': 'Da Trash', 'frob': 'Frob'})]),
            ('Multi', '_multi', 'dropdown', '100', None, \
                [('name', {'50': 'A 50', '100': 'B 100', '9': 'C 9'}),
                ('sepp', {'50': 'se 50', '100': 'se 100', '9': 'se 9'})]),
            ('Noe', '_depp', 'label', 'furz', None, []),
            ('Das ist jetzt ein Fehler', None, 'warning', None, None, []),
            ('Firma', 'company', 'dropdown', 'c1', None,
                [('Name', cname), ('Code', ckey)]),
            ('Wirtschaftsjahr', 'wija', 'dropdown', '05', 'company',
                [('Jahr', wija)]),
            ('Codes', 'codes', 'dropdown', None, 'wija',
                [('Code', codes)]),
            (u"Dre\xf6ksau 'bl\xf6sepp'", None, 'warning', None, None, [])]

            
  #fields = [('Username', '_username', 'string', 'foobar', None, []),
            #('Password', '_password', 'password', None, None, [])]
  dialog = InputDialog ('Hackery', fields)
  dialog.run ()
  print "R:", dialog.inputData

  dialog.destroy ()
