#!/opt/anaconda1anaconda2anaconda3/bin/python

# 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 3 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, see <http://www.gnu.org/licenses/>.

"""Manage your address book."""

import cPickle
import gtk
import os
import subprocess
import sys

from valentine.delvian import ab

__author__ = ab.__author__
__version__ = ab.__version__


class ContactDialog(gtk.Dialog):

    """The Contact dialog.

    ATTRIBUTES

    email_entry         The entry for the email address of the contact.
    email_label         The label for the email address entry.
    name_entry          The entry for the name of the contact.
    name_label          The label for the name entry.
    table               The table for the layout of the dialog.

    METHODS

    __init__            Create a new Contact dialog.

    """

    def __init__(self, title, parent):
        """Create a new Contact dialog.

        ARGUMENTS

        title           The title of the dialog.
        parent          The parent of the dialog.

        """
        # The entry for the name of the contact.
        self.name_entry = gtk.Entry()
        self.name_entry.set_activates_default(True)
        # The entry for the email address of the contact.
        self.email_entry = gtk.Entry()
        self.email_entry.set_activates_default(True)
        # The label for the name entry.
        self.name_label = gtk.Label()
        self.name_label.set_mnemonic_widget(self.name_entry)
        self.name_label.set_text_with_mnemonic('_Name:')
        self.name_label.set_alignment(0, 0.5)
        # The label for the email address entry.
        self.email_label = gtk.Label()
        self.email_label.set_mnemonic_widget(self.email_entry)
        self.email_label.set_text_with_mnemonic('_Email:')
        self.email_label.set_alignment(0, 0.5)
        # The table for the layout of the dialog.
        self.table = gtk.Table(2, 2)
        self.table.attach(self.name_label, 0, 1, 0, 1)
        self.table.attach(self.name_entry, 1, 2, 0, 1)
        self.table.attach(self.email_label, 0, 1, 1, 2)
        self.table.attach(self.email_entry, 1, 2, 1, 2)
        self.table.set_row_spacings(6)
        self.table.set_col_spacings(12)
        self.table.set_border_width(6)
        self.table.show_all()
        # The dialog.
        gtk.Dialog.__init__(self, title, parent,
                            buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT))
        self.set_default_response(gtk.RESPONSE_ACCEPT)
        self.set_deletable(False)
        self.vbox.pack_start(self.table)


class FilterDialog(gtk.Dialog):

    """The Filter dialog.

    ATTRIBUTES

    entry               The entry.
    label               The label.
    table               The table for the layout of the dialog.

    METHODS

    __init__            Create a new Filter dialog.

    """

    def __init__(self, parent, criteria):
        """Create a new Filter dialog.

        ARGUMENTS

        parent          The parent of the dialog.
        criteria        The filter criteria.

        """
        # The entry.
        self.entry = gtk.Entry()
        self.entry.set_activates_default(True)
        self.entry.set_text(criteria)
        # The label.
        self.label = gtk.Label()
        self.label.set_mnemonic_widget(self.entry)
        self.label.set_text_with_mnemonic('Co_ntains:')
        self.label.set_alignment(0, 0.5)
        # The table for the layout of the dialog.
        self.table = gtk.Table(1, 2)
        self.table.attach(self.label, 0, 1, 0, 1)
        self.table.attach(self.entry, 1, 2, 0, 1)
        self.table.set_col_spacings(12)
        self.table.set_border_width(6)
        self.table.show_all()
        # The dialog.
        gtk.Dialog.__init__(self, 'Filter', parent,
                            buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, '_Filter', gtk.RESPONSE_ACCEPT))
        self.set_default_response(gtk.RESPONSE_ACCEPT)
        self.set_deletable(False)
        self.vbox.pack_start(self.table)


class GUI:

    """The GUI.

    ATTRIBUTES

    ab                  The address book.
    always_action_group
                        The actions that are always enabled.
    box                 The box for the layout of the window.
    email_cell_renderer
                        The cell renderer for the email address of the contacts.
    filter_criteria     The filter criteria for the tree model.
    filtered_tree_model
                        The filtered tree model.
    selected_action_group
                        The actions that are enabled if a row in the tree view is selected.
    selected_label      The label for the number of selected rows.
    toolbar             The toolbar.
    tree_model          The tree model to store the address book.
    tree_selection      The selection of the tree view.
    tree_view           The tree view to show the address book.
    tree_view_label     The label for the tree view.
    ui_manager          The UI manager.
    window              The window.

    METHODS

    __init__            Create a new GUI.
    about               Show the About dialog.
    delete              Remove the selected contact(s) from the address book.
    destroy_dialog      Destroy a dialog.
    email_cell_renderer_edited
                        Edit the contact in the address book.
    email_hook          Run the preferred email composer.
    filter              Show the Filter dialog.
    filter_response     Check the response to the Filter dialog.
    filter_tree_model   Filter the tree model.
    new                 Show the New Contact dialog.
    new_response        Check the response to the New Contact dialog.
    open                Show Contact dialog(s).
    quit                Quit the GUI.
    run                 Run the GUI.
    select_all          Select all the rows in the tree view.
    toolbar             Show or hide the toolbar.
    tree_selection_changed
                        Check the selection of the tree view.
    tree_view_row_activated
                        Show the Contact dialog.
    url_hook            Run the preferred web browser.

    """

    # The columns of the tree model.
    APPEND = -1
    NAME_COLUMN = 0
    EMAIL_COLUMN = 1

    def __init__(self):
        """Create a new GUI."""
        gtk.about_dialog_set_email_hook(self.email_hook)
        gtk.about_dialog_set_url_hook(self.url_hook)
        gtk.window_set_default_icon_name('stock_addressbook')
        # The actions that are always enabled.
        self.always_action_group = gtk.ActionGroup('always')
        self.always_action_group.add_actions((('File', None, '_File'),
                                              ('New', gtk.STOCK_NEW, None, None, 'Create a new contact.', self.new),
                                              ('Quit', gtk.STOCK_QUIT, None, None, None, self.quit),
                                              ('Edit', None, '_Edit'),
                                              ('Select All', None, 'Select _All', '<Ctrl>A', None, self.select_all),
                                              ('View', None, '_View'),
                                              ('Filter', None, '_Filter...', None, None, self.filter),
                                              ('Help', None, '_Help'),
                                              ('About', gtk.STOCK_ABOUT, None, None, None, self.about)))
        self.always_action_group.add_toggle_actions([('Toolbar', None, '_Toolbar', None, None, self.toolbar, True)])
        # The actions that are enabled if a row in the tree view is selected.
        self.selected_action_group = gtk.ActionGroup('selected')
        self.selected_action_group.set_sensitive(False)
        self.selected_action_group.add_actions((('Open', gtk.STOCK_OPEN, None, None, None, self.open),
                                                ('Delete', gtk.STOCK_DELETE, None, 'Delete', 'Delete the selected contact(s).', self.delete)))
        # The UI manager.
        self.ui_manager = gtk.UIManager()
        self.ui_manager.insert_action_group(self.always_action_group)
        self.ui_manager.insert_action_group(self.selected_action_group)
        self.ui_manager.add_ui_from_string('''<menubar>
                                                  <menu action="File">
                                                      <menuitem action="New" />
                                                      <menuitem action="Open" />
                                                      <separator />
                                                      <menuitem action="Quit" />
                                                  </menu>
                                                  <menu action="Edit">
                                                      <menuitem action="Delete" />
                                                      <separator />
                                                      <menuitem action="Select All" />
                                                  </menu>
                                                  <menu action="View">
                                                      <menuitem action="Toolbar" />
                                                      <separator />
                                                      <menuitem action="Filter" />
                                                  </menu>
                                                  <menu action="Help">
                                                      <menuitem action="About" />
                                                  </menu>
                                              </menubar>
                                              <toolbar>
                                                  <toolitem action="New" />
                                                  <separator />
                                                  <toolitem action="Delete" />
                                              </toolbar>''')
        # The toolbar.
        self.toolbar = self.ui_manager.get_widget('/toolbar')
        self.toolbar.get_nth_item(0).set_is_important(True)
        # The address book.
        if os.path.exists(ab.path):
            try:
                self.ab = cPickle.load(open(ab.path, 'rb'))
            except IOError, err:
                message_dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
                                                   buttons=gtk.BUTTONS_OK,
                                                   message_format=str(err))
                message_dialog.format_secondary_text('An input/output error occurred while loading your address book.')
                message_dialog.set_deletable(False)
                message_dialog.run()
                sys.exit('An input/output error occurred while loading your address book.')
        else:
            self.ab = ab.AddressBook()
        # The tree model to store the address book.
        self.tree_model = gtk.ListStore(str, str)
        for name in self.ab:
            self.tree_model.append((name, self.ab[name].email))
        self.tree_model.set_sort_column_id(GUI.NAME_COLUMN, gtk.SORT_ASCENDING)
        # The filter criteria for the tree model.
        self.filter_criteria = ''
        # The filtered tree model.
        self.filtered_tree_model = self.tree_model.filter_new()
        self.filtered_tree_model.set_visible_func(self.filter_tree_model)
        # The cell renderer for the email address of the contacts.
        self.email_cell_renderer = gtk.CellRendererText()
        self.email_cell_renderer.connect('edited',
                                         self.email_cell_renderer_edited)
        self.email_cell_renderer.set_property('editable', True)
        # The tree view to show the address book.
        self.tree_view = gtk.TreeView(self.filtered_tree_model)
        self.tree_view.connect('row-activated', self.tree_view_row_activated)
        self.tree_view.insert_column_with_attributes(GUI.APPEND, 'Name',
                                                     gtk.CellRendererText(),
                                                     text=GUI.NAME_COLUMN)
        self.tree_view.insert_column_with_attributes(GUI.APPEND,
                                                     'Email',
                                                     self.email_cell_renderer,
                                                     text=GUI.EMAIL_COLUMN)
        # The selection of the tree view.
        self.tree_selection = self.tree_view.get_selection()
        self.tree_selection.connect('changed', self.tree_selection_changed)
        self.tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
        # The label for the tree view.
        self.tree_view_label = gtk.Label()
        self.tree_view_label.set_mnemonic_widget(self.tree_view)
        self.tree_view_label.set_text_with_mnemonic('_Contacts:')
        self.tree_view_label.set_alignment(0, 0.5)
        self.tree_view_label.set_padding(3, 6)
        # The label for the number of selected rows.
        self.selected_label = gtk.Label('Number of selected contacts: 0')
        self.selected_label.set_alignment(0, 0.5)
        self.selected_label.set_padding(3, 6)
        # The box for the layout of the window.
        self.box = gtk.VBox()
        self.box.pack_start(self.ui_manager.get_widget('/menubar'), False)
        self.box.pack_start(self.toolbar, False)
        self.box.pack_start(self.tree_view_label, False)
        self.box.pack_start(self.tree_view)
        self.box.pack_start(self.selected_label, False)
        # The window.
        self.window = gtk.Window()
        self.window.connect('destroy', self.quit)
        self.window.set_title('Address Book')
        self.window.add_accel_group(self.ui_manager.get_accel_group())
        self.window.add(self.box)
        self.window.show_all()

    def about(self, widget):
        """Show the About dialog.

        ARGUMENTS

        widget          The widget that was activated.

        """
        about_dialog = gtk.AboutDialog()
        about_dialog.connect('response', self.destroy_dialog)
        about_dialog.set_name('Address Book')
        about_dialog.set_version(__version__)
        about_dialog.set_copyright(u'\u00A9 2010 Delvian Valentine')
        about_dialog.set_comments('Manage your address book.')
        about_dialog.set_license('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 3 of the License, or (at your option) any later version.\n\n'
                                 '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.\n\n'
                                 'You should have received a copy of the GNU General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>.')
        about_dialog.set_wrap_license(True)
        about_dialog.set_website('http://pypi.python.org/pypi/ab/')
        about_dialog.set_authors([__author__])
        about_dialog.set_logo_icon_name('stock_addressbook')
        about_dialog.set_transient_for(self.window)
        about_dialog.show()

    def delete(self, widget):
        """Remove the selected contact(s) from the address book.

        ARGUMENTS

        widget          The widget that was activated.

        """
        tree_model, tree_paths = self.tree_selection.get_selected_rows()
        selected_rows = self.tree_selection.count_selected_rows()
        if selected_rows > 1:
            message_dialog = gtk.MessageDialog(self.window,
                                               type=gtk.MESSAGE_WARNING,
                                               message_format='Are you sure you want to delete %s contacts from your address book?' % selected_rows)
        else:
            message_dialog = gtk.MessageDialog(self.window,
                                               type=gtk.MESSAGE_WARNING,
                                               message_format='Are you sure you want to delete %s from your address book?' % tree_model[tree_paths[0]][GUI.NAME_COLUMN])
        message_dialog.format_secondary_text('')
        message_dialog.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                                   gtk.STOCK_DELETE, gtk.RESPONSE_ACCEPT)
        message_dialog.set_default_response(gtk.RESPONSE_ACCEPT)
        message_dialog.set_deletable(False)
        if message_dialog.run() == gtk.RESPONSE_ACCEPT:
            message_dialog.destroy()
            tree_row_references = []
            for tree_path in tree_paths:
                tree_row_references.append(gtk.TreeRowReference(tree_model,
                                                                tree_path))
            for tree_row_reference in tree_row_references:
                tree_path = tree_row_reference.get_path()
                self.ab.remove(tree_model[tree_path][GUI.NAME_COLUMN])
                try:
                    cPickle.dump(self.ab, open(ab.path, 'wb'),
                                 cPickle.HIGHEST_PROTOCOL)
                except IOError, err:
                    message_dialog = gtk.MessageDialog(self.window,
                                                       type=gtk.MESSAGE_ERROR,
                                                       buttons=gtk.BUTTONS_OK,
                                                       message_format=str(err))
                    message_dialog.format_secondary_text('An input/output error occurred while saving your address book.')
                    message_dialog.set_deletable(False)
                    message_dialog.run()
                    message_dialog.destroy()
                else:
                    del self.tree_model[tree_model.convert_path_to_child_path(tree_path)]
        else:
            message_dialog.destroy()

    def destroy_dialog(self, dialog, response):
        """Destroy a dialog.

        ARGUMENTS

        dialog          The dialog.
        response        The response to the dialog.

        """
        dialog.destroy()

    def email_cell_renderer_edited(self, cell_renderer, tree_path, email):
        """Edit the contact in the address book.

        ARGUMENTS

        cell_renderer   The cell renderer.
        tree_path       The path to the selected row.
        email           The email address of the contact.

        """
        self.ab.edit(ab.Contact(self.filtered_tree_model[tree_path][GUI.NAME_COLUMN],
                                email))
        try:
            cPickle.dump(self.ab, open(ab.path, 'wb'), cPickle.HIGHEST_PROTOCOL)
        except IOError, err:
            message_dialog = gtk.MessageDialog(dialog, type=gtk.MESSAGE_ERROR,
                                               buttons=gtk.BUTTONS_OK,
                                               message_format=str(err))
            message_dialog.format_secondary_text('An input/output error occurred while saving your address book.')
            message_dialog.set_deletable(False)
            message_dialog.run()
            message_dialog.destroy()
        else:
            self.tree_model[self.filtered_tree_model.convert_path_to_child_path(tree_path)][GUI.EMAIL_COLUMN] = email

    def email_hook(self, dialog, email):
        """Run the preferred email composer.

        ARGUMENTS

        dialog          The About dialog.
        email           The email address to send an email to.

        """
        try:
            subprocess.call(('xdg-email', email))
        except OSError, err:
            message_dialog = gtk.MessageDialog(dialog, type=gtk.MESSAGE_ERROR,
                                               buttons=gtk.BUTTONS_OK,
                                               message_format=str(err))
            message_dialog.format_secondary_text('An operating system error occurred while running your preferred email composer.')
            message_dialog.set_deletable(False)
            message_dialog.run()
            message_dialog.destroy()

    def filter(self, widget):
        """Show the Filter dialog.

        ARGUMENTS

        widget          The widget that was activated.

        """
        filter_dialog = FilterDialog(self.window, self.filter_criteria)
        filter_dialog.connect('response', self.filter_response)
        filter_dialog.show()

    def filter_response(self, dialog, response):
        """Check the response to the Filter dialog.

        ARGUMENTS

        dialog          The dialog.
        response        The response to the dialog.

        """
        if response == gtk.RESPONSE_ACCEPT:
            self.filter_criteria = dialog.entry.get_text()
            if self.filter_criteria:
                self.tree_view_label.set_text_with_mnemonic('Filtered _contacts:')
            else:
                self.tree_view_label.set_text_with_mnemonic('_Contacts:')
            self.filtered_tree_model.refilter()
        dialog.destroy()

    def filter_tree_model(self, tree_model, tree_iter):
        """Filter the tree model.

        Return True if the contact should be visible or False if it should not.

        ARGUMENTS

        tree_model      The tree model.
        tree_iter       The iterator of the tree model.

        """
        if self.filter_criteria:
            return tree_model[tree_iter][GUI.NAME_COLUMN] in self.ab.search([self.filter_criteria])
        return tree_model[tree_iter][GUI.NAME_COLUMN] in self.ab

    def new(self, widget):
        """Show the New Contact dialog.

        ARGUMENTS

        widget          The widget that was activated.

        """
        contact_dialog = ContactDialog('New', self.window)
        contact_dialog.connect('response', self.new_response)
        contact_dialog.show()

    def new_response(self, dialog, response):
        """Check the response to the New Contact dialog.

        ARGUMENTS

        dialog          The dialog.
        response        The response to the dialog.

        """
        if response == gtk.RESPONSE_ACCEPT:
            name = dialog.name_entry.get_text()
            email = dialog.email_entry.get_text()
            try:
                self.ab.add(ab.Contact(name, email))
                cPickle.dump(self.ab, open(ab.path, 'wb'),
                             cPickle.HIGHEST_PROTOCOL)
            except ab.ContactExistsError, err:
                message_dialog = gtk.MessageDialog(dialog,
                                                   type=gtk.MESSAGE_ERROR,
                                                   buttons=gtk.BUTTONS_OK,
                                                   message_format=str(err))
                message_dialog.format_secondary_text('')
                message_dialog.set_deletable(False)
                message_dialog.run()
                message_dialog.destroy()
            except IOError, err:
                message_dialog = gtk.MessageDialog(dialog,
                                                   type=gtk.MESSAGE_ERROR,
                                                   buttons=gtk.BUTTONS_OK,
                                                   message_format=str(err))
                message_dialog.format_secondary_text('An input/output error occurred while saving your address book.')
                message_dialog.set_deletable(False)
                message_dialog.run()
                message_dialog.destroy()
            else:
                self.tree_model.append((name, email))
                dialog.destroy()
        else:
            dialog.destroy()

    def open(self, widget):
        """Show the Contact dialog(s).

        ARGUMENTS

        widget          The widget that was activated.

        """
        tree_model, tree_paths = self.tree_selection.get_selected_rows()
        tree_row_references = []
        for tree_path in tree_paths:
            tree_row_references.append(gtk.TreeRowReference(tree_model,
                                                            tree_path))
        for tree_row_reference in tree_row_references:
            tree_path = tree_row_reference.get_path()
            name = tree_model[tree_path][GUI.NAME_COLUMN]
            contact_dialog = ContactDialog(name, self.window)
            contact_dialog.name_entry.set_text(name)
            contact_dialog.name_entry.set_sensitive(False)
            contact_dialog.email_entry.set_text(tree_model[tree_path][GUI.EMAIL_COLUMN])
            if gtk.RESPONSE_ACCEPT == contact_dialog.run():
                email = contact_dialog.email_entry.get_text()
                self.ab.edit(ab.Contact(name, email))
                try:
                    cPickle.dump(self.ab, open(ab.path, 'wb'),
                                 cPickle.HIGHEST_PROTOCOL)
                except IOError, err:
                    message_dialog = gtk.MessageDialog(contact_dialog,
                                                       type=gtk.MESSAGE_ERROR,
                                                       buttons=gtk.BUTTONS_OK,
                                                       message_format=str(err))
                    message_dialog.format_secondary_text('An input/output error occurred while saving your address book.')
                    message_dialog.set_deleteable(False)
                    message_dialog.run()
                    message_dialog.destroy()
                else:
                    self.tree_model[tree_model.convert_path_to_child_path(tree_path)][GUI.EMAIL_COLUMN] = email
                    contact_dialog.destroy()
            else:
                contact_dialog.destroy()

    def quit(self, widget):
        """Quit the GUI.

        ARGUMENTS

        widget          The widget that was activated.

        """
        gtk.main_quit()

    def run(self):
        """Run the GUI."""
        gtk.main()

    def select_all(self, widget):
        """Select all the rows in the tree view.

        ARGUMENTS

        widget          The widget that was activated.

        """
        self.tree_selection.select_all()

    def toolbar(self, widget):
        """Show or hide the toolbar.

        ARGUMENTS

        widget          The widget that was activated.

        """
        if widget.get_active():
            self.toolbar.show()
        else:
            self.toolbar.hide()

    def tree_selection_changed(self, tree_selection):
        """Check the selection of the tree view.

        ARGUMENTS

        tree_selection  The selection of the tree view.

        """
        selected_rows = tree_selection.count_selected_rows()
        self.selected_action_group.set_sensitive(selected_rows)
        self.selected_label.set_text('Number of selected contacts: %s' % selected_rows)

    def tree_view_row_activated(self, tree_view, tree_path, tree_view_column):
        """Show the Contact dialog.

        ARGUMENTS

        tree_view       The tree view.
        tree_path       The path to the row that was activated.
        tree_view_column
                        The column in the row.

        """
        self.open(self.filtered_tree_model[tree_path])

    def url_hook(self, dialog, url):
        """Run the preferred web browser.

        ARGUMENTS

        dialog          The About dialog.
        url             The URL to open.

        """
        try:
            subprocess.call(('xdg-open', url))
        except OSError, err:
            message_dialog = gtk.MessageDialog(dialog, type=gtk.MESSAGE_ERROR,
                                               buttons=gtk.BUTTONS_OK,
                                               message_format=str(err))
            message_dialog.format_secondary_text('An operating system error occurred while running your preferred web browser.')
            message_dialog.set_deletable(False)
            message_dialog.run()
            message_dialog.destroy()


# Run the GUI.
if __name__ == '__main__':
    GUI().run()

# (c) 2010 Delvian Valentine <djdvalentine@gmail.com>
