gramps/src/DateEdit.py
Benny Malengier c2e353a72b 4383: Error while attempt to edit family
2521: Problem with erroneous birth date in using the family editor.
We show validation error on date > next year, and don't crash on large year anymore



svn: r16468
2011-01-25 12:45:49 +00:00

424 lines
16 KiB
Python

#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2002-2006 Donald N. Allingham
# Copyright (C) 2009 Douglas S. Blank
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id$
"""
Date editing module for GRAMPS.
The DateEdit.DateEdit provides visual feedback to the user via a pixamp
to indicate if the associated GtkEntry box contains a valid date. Green
means complete and regular date. Yellow means a valid, but not a regular date.
Red means that the date is not valid, and will be viewed as a text string
instead of a date.
The DateEdit.DateEditor provides a dialog in which the date can be
unambiguously built using UI controls such as menus and spin buttons.
"""
#-------------------------------------------------------------------------
#
# Python modules
#
#-------------------------------------------------------------------------
#-------------------------------------------------------------------------
#
# set up logging
#
#-------------------------------------------------------------------------
import logging
__LOG = logging.getLogger(".DateEdit")
#-------------------------------------------------------------------------
#
# GNOME modules
#
#-------------------------------------------------------------------------
import gtk
#-------------------------------------------------------------------------
#
# gramps modules
#
#-------------------------------------------------------------------------
from gen.ggettext import sgettext as _
from gen.lib.date import Date, NextYear
import DateHandler
import const
import GrampsDisplay
import ManagedWindow
from Errors import ValidationError
from glade import Glade
#-------------------------------------------------------------------------
#
# Constants
#
#-------------------------------------------------------------------------
MOD_TEXT = (
(Date.MOD_NONE , _('Regular')),
(Date.MOD_BEFORE , _('Before')),
(Date.MOD_AFTER , _('After')),
(Date.MOD_ABOUT , _('About')),
(Date.MOD_RANGE , _('Range')),
(Date.MOD_SPAN , _('Span')),
(Date.MOD_TEXTONLY , _('Text only')) )
QUAL_TEXT = (
(Date.QUAL_NONE, _('Regular')),
(Date.QUAL_ESTIMATED, _('Estimated')),
(Date.QUAL_CALCULATED, _('Calculated')) )
CAL_TO_MONTHS_NAMES = {
Date.CAL_GREGORIAN : DateHandler.displayer.short_months,
Date.CAL_JULIAN : DateHandler.displayer.short_months,
Date.CAL_HEBREW : DateHandler.displayer.hebrew,
Date.CAL_FRENCH : DateHandler.displayer.french,
Date.CAL_PERSIAN : DateHandler.displayer.persian,
Date.CAL_ISLAMIC : DateHandler.displayer.islamic,
Date.CAL_SWEDISH : DateHandler.displayer.swedish }
WIKI_HELP_PAGE = '%s_-_Entering_and_Editing_Data:_Detailed_-_part_1' % const.URL_MANUAL_PAGE
WIKI_HELP_SEC = _('manual|Editing_Dates')
#-------------------------------------------------------------------------
#
# DateEdit
#
#-------------------------------------------------------------------------
class DateEdit(object):
"""Class that associates a pixmap with a text widget, providing visual
feedback that indicates if the text widget contains a valid date"""
def __init__(self, date_obj, text_obj, button_obj, uistate, track):
"""
Create a connection between the date_obj, text_obj and the pixmap_obj.
Assigns callbacks to parse and change date when the text
in text_obj is changed, and to invoke Date Editor when the LED
button_obj is pressed.
"""
self.uistate = uistate
self.track = track
self.date_obj = date_obj
self.text_obj = text_obj
self.button_obj = button_obj
image = gtk.Image()
image.set_from_stock('gramps-date-edit', gtk.ICON_SIZE_BUTTON)
self.button_obj.set_image(image)
self.button_obj.set_relief(gtk.RELIEF_NORMAL)
self.pixmap_obj = button_obj.get_child()
self.text_obj.connect('validate', self.validate)
self.text_obj.connect('content-changed', self.set_date)
self.button_obj.connect('clicked', self.invoke_date_editor)
self.text_obj.set_text(DateHandler.displayer.display(self.date_obj))
self.text_obj.validate()
def set_date(self, widget):
"""
Parse date from text entry to date object
"""
date = DateHandler.parser.parse(unicode(self.text_obj.get_text()))
self.date_obj.copy(date)
def validate(self, widget, data):
"""
Validate current date in text entry
"""
# if text could not be parsed it is assumed invalid
if self.date_obj.get_modifier() == Date.MOD_TEXTONLY:
return ValidationError(_('Bad Date'))
elif (self.date_obj.to_calendar(calendar_name=Date.CAL_GREGORIAN) >
NextYear()):
return ValidationError(_('Date more than one year in the future'))
def invoke_date_editor(self, obj):
"""
Invokes Date Editor dialog when the user clicks the Calendar button.
If date was in fact built, sets the date_obj to the newly built
date.
"""
date_dialog = DateEditorDialog(self.date_obj, self.uistate, self.track)
the_date = date_dialog.return_date
self.update_after_editor(the_date)
def update_after_editor(self, date_obj):
"""
Update text entry and validate it
"""
if date_obj:
# first we set the text entry, that emits 'content-changed'
# signal thus the date object gets updated too
self.text_obj.set_text(DateHandler.displayer.display(date_obj))
self.text_obj.validate()
#-------------------------------------------------------------------------
#
# DateEditorDialog
#
#-------------------------------------------------------------------------
class DateEditorDialog(ManagedWindow.ManagedWindow):
"""
Dialog allowing to build the date precisely, to correct possible
limitations of parsing and/or underlying structure of Date.
"""
def __init__(self, date, uistate, track):
"""
Initiate and display the dialog.
"""
ManagedWindow.ManagedWindow.__init__(self, uistate, track, self)
# Create self.date as a copy of the given Date object.
self.date = Date(date)
self.top = Glade()
self.set_window(
self.top.toplevel,
self.top.get_object('title'),
_('Date selection'))
self.calendar_box = self.top.get_object('calendar_box')
for name in Date.ui_calendar_names:
self.calendar_box.get_model().append([name])
self.calendar_box.set_active(self.date.get_calendar())
self.calendar_box.connect('changed', self.switch_calendar)
self.quality_box = self.top.get_object('quality_box')
for item_number in range(len(QUAL_TEXT)):
self.quality_box.append_text(QUAL_TEXT[item_number][1])
if self.date.get_quality() == QUAL_TEXT[item_number][0]:
self.quality_box.set_active(item_number)
self.type_box = self.top.get_object('type_box')
for item_number in range(len(MOD_TEXT)):
self.type_box.append_text(MOD_TEXT[item_number][1])
if self.date.get_modifier() == MOD_TEXT[item_number][0]:
self.type_box.set_active(item_number)
self.type_box.connect('changed', self.switch_type)
self.start_month_box = self.top.get_object('start_month_box')
self.stop_month_box = self.top.get_object('stop_month_box')
month_names = CAL_TO_MONTHS_NAMES[self.date.get_calendar()]
for name in month_names:
self.start_month_box.append_text(name)
self.stop_month_box.append_text(name)
self.start_month_box.set_active(self.date.get_month())
self.stop_month_box.set_active(self.date.get_stop_month())
self.start_day = self.top.get_object('start_day')
self.start_day.set_value(self.date.get_day())
self.start_year = self.top.get_object('start_year')
self.start_year.set_value(self.date.get_year())
self.stop_day = self.top.get_object('stop_day')
self.stop_day.set_value(self.date.get_stop_day())
self.stop_year = self.top.get_object('stop_year')
self.stop_year.set_value(self.date.get_stop_year())
self.dual_dated = self.top.get_object('dualdated')
self.new_year = self.top.get_object('newyear')
self.new_year.set_text(self.date.newyear_to_str())
# Disable second date controls if not compound date
if not self.date.is_compound():
self.stop_day.set_sensitive(0)
self.stop_month_box.set_sensitive(0)
self.stop_year.set_sensitive(0)
# Disable the rest of controls if a text-only date
if self.date.get_modifier() == Date.MOD_TEXTONLY:
self.start_day.set_sensitive(0)
self.start_month_box.set_sensitive(0)
self.start_year.set_sensitive(0)
self.calendar_box.set_sensitive(0)
self.quality_box.set_sensitive(0)
self.dual_dated.set_sensitive(0)
self.new_year.set_sensitive(0)
self.text_entry = self.top.get_object('date_text_entry')
self.text_entry.set_text(self.date.get_text())
if self.date.get_slash():
self.dual_dated.set_active(1)
self.calendar_box.set_sensitive(0)
self.calendar_box.set_active(Date.CAL_JULIAN)
self.dual_dated.connect('toggled', self.switch_dual_dated)
# The dialog is modal -- since dates don't have names, we don't
# want to have several open dialogs, since then the user will
# loose track of which is which. Much like opening files.
self.return_date = None
self.show()
while True:
response = self.window.run()
if response == gtk.RESPONSE_HELP:
GrampsDisplay.help(webpage=WIKI_HELP_PAGE,
section=WIKI_HELP_SEC)
elif response == gtk.RESPONSE_DELETE_EVENT:
break
else:
if response == gtk.RESPONSE_OK:
(the_quality, the_modifier, the_calendar,
the_value, the_text, the_newyear) = self.build_date_from_ui()
self.return_date = Date(self.date)
self.return_date.set(
quality=the_quality,
modifier=the_modifier,
calendar=the_calendar,
value=the_value,
text=the_text,
newyear=the_newyear)
self.close()
break
def build_menu_names(self, obj):
"""
Define the menu entry for the ManagedWindows
"""
return (_("Date selection"), None)
def build_date_from_ui(self):
"""
Collect information from the UI controls and return
5-tuple of (quality,modifier,calendar,value,text)
"""
# It is important to not set date based on these controls.
# For example, changing the caledar makes the date inconsistent
# until the callback of the calendar menu is finished.
# We need to be able to use this function from that callback,
# so here we just report on the state of all widgets, without
# actually modifying the date yet.
modifier = MOD_TEXT[self.type_box.get_active()][0]
text = self.text_entry.get_text()
if modifier == Date.MOD_TEXTONLY:
return (Date.QUAL_NONE, Date.MOD_TEXTONLY, Date.CAL_GREGORIAN,
Date.EMPTY,text, Date.NEWYEAR_JAN1)
quality = QUAL_TEXT[self.quality_box.get_active()][0]
if modifier in (Date.MOD_RANGE, Date.MOD_SPAN):
value = (
self.start_day.get_value_as_int(),
self.start_month_box.get_active(),
self.start_year.get_value_as_int(),
self.dual_dated.get_active(),
self.stop_day.get_value_as_int(),
self.stop_month_box.get_active(),
self.stop_year.get_value_as_int(),
self.dual_dated.get_active())
else:
value = (
self.start_day.get_value_as_int(),
self.start_month_box.get_active(),
self.start_year.get_value_as_int(),
self.dual_dated.get_active())
calendar = self.calendar_box.get_active()
newyear = Date.newyear_to_code(self.new_year.get_text())
return (quality, modifier, calendar, value, text, newyear)
def switch_type(self, obj):
"""
Disable/enable various date controls depending on the date
type selected via the menu.
"""
the_modifier = MOD_TEXT[self.type_box.get_active()][0]
# Disable/enable second date controls based on whether
# the type allows compound dates
if the_modifier in (Date.MOD_RANGE, Date.MOD_SPAN):
stop_date_sensitivity = 1
else:
stop_date_sensitivity = 0
self.stop_day.set_sensitive(stop_date_sensitivity)
self.stop_month_box.set_sensitive(stop_date_sensitivity)
self.stop_year.set_sensitive(stop_date_sensitivity)
# Disable/enable the rest of the controls if the type is text-only.
date_sensitivity = not the_modifier == Date.MOD_TEXTONLY
self.start_day.set_sensitive(date_sensitivity)
self.start_month_box.set_sensitive(date_sensitivity)
self.start_year.set_sensitive(date_sensitivity)
self.calendar_box.set_sensitive(date_sensitivity)
self.quality_box.set_sensitive(date_sensitivity)
self.dual_dated.set_sensitive(date_sensitivity)
self.new_year.set_sensitive(date_sensitivity)
def switch_dual_dated(self, obj):
"""
Changed whether this is a dual dated year, or not.
Dual dated years are represented in the Julian calendar
so that the day/months don't changed in the Text representation.
"""
if self.dual_dated.get_active():
self.calendar_box.set_active(Date.CAL_JULIAN)
self.calendar_box.set_sensitive(0)
else:
self.calendar_box.set_sensitive(1)
def switch_calendar(self, obj):
"""
Change month names and convert the date based on the calendar
selected via the menu.
"""
old_cal = self.date.get_calendar()
new_cal = self.calendar_box.get_active()
(the_quality, the_modifier, the_calendar,
the_value, the_text, the_newyear) = self.build_date_from_ui()
self.date.set(
quality=the_quality,
modifier=the_modifier,
calendar=old_cal,
value=the_value,
text=the_text,
newyear=the_newyear)
if not self.date.is_empty():
self.date.convert_calendar(new_cal)
self.start_month_box.get_model().clear()
self.stop_month_box.get_model().clear()
month_names = CAL_TO_MONTHS_NAMES[new_cal]
for name in month_names:
self.start_month_box.append_text(name)
self.stop_month_box.append_text(name)
self.start_day.set_value(self.date.get_day())
self.start_month_box.set_active(self.date.get_month())
self.start_year.set_value(self.date.get_year())
self.stop_day.set_value(self.date.get_stop_day())
self.stop_month_box.set_active(self.date.get_stop_month())
self.stop_year.set_value(self.date.get_stop_year())