[gnome-games/sudoku-tube] Add contact selector
- From: Zhang Sen <zhangsen src gnome org>
- To: svn-commits-list gnome org
- Subject: [gnome-games/sudoku-tube] Add contact selector
- Date: Thu, 18 Jun 2009 08:39:06 -0400 (EDT)
commit 4738322221f38dfd45edb72877d97f60d29b2a32
Author: Zhang Sen <zh jesse gmail com>
Date: Thu Jun 18 20:36:38 2009 +0800
Add contact selector
gnome-sudoku/data/contact_selector_dialog.ui | 61 +++++
gnome-sudoku/images/available-16x16.png | Bin 0 -> 685 bytes
gnome-sudoku/images/away-16x16.png | Bin 0 -> 536 bytes
gnome-sudoku/images/busy-16x16.png | Bin 0 -> 618 bytes
gnome-sudoku/images/extended-away-16x16.png | Bin 0 -> 752 bytes
gnome-sudoku/src/lib/contact_selector.py | 323 ++++++++++++++++++++++++++
gnome-sudoku/src/lib/main.py | 12 +-
7 files changed, 393 insertions(+), 3 deletions(-)
---
diff --git a/gnome-sudoku/data/contact_selector_dialog.ui b/gnome-sudoku/data/contact_selector_dialog.ui
new file mode 100644
index 0000000..82c164e
--- /dev/null
+++ b/gnome-sudoku/data/contact_selector_dialog.ui
@@ -0,0 +1,61 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy toplevel-contextual -->
+ <object class="GtkDialog" id="contact_selector_dialog">
+ <property name="border_width">5</property>
+ <property name="title" translatable="yes">Choose a contact</property>
+ <property name="type_hint">normal</property>
+ <property name="has_separator">False</property>
+ <child internal-child="vbox">
+ <object class="GtkVBox" id="vbox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child internal-child="action_area">
+ <object class="GtkHButtonBox" id="action_area">
+ <property name="visible">True</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="label" translatable="yes">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="ok_button">
+ <property name="label" translatable="yes">gtk-ok</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-6">cancel_button</action-widget>
+ <action-widget response="-5">ok_button</action-widget>
+ </action-widgets>
+ </object>
+</interface>
diff --git a/gnome-sudoku/images/available-16x16.png b/gnome-sudoku/images/available-16x16.png
new file mode 100644
index 0000000..5435e02
Binary files /dev/null and b/gnome-sudoku/images/available-16x16.png differ
diff --git a/gnome-sudoku/images/away-16x16.png b/gnome-sudoku/images/away-16x16.png
new file mode 100644
index 0000000..7576a61
Binary files /dev/null and b/gnome-sudoku/images/away-16x16.png differ
diff --git a/gnome-sudoku/images/busy-16x16.png b/gnome-sudoku/images/busy-16x16.png
new file mode 100644
index 0000000..90eab60
Binary files /dev/null and b/gnome-sudoku/images/busy-16x16.png differ
diff --git a/gnome-sudoku/images/extended-away-16x16.png b/gnome-sudoku/images/extended-away-16x16.png
new file mode 100644
index 0000000..9dec477
Binary files /dev/null and b/gnome-sudoku/images/extended-away-16x16.png differ
diff --git a/gnome-sudoku/src/lib/contact_selector.py b/gnome-sudoku/src/lib/contact_selector.py
new file mode 100644
index 0000000..4e5cced
--- /dev/null
+++ b/gnome-sudoku/src/lib/contact_selector.py
@@ -0,0 +1,323 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/python
+
+import logging
+import os
+
+import gtk
+import telepathy
+import dbus.mainloop.glib
+
+from telepathy.interfaces import (
+ CHANNEL,
+ CHANNEL_INTERFACE_GROUP,
+ CHANNEL_TYPE_CONTACT_LIST,
+ CONNECTION,
+ CONNECTION_INTERFACE_ALIASING,
+ CONNECTION_INTERFACE_CONTACTS,
+ CONNECTION_INTERFACE_REQUESTS,
+ CONNECTION_INTERFACE_SIMPLE_PRESENCE)
+
+from telepathy.constants import (
+ CONNECTION_PRESENCE_TYPE_AVAILABLE,
+ CONNECTION_PRESENCE_TYPE_AWAY,
+ CONNECTION_PRESENCE_TYPE_BUSY,
+ CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY,
+ HANDLE_TYPE_LIST)
+
+import defaults
+
+dbus.mainloop.glib.DBusGMainLoop(set_as_default = True)
+
+logging.basicConfig()
+logger = logging.getLogger("contact_selector")
+logger.setLevel(logging.DEBUG)
+
+
+def dbus_int_list_to_string(dbus_list):
+ """Convert a dbus-list of interger to normal python one
+
+ dbus_list can also be a normal python list; can't hurt"""
+ return [int(x) for x in dbus_list]
+
+class _Contact:
+ """Help class for the contact to be stored in ContactListStore"""
+
+ def __init__(self, contact_id, alias, presence):
+ self.contact_id = contact_id
+ self.alias = alias
+ self.presence = presence
+
+
+class ContactList:
+ """the list of contacts from *one* connection"""
+
+ def __init__(self, list_store, conn):
+ self._list_store = list_store
+ self._conn = conn
+
+ # dict of {handle=>Contact}
+ # If ListStore supports searching, this _contact_list is not needed.
+ self._contact_list = {}
+
+ self._conn.call_when_ready(self._connection_ready_cb)
+
+ def _connection_ready_cb(self, conn):
+ """Request Channel from conn when it's ready"""
+ assert self._conn == conn
+ logger.debug("Connection is ready: %s" % self._conn.service_name)
+ # TODO react to connection status change
+ # TODO is this needed?
+ # if CONNECTION_INTERFACE_SIMPLE_PRESENCE in conn.interfaces:
+ self._conn[CONNECTION_INTERFACE_SIMPLE_PRESENCE].connect_to_signal(
+ "PresencesChanged", self._contact_presence_changed_cb)
+ self._ensure_channel()
+
+ def _ensure_channel(self):
+ """Request the channel we needed to fetch contact-list"""
+ # TODO is it ok to include only these two groups?
+ groups = ["subscribe", "publish"]
+ for group in groups:
+ requests = {
+ CHANNEL + ".ChannelType": CHANNEL_TYPE_CONTACT_LIST,
+ CHANNEL + ".TargetHandleType": HANDLE_TYPE_LIST,
+ CHANNEL + ".TargetID": group}
+ self._conn[CONNECTION_INTERFACE_REQUESTS].EnsureChannel(
+ requests,
+ reply_handler = self._ensure_channel_cb,
+ error_handler = self._error_cb)
+
+ def _ensure_channel_cb(self, is_yours, channel, properties):
+ """Got channel"""
+ logger.debug("new channel ensured: channel=%s; it-is-yours=%s" %
+ (channel, is_yours))
+ channel = telepathy.client.Channel(
+ service_name = self._conn.service_name,
+ object_path = channel)
+ DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties'
+ channel[DBUS_PROPERTIES].Get(
+ CHANNEL_INTERFACE_GROUP,
+ 'Members',
+ reply_handler = self._request_contact_info,
+ error_handler = self._error_cb)
+
+ def _request_contact_info(self, handles):
+ """Request contact-info for a list of contact-handles"""
+ logger.debug("Requesting contact-info for handles: %s" %
+ dbus_int_list_to_string(handles))
+ interfaces = [CONNECTION,
+ CONNECTION_INTERFACE_ALIASING,
+ CONNECTION_INTERFACE_SIMPLE_PRESENCE]
+ self._conn[CONNECTION_INTERFACE_CONTACTS].GetContactAttributes(
+ handles,
+ interfaces,
+ False,
+ reply_handler = self._get_contact_attributes_cb,
+ error_handler = self._error_cb)
+
+ def _get_contact_attributes_cb(self, attributes):
+ logger.debug("Got contact information")
+ for handle, member in attributes.iteritems():
+ contact_info = self._parse_member_attributes(member)
+ contact = _Contact(*contact_info)
+ if handle not in self._contact_list:
+ self._add_contact(handle, contact)
+
+ def _parse_member_attributes(self, member):
+ """Parse contact information out from dbus.Struct member"""
+ contact_id, alias, presence = None, None, None
+ # TODO won't these be in a certain order?
+ for key, value in member.iteritems():
+ if key == CONNECTION + '/contact-id':
+ contact_id = value
+ elif key == CONNECTION_INTERFACE_ALIASING + '/alias':
+ alias = value
+ elif key == CONNECTION_INTERFACE_SIMPLE_PRESENCE + '/presence':
+ presence = value
+
+ # TODO Can they be None?
+ assert contact_id is not None
+ assert presence is not None
+ assert alias is not None
+ return (contact_id, alias, presence)
+
+ def _add_contact(self, handle, contact):
+ self._contact_list[handle] = contact
+ self._list_store.add_contact(self._conn, handle, contact)
+
+ def _contact_presence_changed_cb(self, presences):
+ for handle, presence in presences.iteritems():
+ logger.debug("presence changed: %s->%s" %
+ (self._contact_list[handle].alias, presence[1]))
+ if handle in self._contact_list:
+ self._update_contact_presence(handle, presence)
+ else:
+ # request its info and consequently add it to the list
+ self._request_contact_info([handle])
+
+ def _update_contact_presence(self, handle, presence):
+ logger.debug("update presence of %s" % self._contact_list[handle].alias)
+ self._contact_list[handle].presence = presence
+ self._list_store.update_contact_presence(self._conn, handle, presence)
+
+ def _error_cb(self, *args):
+ logger.error("Error happends: %s" % args)
+
+
+class ContactListStore(gtk.ListStore):
+ """ListStore that contains contact list from (all connections of) telepathy
+ """
+
+ # indices of columns that are of interest to display
+ COLUMN_CONTACT = 2
+
+ def __init__(self):
+ # row format: <Connection>, <handle>, <Contact>
+ gtk.ListStore.__init__(self, object, int, object)
+
+ # dict of {connection->ContactList}
+ self._contact_lists = {}
+
+ connections = self._get_telepathy_connections()
+ # TODO: what if new connection is available?
+ self._watch_contacts(connections)
+
+ def _get_telepathy_connections(self):
+ """Return telepathy connections on dbus
+
+ Use a separate method for the sake of easy test"""
+ return telepathy.client.Connection.get_connections()
+
+ def _watch_contacts(self, connections):
+ if not connections:
+ logger.debug("No telepathy connections available")
+ return
+
+ logger.debug("Got %s telepathy connections" % len(connections))
+
+ for conn in connections:
+ self._contact_lists[conn] = ContactList(self, conn)
+
+ def add_contact(self, conn, handle, contact):
+ """Add a row to ListStore.
+
+ conn and handle are used to uniquely refer to a certain row/contact"""
+ logger.debug("adding to ListStore: handle-%s, alias-%s" %
+ (handle, contact.alias))
+ self.append((conn, handle, contact))
+
+ def update_contact_presence(self, conn, handle, presence):
+ # TODO: bruteforce
+ for row in self:
+ if (row[0], row[1]) == (conn, handle):
+ self[row.iter][self.COLUMN_CONTACT].presence = presence
+ self.row_changed(row.path, row.iter)
+
+
+class ContactSelector(gtk.ComboBox):
+ """A ComboBox that display a list of contact"""
+
+ def __init__(self):
+ list_store = ContactListStore()
+ online_model_filter = list_store.filter_new()
+ online_model_filter.set_visible_func(self._filter_online_contacts)
+
+ gtk.ComboBox.__init__(self, online_model_filter)
+ self._setup_view()
+
+ def _filter_online_contacts(self, model, treeiter):
+ online_statues = [
+ CONNECTION_PRESENCE_TYPE_AVAILABLE,
+ CONNECTION_PRESENCE_TYPE_AWAY,
+ CONNECTION_PRESENCE_TYPE_BUSY,
+ CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY]
+ contact = self._get_contact(model, treeiter)
+ if contact:
+ return contact.presence[0] in online_statues
+ else:
+ return False
+
+ def _setup_view(self):
+ """Setup the CellRenderers to display
+
+ 1st entry is status icon,
+ 2nd entry is contact alias.
+ """
+ status_icon_pixbuf = gtk.CellRendererPixbuf()
+ self.pack_start(status_icon_pixbuf, expand=False)
+ self.set_cell_data_func(status_icon_pixbuf, self._draw_status_icon)
+
+ contact_alias = gtk.CellRendererText()
+ self.pack_start(contact_alias)
+ self.set_cell_data_func(contact_alias, self._draw_contact_alias)
+
+ def _draw_status_icon(self, celllayout, cell, model, treeiter):
+ # TODO: proper way to get status-icon
+ contact = self._get_contact(model, treeiter)
+ if contact:
+ icon_file = os.path.join(defaults.IMAGE_DIR, "%s-16x16.png" %
+ self._get_status_str(contact.presence))
+ status_icon_pb = gtk.gdk.pixbuf_new_from_file(icon_file)
+ cell.set_property('pixbuf', status_icon_pb)
+
+ def _get_status_str(self, presence):
+ status_strings = {
+ CONNECTION_PRESENCE_TYPE_AVAILABLE: "available",
+ CONNECTION_PRESENCE_TYPE_AWAY: "away",
+ CONNECTION_PRESENCE_TYPE_BUSY: "busy",
+ CONNECTION_PRESENCE_TYPE_EXTENDED_AWAY: "extended-away"}
+ # These should be all the possible statues, as in _filter_online_contact
+ # TODO: but can we receive something unkown?
+ # Maybe we can provide an icon for unknow status?
+ assert presence[0] in status_strings
+ return status_strings[presence[0]]
+
+ def _draw_contact_alias(self, celllayout, cell, model, treeiter):
+ contact = self._get_contact(model, treeiter)
+ if contact:
+ cell.set_property('text', contact.alias)
+
+ def _get_contact(self, model, treeiter):
+ contact_column = ContactListStore.COLUMN_CONTACT
+ return model[treeiter][contact_column] # TODO: why contact can be None?
+
+ def dup_selected(self):
+ """Should return something that can be used to reference the contact
+ later"""
+ active_row = self.get_active()
+ if active_row == -1:
+ return None
+ else:
+ return self.get_model()[active_row]
+
+
+def contact_selector_run(parent=None):
+ out = None
+
+ builder = gtk.Builder()
+ ui_file = os.path.join(defaults.UI_DIR, 'contact_selector_dialog.ui')
+ builder.add_from_file(ui_file)
+ dialog = builder.get_object("contact_selector_dialog")
+
+ if parent:
+ dialog.set_transient_for(parent)
+
+ selector = ContactSelector()
+ selector.show()
+
+ dialog.vbox.pack_start(selector)
+ response = dialog.run()
+ if response == gtk.RESPONSE_OK:
+ out = selector.dup_selected()
+ dialog.destroy()
+
+ return out
+
+
+if __name__ == "__main__":
+ out = contact_selector_run()
+ if out:
+ print "selected: %s\nconnection: %s\nhandle: %d" % (
+ out[2].alias, out[0].service_name, out[1])
+ else:
+ print "None selected"
diff --git a/gnome-sudoku/src/lib/main.py b/gnome-sudoku/src/lib/main.py
index e325a0d..c6e7cf6 100644
--- a/gnome-sudoku/src/lib/main.py
+++ b/gnome-sudoku/src/lib/main.py
@@ -25,6 +25,7 @@ from defaults import (APPNAME, APPNAME_SHORT, AUTHORS, COPYRIGHT, DESCRIPTION,
from gtk_goodies import gconf_wrapper, Undo, dialog_extras
from simple_debug import simple_debug, options
+import contact_selector
import tp_tube
ICON_FACTORY = gtk.IconFactory()
@@ -250,7 +251,7 @@ class UI (gconf_wrapper.GConfWrapper):
('New', gtk.STOCK_NEW, None,
'<Control>n', _('New game'), self.new_cb),
('NewWithContact', gtk.STOCK_NEW, _('New with contact'),
- None, _('New with contact'), self._new_with_contact),
+ None, _('New with contact'), self._new_with_contact_cb),
('Print', gtk.STOCK_PRINT, None,
None, _('Print current game'), self.print_game),
('PrintMany', gtk.STOCK_PRINT, _('Print _Multiple Sudokus'),
@@ -465,8 +466,13 @@ class UI (gconf_wrapper.GConfWrapper):
if choice:
self._open_game(choice[0], choice[1])
- def _new_with_contact(self, *args):
- pass
+ def _new_with_contact_cb(self, action):
+ choice = contact_selector.contact_selector_run(self.w)
+ if choice:
+ print "selected: %s\nconnection: %s\nhandle: %d" % (
+ choice[2].alias, choice[0].service_name, choice[1])
+ else:
+ print "None selected"
@simple_debug
def _close_current_game (self):
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]