ln -s ../lib/gui.py ${DESTDIR}/opt/hermes/bin/hermes
install -D -m 0644 -o root -g root src/*.py* ${DESTDIR}/opt/hermes/lib/
install -D -m 0644 -o root -g root share/background.png ${DESTDIR}/opt/hermes/share/
+ install -D -m 0644 -o root -g root share/account-*.png ${DESTDIR}/opt/hermes/share/
install -D -m 0644 -o root -g root share/hermes-64.png ${DESTDIR}/usr/share/icons/hicolor/scalable/hildon/hermes.png
install -D -m 0644 -o root -g root share/hermes-48.png ${DESTDIR}/usr/share/icons/hicolor/48x48/hildon/hermes.png
install -D -m 0644 -o root -g root share/hermes.desktop ${DESTDIR}/usr/share/applications/hildon/hermes.desktop
-hermes (0.1.1) unstable; urgency=low
+hermes (0.2.0) unstable; urgency=low
* New icons from Tim Samoff.
* Background image for the main window from Tim Samoff.
+ * Use URLs in profiles as a canonical mapping.
+ * Provide first pass at manual mapping through the GUI.
+ * Revise language on two buttons (inspired by MB#5452, reported by
+ Zach Goldberg).
- -- Andrew Flegg <andrew@bleb.org> Fri, 23 Oct 2009 10:26:28 +0100
+ -- Andrew Flegg <andrew@bleb.org> Sat, 31 Oct 2009 00:41:04 +0000
hermes (0.1.0) unstable; urgency=low
urls = re.findall('(?:(?:ftp|https?):\/\/|\\bwww\.|\\bftp\.)[,\w\.\-\/@:%?&=%+#~_$\*]+[\w=\/&=+#]', str, re.I | re.S)
updated = False
for url in urls:
- updated = self._add_url(contact, url, unique or url) or updated
+ updated = self._add_url(contact, url, unique or re.sub('(?:.*://)?(\w+(?:[\w\.])*).*', '\\1', url)) or updated
return updated
while ai.has_next():
attr = ai.next(as_a = EVCardAttribute)
existing = string_at(attr.value().next())
+ #print "Existing URL [%s] when adding [%s] to [%s] with constraint [%s]" % (existing, url, contact.get_name(), unique)
if existing == unique or existing == url:
return False
elif existing.find(unique) > -1:
url_attr = attr
- break
if not url_attr:
ai.add()
import gtk
import hildon
+import gobject
+import evolution
from ctypes import *
from pygobject import *
hildon.PannableArea.__init__(self)
self.contacts = contacts
- self.treestore = gtk.ListStore(str, gtk.gdk.Pixbuf)
+ self.treestore = gtk.ListStore(str, gtk.gdk.Pixbuf, gobject.TYPE_PYOBJECT)
for contact in self.contacts:
if not contact.get_name():
continue
if pixbuf:
size = min(pixbuf.get_width(), pixbuf.get_height())
pixbuf = pixbuf.subpixbuf(0, 0, size, size).scale_simple(48, 48, gtk.gdk.INTERP_BILINEAR)
- self.treestore.append(row = [contact.get_name(), pixbuf])
+ self.treestore.append(row = [contact.get_name(), pixbuf, contact])
self.treeview = gtk.TreeView(self.treestore)
tvcolumn = gtk.TreeViewColumn('Name', gtk.CellRendererText(), text = 0)
cell.set_property('xalign', 1.0)
tvcolumn = gtk.TreeViewColumn('Picture', cell, pixbuf = 1)
self.treeview.append_column(tvcolumn)
-
+
+ self.treeview.set_search_column(0)
+ self.treeview.connect('row-activated', self._activated)
self.add(self.treeview)
self.set_size_request(600, 380)
+
+
+ # -----------------------------------------------------------------------
+ def _activated(self, treeview, path, column):
+ """Used to emit the `contact-activated' signal once a row has been
+ selected."""
+
+ iter = treeview.get_model().get_iter(path)
+ contact = treeview.get_model().get_value(iter, 2)
+ self.emit('contact-activated', contact)
+
+
+_contact_activated = gobject.signal_new('contact-activated', ContactView, gobject.SIGNAL_ACTION, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT])
+
+
#!/usr/bin/env python
import gtk, gobject
-import gnome.gconf
import traceback
import time
import thread
-import os.path
import contactview
import urllib2
+import hildon
+import wimpworks
+import mapcontact
from wimpworks import WimpWorks
from hermes import Hermes
-gobject.threads_init()
-
class HermesGUI(WimpWorks):
"""Provides the GUI for Hermes, allowing the syncing of Facebook and
Twitter friends' information with the Evolution contacts' database.
# -----------------------------------------------------------------------
def __init__(self):
- WimpWorks.__init__(self, 'Hermes', version = '0.1.1', 'org.maemo.hermes')
+ WimpWorks.__init__(self, 'Hermes', version = '0.2.0', dbus_name = 'org.maemo.hermes')
self.set_background('background.png')
- layout = wimporks.HildonMainScreenLayout(offset = 0.8, container = self)
+ layout = wimpworks.HildonMainScreenLayout(offset = 0.8, container = self)
layout.add_button('Retrieve', "Get contacts' missing info")
layout.add_button('Refresh', "Update contacts' info")
# -----------------------------------------------------------------------
def do_accounts(self, widget = None):
- dialog = gtk.Dialog('Accounts', self.window)
+ dialog = gtk.Dialog('Accounts', self.main_window)
dialog.add_button('Save', gtk.RESPONSE_OK)
#pa = hildon.PannableArea()
tw_user = self.link_control(use_twitter, self.new_input('Twitter username'), indent)
tw_user.set_text(self.get_twitter_credentials()[0])
- tw_pass = self.link_control(indent, use_twitter, self.new_input('Twitter password', password = True), indent)
+ tw_pass = self.link_control(use_twitter, self.new_input('Twitter password', password = True), indent)
tw_pass.set_text(self.get_twitter_credentials()[1])
dialog.show_all()
return
if main:
- self.window.set_property('sensitive', False)
- thread.start_new_thread(self.doSync, (widget, force, False))
+ self.main_window.set_property('sensitive', False)
+ thread.start_new_thread(self.sync, (widget, force, False))
else:
try:
fb2c = Hermes(self,
# -----------------------------------------------------------------------
def open_summary(self, fb2c):
- gobject.idle_add(self.window.set_property, 'sensitive', True)
+ gobject.idle_add(self.main_window.set_property, 'sensitive', True)
- dialog = gtk.Dialog('Summary', self.window)
+ dialog = gtk.Dialog('Summary', self.main_window)
dialog.add_button('Done', gtk.RESPONSE_OK)
button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
title = 'Updated %d contacts' % (len(fb2c.updated)))
- button.connect('clicked', self.show_contacts, fb2c.updated)
+ button.connect('clicked', self.show_contacts, fb2c, fb2c.updated)
button.set_property('sensitive', len(fb2c.updated) > 0)
dialog.vbox.add(button)
button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
title = 'Matched %d contacts' % (len(fb2c.matched)))
- button.connect('clicked', self.show_contacts, fb2c.matched)
+ button.connect('clicked', self.show_contacts, fb2c, fb2c.matched)
button.set_property('sensitive', len(fb2c.matched) > 0)
dialog.vbox.add(button)
button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
title = '%d contacts unmatched' % (len(fb2c.unmatched)))
- button.connect('clicked', self.show_contacts, fb2c.unmatched)
+ button.connect('clicked', self.show_contacts, fb2c, fb2c.unmatched)
button.set_property('sensitive', len(fb2c.unmatched) > 0)
dialog.vbox.add(button)
# -----------------------------------------------------------------------
- def show_contacts(self, widget, contacts):
+ def show_contacts(self, widget, fb2c, contacts):
view = contactview.ContactView(contacts)
- dialog = gtk.Dialog('Contacts', self.window)
- #view.connect('contact-activated', self.map_contact)
+ dialog = gtk.Dialog('Contacts', self.main_window)
+ view.connect('contact-activated', self.map_contact, fb2c)
dialog.vbox.add(view)
dialog.show_all()
# -----------------------------------------------------------------------
- def map_contact(self, widget, contact):
- print widget, contact
+ def map_contact(self, widget, contact, fb2c):
+ view = mapcontact.MapContact(fb2c.friends, contact)
+
+ dialog = gtk.Dialog(contact.get_name(), self.main_window)
+ dialog.add_button('Update', gtk.RESPONSE_OK)
+ dialog.vbox.add(view)
+ dialog.show_all()
+
+ result = dialog.run()
+ dialog.hide()
+ if result == gtk.RESPONSE_OK:
+ friend = view.get_selected_friend()
+ if friend:
+ if 'contact' in friend and friend['contact'] == contact:
+ hildon.hildon_banner_show_information(self.main_window, '', "Removing existing mappings is not yet supported")
+ elif view.contact_mapped:
+ hildon.hildon_banner_show_information(self.main_window, '', "Changing existing mappings is not yet supported")
+ else:
+ if fb2c.update_contact(contact, friend, False):
+ fb2c.addresses.commit_contact(contact)
# -----------------------------------------------------------------------
def need_auth(self, main = False):
if main:
- hildon.hildon_banner_show_information(self.window, '', "Need to authenticate with Facebook")
+ hildon.hildon_banner_show_information(self.main_window, '', "Need to authenticate with Facebook")
else:
gobject.idle_add(self.need_auth, True)
# -----------------------------------------------------------------------
def block_for_auth(self, main = False, lock = None):
if main:
- note = gtk.Dialog('Facebook authorisation', self.window)
+ note = gtk.Dialog('Facebook authorisation', self.main_window)
note.add_button("Validate", gtk.RESPONSE_OK)
note.vbox.add(gtk.Label("\nPress 'Validate' once Facebook has\nbeen authenticated in web browser.\n"))
if main:
if i == 0:
self.progressbar = gtk.ProgressBar()
- self.progressnote = gtk.Dialog("Fetching friends' info", self.window)
+ self.progressnote = gtk.Dialog("Fetching friends' info", self.main_window)
self.progressnote.vbox.add(self.progressbar)
hildon.hildon_gtk_window_set_progress_indicator(self.progressnote, 1)
# -----------------------------------------------------------------------
def report_error(self, e, prefs = False):
if self.progressnote:
- self.window.set_property('sensitive', True)
+ self.main_window.set_property('sensitive', True)
self.progressnote.destroy()
- hildon.hildon_banner_show_information(self.window, '', e)
+ hildon.hildon_banner_show_information(self.main_window, '', e)
if prefs:
self.do_accounts()
def get_use_facebook(self):
- return self.gc.get_bool("/apps/maemo/hermes/use_facebook")
+ return self.gconf.get_bool("/apps/maemo/hermes/use_facebook")
def set_use_facebook(self, value):
- self.gc.set_bool("/apps/maemo/hermes/use_facebook", value)
+ self.gconf.set_bool("/apps/maemo/hermes/use_facebook", value)
def get_create_empty(self):
- return self.gc.get_bool("/apps/maemo/hermes/create_empty")
+ return self.gconf.get_bool("/apps/maemo/hermes/create_empty")
def set_create_empty(self, value):
- self.gc.set_bool("/apps/maemo/hermes/create_empty", value)
+ self.gconf.set_bool("/apps/maemo/hermes/create_empty", value)
def get_use_twitter(self):
- return self.gc.get_bool("/apps/maemo/hermes/use_twitter")
+ return self.gconf.get_bool("/apps/maemo/hermes/use_twitter")
def set_use_twitter(self, value, user, password):
- self.gc.set_bool("/apps/maemo/hermes/use_twitter", value)
- self.gc.set_string("/apps/maemo/hermes/twitter_user", user)
- self.gc.set_string("/apps/maemo/hermes/twitter_pwd", password)
+ self.gconf.set_bool("/apps/maemo/hermes/use_twitter", value)
+ self.gconf.set_string("/apps/maemo/hermes/twitter_user", user)
+ self.gconf.set_string("/apps/maemo/hermes/twitter_pwd", password)
def get_twitter_credentials(self):
- return (self.gc.get_string("/apps/maemo/hermes/twitter_user") or '',
- self.gc.get_string("/apps/maemo/hermes/twitter_pwd") or '')
+ return (self.gconf.get_string("/apps/maemo/hermes/twitter_user") or '',
+ self.gconf.get_string("/apps/maemo/hermes/twitter_pwd") or '')
# -------------------------------------------------------------------------
self.friends = {}
self.blocked_pictures = []
self.callback.progress(0, 0)
+ self.friends_by_url = {}
# -- Get a user session and retrieve Facebook friends...
#
if self.facebook:
+ print "+++ Opening Facebook..."
if self.fb.session_key is None:
self.fb.session_key = self.gc.get_string('/apps/maemo/hermes/session_key')
self.fb.secret = self.gc.get_string('/apps/maemo/hermes/secret_key')
for friend in self.fb.users.getInfo(self.fb.friends.get(), attrs):
key = unicode(friend['name']).encode('trans')
self.friends[key] = friend
+ self.friends_by_url[friend['profile_url']] = friend
friend['pic'] = friend[attrs[2]]
+ friend['account'] = 'facebook'
if friend['website']:
friend['homepage'] = friend['website']
if not friend['pic']:
self.blocked_pictures.append(friend)
+
# -- Retrieve following information from Twitter...
#
if self.twitter is not None:
+ print "+++ Opening Twitter..."
(user, passwd) = self.twitter
api = twitter.Api(username=user, password=passwd)
users = api.GetFriends()
for friend in api.GetFriends():
- self.friends[friend.name] = {'name': unicode(friend.name).encode('trans'), 'pic': friend.profile_image_url, 'birthday_date': None, 'twitter_url': 'http://twitter.com/%s' % (friend.screen_name), 'homepage' : friend.url}
+ key = unicode(friend.name).encode('trans')
+ url = 'http://twitter.com/%s' % (friend.screen_name)
+ self.friends[key] = {'name': friend.name, 'pic': friend.profile_image_url, 'birthday_date': None, 'twitter_url': url, 'homepage': friend.url, 'account': 'twitter'}
+ self.friends_by_url[url] = self.friends[key]
# TODO What if the user has *no* contacts?
# -- Find addresses...
#
print "+++ Syncing contacts..."
- addresses = evolution.ebook.open_addressbook('default')
+ self.addresses = evolution.ebook.open_addressbook('default')
print "+++ Addressbook opened..."
- self.store = ContactStore(addresses)
+ self.store = ContactStore(self.addresses)
print "+++ Contact store created..."
self.updated = []
self.unmatched = []
self.matched = []
- contacts = addresses.get_all_contacts()
+ contacts = self.addresses.get_all_contacts()
contacts.sort(key=lambda obj: obj.get_name())
current = 0
maximum = len(contacts)
current += 1
self.callback.progress(current, maximum)
found = False
- for name in names.variants(contact.get_name()):
- if name in self.friends:
- updated = self.update_contact(contact, self.friends[name], resync)
+ updated = False
+
+ # Try match on existing URL...
+ for url in self.store.get_urls(contact):
+ if url in self.friends_by_url:
+ updated = self.update_contact(contact, self.friends_by_url[url], resync)
found = True
-
if updated:
- self.updated.append(contact)
- addresses.commit_contact(contact)
- print "Saved changes to [%s]" % (contact.get_name())
-
- break
+ break
+
+ # Fallback to names...
+ if not found:
+ for name in names.variants(contact.get_name()):
+ if name in self.friends:
+ updated = self.update_contact(contact, self.friends[name], resync)
+ found = True
+ if updated:
+ break
+
+ # Keep track of updated stuff...
+ if updated:
+ self.updated.append(contact)
+ self.addresses.commit_contact(contact)
+ print "Saved changes to [%s]" % (contact.get_name())
if found:
self.matched.append(contact)
self.update_contact(contact, friend)
- addresses.add_contact(contact)
+ self.addresses.add_contact(contact)
self.updated.append(contact)
- addresses.commit_contact(contact)
+ self.addresses.commit_contact(contact)
print "Created [%s]" % (contact.get_name())
self.matched.append(contact)
--- /dev/null
+import gtk
+import hildon
+from ctypes import *
+from pygobject import *
+
+class MapContact(hildon.PannableArea):
+ """Widget which shows a list of friends from various feeds and allows
+ the mapping to a particular contact.
+
+ Copyright (c) Andrew Flegg <andrew@bleb.org> 2009.
+ Released under the Artistic Licence."""
+
+
+ # -----------------------------------------------------------------------
+ def __init__(self, friends, contact):
+ """Constructor. Passed a list of `friends' and the contact we're mapping."""
+
+ hildon.PannableArea.__init__(self)
+ self.friends = friends
+ self.contact = contact
+ self.treestore = gtk.ListStore(gtk.gdk.Pixbuf, str, gtk.gdk.Pixbuf, gobject.TYPE_PYOBJECT)
+
+ accounts = {}
+ _facebook = gtk.gdk.pixbuf_new_from_file('/opt/hermes/share/account-facebook.png')
+ _twitter = gtk.gdk.pixbuf_new_from_file('/opt/hermes/share/account-twitter.png')
+ _tick = gtk.icon_theme_get_default().load_icon('widgets_tickmark_list', 48, 0)
+
+ self.contact_mapped = False
+ mapped_iter = None
+ for key in sorted(self.friends.keys(), cmp = lambda a, b: cmp(a.lower(), b.lower())):
+ friend = self.friends[key]
+ if friend['account'] not in accounts:
+ accounts[friend['account']] = gtk.gdk.pixbuf_new_from_file("/opt/hermes/share/account-%s.png" % (friend['account']))
+
+ photo = friend['pic']
+ pixbuf = None
+ if 'contact' in friend:
+ if friend['contact'] == contact:
+ pixbuf = _tick
+ self.contact_mapped = True
+ mapped_iter = self.treestore.append([accounts[friend['account']], friend['name'], pixbuf, friend])
+ else:
+ continue
+ else:
+ self.treestore.append([accounts[friend['account']], friend['name'], pixbuf, friend])
+
+ self.treeview = gtk.TreeView(self.treestore)
+ hildon.hildon_gtk_tree_view_set_ui_mode(self.treeview, gtk.HILDON_UI_MODE_EDIT)
+
+ self.treeview.append_column(gtk.TreeViewColumn('Account', gtk.CellRendererPixbuf(), pixbuf = 0))
+ self.treeview.append_column(gtk.TreeViewColumn('Name', gtk.CellRendererText(), text = 1))
+
+ cell = gtk.CellRendererPixbuf()
+ cell.set_property('xalign', 1.0)
+ self.treeview.append_column(gtk.TreeViewColumn('Picture', cell, pixbuf = 2))
+
+ if mapped_iter:
+ path = self.treestore.get_path(mapped_iter)
+ self.treeview.get_selection().select_path(path)
+ self.treeview.scroll_to_cell(path)
+
+ self.add(self.treeview)
+ self.set_size_request(600, 320)
+
+ # -----------------------------------------------------------------------
+ def get_selected_friend(self):
+ """Return the selected friend, or `None' if none."""
+
+ (model, iter) = self.treeview.get_selection().get_selected()
+ if not iter:
+ return None
+
+ return model.get_value(iter, 3)
+
+
+_account_selected = gobject.signal_new('account-selected', MapContact, gobject.SIGNAL_ACTION, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])
+
# http://www.bleb.org/
import gtk, gobject
-import gnome.gconf
-import hildon, osso
-import traceback
-import time
import re
import thread
import os.path
_have_osso = True
except ImportError:
_have_osso = False
+
+try:
+ import gnome.gconf
+ _have_gconf = True
+except ImportError:
+ _have_gconf = False
gobject.threads_init()
# -- Main class...
#
-class Wimpworks:
+class WimpWorks:
'''A framework for creating easy-to-use graphical user interfaces using
GTK+, Python, DBus and more.
@param dbus_name Name to register with DBus. If unspecified, no
DBus registration will be performed.'''
- self.gconf = gnome.gconf.client_get_default()
self.name = application
self.dbus_name = dbus_name
self.menu = None
+
+ if _have_gconf:
+ self.gconf = gnome.gconf.client_get_default()
if _have_hildon:
self.app = hildon.Program()
self.main_window = hildon.Window()
+ gtk.set_application_name(application)
else:
self.app = None
self.main_window = gtk.Window()
-
- gtk.set_application_name(application)
- self.window.connect("delete-event", gtk.main_quit)
+
+ self.main_window.set_title(application)
+ self.main_window.connect("delete-event", gtk.main_quit)
if _have_osso and dbus_name:
self.osso_context = osso.Context(dbus_name, version, False)
self.app.add_window(self.main_window)
if _have_hildon:
- self._expose_hid = self.window.connect('expose-event', self._take_screenshot)
+ self._expose_hid = self.main_window.connect('expose-event', self._take_screenshot)
# -----------------------------------------------------------------------
@param window Window to set background of. If unset, will default to the
main application window.'''
- file = "/opt/%s/share/%s" % (self.name, file) # TODO Handle other forms of path
+ # TODO Handle other forms of path
+ file = "/opt/%s/share/%s" % (re.sub('[^a-z0-9_]', '', self.name.lower()), file)
if not window:
window = self.main_window
called.'''
if not window:
- window = self.main_Window
+ window = self.main_window
if not self.menu:
if _have_hildon:
'''Once the application has been initialised, this will show the main window
and run the mainloop.'''
- self.window.show_all()
+ self.main_window.show_all()
gtk.main()
@see http://maemo.org/api_refs/5.0/5.0-final/hildon/hildon-Additions-to-GTK+.html#hildon-gtk-window-take-screenshot'''
- self.window.disconnect(self._expose_hid)
+ self.main_window.disconnect(self._expose_hid)
if not os.path.isfile("/home/user/.cache/launch/%s.pvr" % (self.dbus_name)):
gobject.timeout_add(80, hildon.hildon_gtk_window_take_screenshot, self.main_window, True)
called 'do_method'.'''
method = re.sub('[^a-z0-9_]', '', method.lower())
- self.attr("do_%s" % (method))(event.window)
+ getattr(self, "do_%s" % (method))(event.window)
# -----------------------------------------------------------------------
@param offset The vertical offset for the buttons. If unspecified,
they will be centred. Ranges from 0.0 (top) to 1.0 (bottom).'''
- print self
self._container = container
alignment = gtk.Alignment(xalign=0.5, yalign=0.8, xscale=0.8)
self._box = gtk.HButtonBox()
alignment.add(self._box)
- container.add(alignment)
+ container.main_window.add(alignment)
self._box.set_property('layout-style', gtk.BUTTONBOX_SPREAD)
@param title Value of the button, and used to derive the callback method.
@param subtitle An optional subtitle containing more information.'''
- if _have_hildon:
- button = hildon.Button(gtk.HILDON_SIZE_THUMB_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
- title = title, value = subtitle)
- else:
- button = gtk.Button(title)
+ if _have_hildon:
+ button = hildon.Button(gtk.HILDON_SIZE_THUMB_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL,
+ title = title, value = subtitle)
+ else:
+ button = gtk.Button(label = title)
+
+ button.set_property('width-request', 250)
+ button.connect('clicked', self._container.callback, title)
+ self._box.add(button)
- button.set_property('width-request', 250)
- button.connect('clicked', self.wimpworks.callback, title)
- self._box.add(button)