[gnome-games/sudoku-tube] Add contact selector



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]