Mercurial > pidgin
changeset 18866:e7314d58ebe6
merge of '568907d26b94a41acc8768523cdc469bdf385d2c'
and '90af3f4273c546393e7066ee5f281a8417cb3876'
author | Will Thompson <will.thompson@collabora.co.uk> |
---|---|
date | Fri, 10 Aug 2007 17:45:05 +0000 |
parents | 7a594763c229 (current diff) 5b27ae2413b7 (diff) |
children | 1d96cfd8879f |
files | |
diffstat | 40 files changed, 1984 insertions(+), 341 deletions(-) [+] |
line wrap: on
line diff
--- a/ChangeLog Fri Aug 10 17:30:59 2007 +0000 +++ b/ChangeLog Fri Aug 10 17:45:05 2007 +0000 @@ -5,10 +5,15 @@ * Added an account action to open your inbox in the yahoo prpl. * Added support for Unicode status messages in Yahoo. * Server-stored aliases for Yahoo. (John Moody) + * Fixed support for Yahoo! doodling. + * Bonjour plugin uses native Avahi instead of Howl + * Bonjour plugin supports Buddy Icons Pidgin: * Show current outgoing conversation formatting on the font label on the toolbar + * Slim new redesign of conversation tabs to maximize number of + conversations that can fit in a window version 2.1.0 (07/28/2007): libpurple:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/ui-ops.dox Fri Aug 10 17:45:05 2007 +0000 @@ -0,0 +1,24 @@ +/** @page ui-ops UiOps structures + + When implementing a UI for libpurple, you need to fill in various UiOps + structures: + + - #PurpleAccountUiOps + - #PurpleBlistUiOps + - #PurpleConnectionUiOps + - #PurpleConversationUiOps + - #PurpleCoreUiOps + - #PurpleDebugUiOps + - #PurpleDnsQueryUiOps + - #PurpleEventLoopUiOps + - #PurpleIdleUiOps + - #PurpleNotifyUiOps + - #PurplePrivacyUiOps + - #PurpleRequestUiOps + - #PurpleRoomlistUiOps + - #PurpleSoundUiOps + - #PurpleWhiteboardUiOps + - #PurpleXferUiOps + + */ +// vim: ft=c.doxygen
--- a/finch/libgnt/gntbindable.c Fri Aug 10 17:30:59 2007 +0000 +++ b/finch/libgnt/gntbindable.c Fri Aug 10 17:45:05 2007 +0000 @@ -184,7 +184,7 @@ action = g_hash_table_lookup(klass->actions, name); if (!action) { - g_printerr("GntWidget: Invalid action name %s for %s\n", + g_printerr("GntBindable: Invalid action name %s for %s\n", name, g_type_name(G_OBJECT_CLASS_TYPE(klass))); if (list) g_list_free(list);
--- a/finch/libgnt/gntmain.c Fri Aug 10 17:30:59 2007 +0000 +++ b/finch/libgnt/gntmain.c Fri Aug 10 17:45:05 2007 +0000 @@ -517,7 +517,8 @@ void gnt_screen_release(GntWidget *widget) { - gnt_wm_window_close(wm, widget); + if (wm) + gnt_wm_window_close(wm, widget); } void gnt_screen_update(GntWidget *widget) @@ -564,7 +565,9 @@ void gnt_quit() { - g_hash_table_destroy(wm->nodes); /* XXX: */ + g_object_unref(G_OBJECT(wm)); + wm = NULL; + update_panels(); doupdate(); gnt_uninit_colors();
--- a/finch/libgnt/gnttextview.c Fri Aug 10 17:30:59 2007 +0000 +++ b/finch/libgnt/gnttextview.c Fri Aug 10 17:45:05 2007 +0000 @@ -68,17 +68,31 @@ int i = 0; GList *lines; int rows, scrcol; + int comp = 0; /* Used for top-aligned text */ gboolean has_scroll = !(view->flags & GNT_TEXT_VIEW_NO_SCROLL); wbkgd(widget->window, COLOR_PAIR(GNT_COLOR_NORMAL)); werase(widget->window); + if ((view->flags & GNT_TEXT_VIEW_TOP_ALIGN) && + g_list_length(view->list) < widget->priv.height) { + GList *now = view->list; + comp = widget->priv.height - g_list_length(view->list); + view->list = g_list_nth_prev(view->list, comp); + if (!view->list) { + view->list = g_list_first(now); + comp = widget->priv.height - g_list_length(view->list); + } else { + comp = 0; + } + } + for (i = 0, lines = view->list; i < widget->priv.height && lines; i++, lines = lines->next) { GList *iter; GntTextLine *line = lines->data; - wmove(widget->window, widget->priv.height - 1 - i, 0); + wmove(widget->window, widget->priv.height - 1 - i - comp, 0); for (iter = line->segments; iter; iter = iter->next) { @@ -398,7 +412,7 @@ static void gnt_text_view_size_changed(GntWidget *widget, int w, int h) { - if (w != widget->priv.width) { + if (w != widget->priv.width && GNT_WIDGET_IS_FLAG_SET(widget, GNT_WIDGET_MAPPED)) { gnt_text_view_reflow(GNT_TEXT_VIEW(widget)); } } @@ -422,11 +436,16 @@ gnt_text_view_init(GTypeInstance *instance, gpointer class) { GntWidget *widget = GNT_WIDGET(instance); - - GNT_WIDGET_SET_FLAGS(GNT_WIDGET(instance), GNT_WIDGET_GROW_Y | GNT_WIDGET_GROW_X); + GntTextView *view = GNT_TEXT_VIEW(widget); + GntTextLine *line = g_new0(GntTextLine, 1); + GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_NO_BORDER | GNT_WIDGET_NO_SHADOW | + GNT_WIDGET_GROW_Y | GNT_WIDGET_GROW_X); widget->priv.minw = 5; widget->priv.minh = 2; + view->string = g_string_new(NULL); + view->list = g_list_append(view->list, line); + GNTDEBUG; } @@ -464,13 +483,6 @@ GntWidget *gnt_text_view_new() { GntWidget *widget = g_object_new(GNT_TYPE_TEXT_VIEW, NULL); - GntTextView *view = GNT_TEXT_VIEW(widget); - GntTextLine *line = g_new0(GntTextLine, 1); - - GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_NO_BORDER | GNT_WIDGET_NO_SHADOW); - - view->string = g_string_new(NULL); - view->list = g_list_append(view->list, line); return widget; }
--- a/finch/libgnt/gnttextview.h Fri Aug 10 17:30:59 2007 +0000 +++ b/finch/libgnt/gnttextview.h Fri Aug 10 17:45:05 2007 +0000 @@ -47,9 +47,11 @@ typedef struct _GntTextViewPriv GntTextViewPriv; typedef struct _GntTextViewClass GntTextViewClass; -typedef enum _GntTextViewFlag { +typedef enum +{ GNT_TEXT_VIEW_NO_SCROLL = 1 << 0, GNT_TEXT_VIEW_WRAP_CHAR = 1 << 1, + GNT_TEXT_VIEW_TOP_ALIGN = 1 << 2, } GntTextViewFlag; struct _GntTextView
--- a/finch/libgnt/gnttree.c Fri Aug 10 17:30:59 2007 +0000 +++ b/finch/libgnt/gnttree.c Fri Aug 10 17:45:05 2007 +0000 @@ -450,7 +450,7 @@ if (COLUMN_INVISIBLE(tree, i)) { continue; } - mvwaddstr(widget->window, pos, x + 1, tree->columns[i].title); + mvwaddnstr(widget->window, pos, x + (x != pos), tree->columns[i].title, tree->columns[i].width); NEXT_X; } if (pos) @@ -768,6 +768,7 @@ g_string_free(tree->priv->search, TRUE); tree->priv->search = NULL; tree->priv->search_timeout = 0; + GNT_WIDGET_UNSET_FLAGS(GNT_WIDGET(tree), GNT_WIDGET_DISABLE_ACTIONS); } } @@ -1053,7 +1054,9 @@ GntTree *tree = GNT_TREE(widget); tree->show_separator = TRUE; tree->priv = g_new0(GntTreePriv, 1); - GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_GROW_X | GNT_WIDGET_GROW_Y | GNT_WIDGET_CAN_TAKE_FOCUS); + GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_GROW_X | GNT_WIDGET_GROW_Y | + GNT_WIDGET_CAN_TAKE_FOCUS | GNT_WIDGET_NO_SHADOW); + gnt_widget_set_take_focus(widget, TRUE); widget->priv.minw = 4; widget->priv.minh = 1; GNTDEBUG; @@ -1605,9 +1608,6 @@ "columns", col, NULL); - GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_NO_SHADOW); - gnt_widget_set_take_focus(widget, TRUE); - return widget; }
--- a/finch/libgnt/gntwm.c Fri Aug 10 17:30:59 2007 +0000 +++ b/finch/libgnt/gntwm.c Fri Aug 10 17:45:05 2007 +0000 @@ -684,6 +684,8 @@ {'j', "┘"}, {'a', "▒"}, {'n', "┼"}, + {'w', "┬"}, + {'v', "┴"}, {'\0', NULL} }; @@ -1137,9 +1139,37 @@ } static void +accumulate_windows(gpointer window, gpointer node, gpointer p) +{ + GList *list = *(GList**)p; + list = g_list_prepend(list, window); + *(GList**)p = list; +} + +static void +gnt_wm_destroy(GObject *obj) +{ + GntWM *wm = GNT_WM(obj); + GList *list = NULL; + g_hash_table_foreach(wm->nodes, accumulate_windows, &list); + g_list_foreach(list, (GFunc)gnt_widget_destroy, NULL); + g_list_free(list); + g_hash_table_destroy(wm->nodes); + wm->nodes = NULL; + + while (wm->workspaces) { + g_object_unref(wm->workspaces->data); + wm->workspaces = g_list_delete_link(wm->workspaces, wm->workspaces); + } +} + +static void gnt_wm_class_init(GntWMClass *klass) { int i; + GObjectClass *gclass = G_OBJECT_CLASS(klass); + + gclass->dispose = gnt_wm_destroy; klass->new_window = gnt_wm_new_window_real; klass->decorate_window = NULL;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/finch/libgnt/pygnt/Makefile.am Fri Aug 10 17:45:05 2007 +0000 @@ -0,0 +1,40 @@ +EXTRA_DIST = gendef.sh + +pg_LTLIBRARIES = gnt.la + +pgdir = $(libdir) + +sources = \ + gnt.def \ + gnt.override \ + gntbox.override \ + gntfilesel.override \ + gnttree.override \ + gntwidget.override + +gnt_la_SOURCES = gnt.c common.c common.h gntmodule.c + +gnt_la_LDFLAGS = -module -avoid-version \ + `pkg-config --libs pygobject-2.0` + +gnt_la_LIBADD = \ + $(GLIB_LIBS) \ + ../libgnt.la + +AM_CPPFLAGS = \ + -I../ \ + $(GLIB_CFLAGS) \ + $(GNT_CFLAGS) \ + -I/usr/include/python2.4 \ + `pkg-config --cflags pygobject-2.0` + +CLEANFILES = gnt.def gnt.c gnt.defe + +gnt.def: $(srcdir)/../*.h + $(srcdir)/gendef.sh + +gnt.c: $(sources) + pygtk-codegen-2.0 --prefix gnt \ + --override gnt.override \ + gnt.def > $@ +
--- a/finch/libgnt/pygnt/Makefile.make Fri Aug 10 17:30:59 2007 +0000 +++ b/finch/libgnt/pygnt/Makefile.make Fri Aug 10 17:45:05 2007 +0000 @@ -10,5 +10,7 @@ --override gnt.override \ gnt.def > $@ +#python codegen/codegen.py --prefix gnt \ + clean: @rm *.so *.o gnt.c
--- a/finch/libgnt/pygnt/dbus-gnt Fri Aug 10 17:30:59 2007 +0000 +++ b/finch/libgnt/pygnt/dbus-gnt Fri Aug 10 17:45:05 2007 +0000 @@ -17,13 +17,14 @@ convwins = {} -def buddysignedon(): +def buddysignedon(buddy): pass def conv_closed(conv): key = get_dict_key(conv) stuff = convwins[key] stuff[0].destroy() + # if a conv window is closed, then reopened, this thing crashes convwins[key] = None def wrote_msg(account, who, msg, conv, flags): @@ -31,10 +32,13 @@ tv = stuff[1] tv.append_text_with_flags("\n", 0) tv.append_text_with_flags(strftime("(%X) "), 8) - tv.append_text_with_flags(who + ": ", 1) - tv.append_text_with_flags(msg, 0) + if flags & 3: + tv.append_text_with_flags(who + ": ", 1) + tv.append_text_with_flags(msg, 0) + stuff[0].set_urgent() + else: + tv.append_text_with_flags(msg, 8) tv.scroll(0) - stuff[0].set_urgent() bus = dbus.SessionBus() obj = bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject") @@ -72,6 +76,9 @@ purple.PurpleConvChatSend(chatdata, entry.get_text()) entry.clear() +def conv_window_destroyed(win, key): + del convwins[key] + def show_conversation(conv): key = get_dict_key(conv) if key in convwins: @@ -91,27 +98,28 @@ tv.clear() win.show() convwins[key] = [win, tv, entry] + win.connect("destroy", conv_window_destroyed, key) return convwins[key] def show_buddylist(): - win = gnt.Window() - tree = gnt.Tree() - tree.set_property("columns", 1) - win.add_widget(tree) - node = purple.PurpleBlistGetRoot() - while node: - if purple.PurpleBlistNodeIsGroup(node): - sys.stderr.write(str(node) + "\n") - tree.add_row_after(str(node), ["asd", ""], None, None) - #tree.add_row_after(node, [str(purple.PurpleGroupGetName(node)), ""], None, None) - #tree.add_row_after(node, ["aasd", ""], None, None) - elif purple.PurpleBlistNodeIsContact(node): - buddy = purple.PurpleContactGetPriorityBuddy(node) - group = purple.PurpleBuddyGetGroup(buddy) - #tree.add_row_after(node, [str(purple.PurpleBuddyGetName(buddy)), ""], group, None) + win = gnt.Window() + tree = gnt.Tree() + tree.set_property("columns", 1) + win.add_widget(tree) + node = purple.PurpleBlistGetRoot() + while node: + if purple.PurpleBlistNodeIsGroup(node): + sys.stderr.write(str(node) + "\n") + tree.add_row_after(str(node), ["asd", ""], None, None) + #tree.add_row_after(node, [str(purple.PurpleGroupGetName(node)), ""], None, None) + #tree.add_row_after(node, ["aasd", ""], None, None) + elif purple.PurpleBlistNodeIsContact(node): + buddy = purple.PurpleContactGetPriorityBuddy(node) + group = purple.PurpleBuddyGetGroup(buddy) + #tree.add_row_after(node, [str(purple.PurpleBuddyGetName(buddy)), ""], group, None) - node = purple.PurpleBlistNodeNext(node, False) - win.show() + node = purple.PurpleBlistNodeNext(node, False) + win.show() gnt.gnt_init()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/finch/libgnt/pygnt/example/rss/gnthtml.py Fri Aug 10 17:45:05 2007 +0000 @@ -0,0 +1,87 @@ +#!/usr/bin/env python + +""" +gr - An RSS-reader built using libgnt and feedparser. + +Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im> + +This application is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This application 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this application; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +USA +""" + +""" +This file defines GParser, which is a simple HTML parser to display HTML +in a GntTextView nicely. +""" + +import sgmllib +import gnt + +class GParser(sgmllib.SGMLParser): + def __init__(self, view): + sgmllib.SGMLParser.__init__(self, False) + self.link = None + self.view = view + self.flag = gnt.TEXT_FLAG_NORMAL + + def parse(self, s): + self.feed(s) + self.close() + + def unknown_starttag(self, tag, attrs): + if tag in ["b", "i", "blockquote", "strong"]: + self.flag = self.flag | gnt.TEXT_FLAG_BOLD + elif tag in ["p", "hr", "br"]: + self.view.append_text_with_flags("\n", self.flag) + else: + print tag + + def unknown_endtag(self, tag): + if tag in ["b", "i", "blockquote", "strong"]: + self.flag = self.flag & ~gnt.TEXT_FLAG_BOLD + elif tag in ["p", "hr", "br"]: + self.view.append_text_with_flags("\n", self.flag) + else: + print tag + + def start_u(self, attrs): + self.flag = self.flag | gnt.TEXT_FLAG_UNDERLINE + + def end_u(self): + self.flag = self.flag & ~gnt.TEXT_FLAG_UNDERLINE + + def start_a(self, attributes): + for name, value in attributes: + if name == "href": + self.link = value + + def do_img(self, attrs): + for name, value in attrs: + if name == 'src': + self.view.append_text_with_flags("[img:" + value + "]", self.flag) + + def end_a(self): + if not self.link: + return + self.view.append_text_with_flags(" (", self.flag) + self.view.append_text_with_flags(self.link, self.flag | gnt.TEXT_FLAG_UNDERLINE) + self.view.append_text_with_flags(")", self.flag) + self.link = None + + def handle_data(self, data): + if len(data.strip()) == 0: + return + self.view.append_text_with_flags(data, self.flag) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/finch/libgnt/pygnt/example/rss/gntrss-ui.py Fri Aug 10 17:45:05 2007 +0000 @@ -0,0 +1,399 @@ +#!/usr/bin/env python + +""" +gr - An RSS-reader built using libgnt and feedparser. + +Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im> + +This application is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This application 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this application; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +USA +""" + +""" +This file deals with the UI part (gnt) of the application + +TODO: + - Allow showing feeds of only selected 'category' and/or 'priority'. A different + window should be used to change such filtering. + - Display details of each item in its own window. + - Add search capability, and allow searching only in title/body. Also allow + filtering in the search results. + - Show the data and time for feed items (probably in a separate column .. perhaps not) + - Have a simple way to add a feed. + - Allow renaming a feed. +""" + +import gntrss +import gnthtml +import gnt +import gobject +import sys + +__version__ = "0.0.1alpha" +__author__ = "Sadrul Habib Chowdhury (sadrul@pidgin.im)" +__copyright__ = "Copyright 2007, Sadrul Habib Chowdhury" +__license__ = "GPL" # see full license statement above + +gnt.gnt_init() + +class RssTree(gnt.Tree): + __gsignals__ = { + 'active_changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)) + } + + __gntbindings__ = { + 'jump-next-unread' : ('jump_next_unread', 'J') + } + + def jump_next_unread(self, null): + first = None + next = None + all = self.get_rows() + for item in all: + if item.unread: + if next: + first = item + break + elif not first and self.active != item: + first = item + if self.active == item: + next = item + if first: + self.set_active(first) + self.set_selected(first) + + def __init__(self): + self.active = None + gnt.Tree.__init__(self) + gnt.set_flag(self, 8) # remove borders + self.connect('key_pressed', self.do_key_pressed) + + def set_active(self, active): + if self.active == active: + return + if self.active: + flag = gnt.TEXT_FLAG_NORMAL + if self.active.unread: + flag = flag | gnt.TEXT_FLAG_BOLD + self.set_row_flags(self.active, flag) + old = self.active + self.active = active + flag = gnt.TEXT_FLAG_UNDERLINE + if self.active.unread: + flag = flag | gnt.TEXT_FLAG_BOLD + self.set_row_flags(self.active, flag) + self.emit('active_changed', old) + + def do_key_pressed(self, null, text): + if text == '\r': + now = self.get_selection_data() + self.set_active(now) + return True + return False + +gobject.type_register(RssTree) +gnt.register_bindings(RssTree) + +win = gnt.Box(homo = False, vert = True) +win.set_toplevel(True) +win.set_title("GntRss") +win.set_pad(0) + +# +# [[[ Generic feed/item callbacks +# +def feed_item_added(feed, item): + add_feed_item(item) + +def add_feed(feed): + if not feed.get_data('gntrss-connected'): + feed.connect('added', feed_item_added) + feed.connect('notify', update_feed_title) + feed.set_data('gntrss-connected', True) + feeds.add_row_after(feed, [feed.title, str(feed.unread)], None, None) + +def remove_item(item, feed): + items.remove(item) + +def update_feed_item(item, property): + if property.name == 'unread': + if feeds.active == item.parent: + flag = 0 + if item == items.active: + flag = gnt.TEXT_FLAG_UNDERLINE + if item.unread: + flag = flag | gnt.TEXT_FLAG_BOLD + else: + flag = flag | gnt.TEXT_FLAG_NORMAL + items.set_row_flags(item, flag) + + unread = item.parent.unread + if item.unread: + unread = unread + 1 + else: + unread = unread - 1 + item.parent.set_property('unread', unread) + +def add_feed_item(item): + currentfeed = feeds.active + if item.parent != currentfeed: + return + months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + dt = str(item.date_parsed[2]) + "." + months[item.date_parsed[1]] + "." + str(item.date_parsed[0]) + items.add_row_after(item, [str(item.title), dt], None, None) + if item.unread: + items.set_row_flags(item, gnt.TEXT_FLAG_BOLD) + if not item.get_data('gntrss-connected'): + item.set_data('gntrss-connected', True) + # this needs to happen *without* having to add the item in the tree + item.connect('notify', update_feed_item) + item.connect('delete', remove_item) + +# +# ]]] Generic feed/item callbacks +# + + +#### +# [[[ The list of feeds +### + +# 'Add Feed' dialog +add_feed_win = None +def add_feed_win_closed(win): + global add_feed_win + add_feed_win = None + +def add_new_feed(): + global add_feed_win + + if add_feed_win: + gnt.gnt_window_present(add_feed_win) + return + win = gnt.Window() + win.set_title("New Feed") + + box = gnt.Box(False, False) + label = gnt.Label("Link") + box.add_widget(label) + entry = gnt.Entry("") + entry.set_size(40, 1) + box.add_widget(entry) + + win.add_widget(box) + win.show() + add_feed_win = win + add_feed_win.connect("destroy", add_feed_win_closed) + +# +# The active row in the feed-list has changed. Update the feed-item table. +def feed_active_changed(tree, old): + items.remove_all() + if not tree.active: + return + update_items_title() + for item in tree.active.items: + add_feed_item(item) + win.give_focus_to_child(items) + +# +# Check for the action keys and decide how to deal with them. +def feed_key_pressed(tree, text): + if tree.is_searching(): + return + if text == 'r': + feed = tree.get_selection_data() + tree.perform_action_key('j') + #tree.perform_action('move-down') + feed.refresh() + elif text == 'R': + feeds = tree.get_rows() + for feed in feeds: + feed.refresh() + elif text == 'm': + feed = tree.get_selection_data() + if feed: + feed.mark_read() + feed.set_property('unread', 0) + elif text == 'a': + add_new_feed() + else: + return False + return True + +feeds = RssTree() +feeds.set_property('columns', 2) +feeds.set_col_width(0, 20) +feeds.set_col_width(1, 6) +feeds.set_column_resizable(0, False) +feeds.set_column_resizable(1, False) +feeds.set_column_is_right_aligned(1, True) +feeds.set_show_separator(False) +feeds.set_column_title(0, "Feeds") +feeds.set_show_title(True) + +feeds.connect('active_changed', feed_active_changed) +feeds.connect('key_pressed', feed_key_pressed) +gnt.unset_flag(feeds, 256) # Fix the width + +#### +# ]]] The list of feeds +### + +#### +# [[[ The list of items in the feed +#### + +# +# The active item in the feed-item list has changed. Update the +# summary content. +def item_active_changed(tree, old): + details.clear() + if not tree.active: + return + item = tree.active + details.append_text_with_flags(str(item.title) + "\n", gnt.TEXT_FLAG_BOLD) + details.append_text_with_flags("Link: ", gnt.TEXT_FLAG_BOLD) + details.append_text_with_flags(str(item.link) + "\n", gnt.TEXT_FLAG_UNDERLINE) + details.append_text_with_flags("Date: ", gnt.TEXT_FLAG_BOLD) + details.append_text_with_flags(str(item.date) + "\n", gnt.TEXT_FLAG_NORMAL) + details.append_text_with_flags("\n", gnt.TEXT_FLAG_NORMAL) + parser = gnthtml.GParser(details) + parser.parse(str(item.summary)) + item.mark_unread(False) + + if old and old.unread: # If the last selected item is marked 'unread', then make sure it's bold + items.set_row_flags(old, gnt.TEXT_FLAG_BOLD) + +# +# Look for action keys in the feed-item list. +def item_key_pressed(tree, text): + if tree.is_searching(): + return + current = tree.get_selection_data() + if text == 'M': # Mark all of the items 'read' + feed = feeds.active + if feed: + feed.mark_read() + elif text == 'm': # Mark the current item 'read' + current.mark_unread(False) + tree.perform_action_key('j') + elif text == 'U': # Mark the current item 'unread' + current.mark_unread(True) + elif text == 'd': + current.remove() + tree.perform_action_key('j') + else: + return False + return True + +items = RssTree() +items.set_property('columns', 2) +items.set_col_width(0, 40) +items.set_col_width(1, 11) +items.set_column_resizable(1, False) +items.set_column_title(0, "Items") +items.set_column_title(1, "Date") +items.set_show_title(True) +items.connect('key_pressed', item_key_pressed) +items.connect('active_changed', item_active_changed) + +#### +# ]]] The list of items in the feed +#### + +# +# Update the title of the items list depending on the selection in the feed list +def update_items_title(): + feed = feeds.active + if feed: + items.set_column_title(0, str(feed.title) + ": " + str(feed.unread) + "(" + str(len(feed.items)) + ")") + else: + items.set_column_title(0, "Items") + items.draw() + +# The container on the top +line = gnt.Line(vertical = False) + +# The textview to show the details of a feed +details = gnt.TextView() +details.set_take_focus(True) +details.set_flag(gnt.TEXT_VIEW_TOP_ALIGN) +details.attach_scroll_widget(details) + +# Make it look nice +s = feeds.get_size() +size = gnt.screen_size() +size[0] = size[0] - s[0] +items.set_size(size[0], size[1] / 2) +details.set_size(size[0], size[1] / 2) + +# Category tree +cat = gnt.Tree() +cat.set_property('columns', 1) +cat.set_column_title(0, 'Category') +cat.set_show_title(True) +gnt.set_flag(cat, 8) # remove borders + +box = gnt.Box(homo = False, vert = False) +box.set_pad(0) + +vbox = gnt.Box(homo = False, vert = True) +vbox.set_pad(0) +vbox.add_widget(feeds) +vbox.add_widget(gnt.Line(False)) +vbox.add_widget(cat) +box.add_widget(vbox) + +box.add_widget(gnt.Line(True)) + +vbox = gnt.Box(homo = False, vert = True) +vbox.set_pad(0) +vbox.add_widget(items) +vbox.add_widget(gnt.Line(False)) +vbox.add_widget(details) +box.add_widget(vbox) + +win.add_widget(box) +win.show() + +def update_feed_title(feed, property): + if property.name == 'title': + if feed.customtitle: + title = feed.customtitle + else: + title = feed.title + feeds.change_text(feed, 0, title) + elif property.name == 'unread': + feeds.change_text(feed, 1, str(feed.unread) + "(" + str(len(feed.items)) + ")") + flag = 0 + if feeds.active == feed: + flag = gnt.TEXT_FLAG_UNDERLINE + update_items_title() + if feed.unread > 0: + flag = flag | gnt.TEXT_FLAG_BOLD + feeds.set_row_flags(feed, flag) + +# populate everything +for feed in gntrss.feeds: + feed.refresh() + feed.set_auto_refresh(True) + add_feed(feed) + +gnt.gnt_register_action("Stuff", add_new_feed) +gnt.gnt_main() + +gnt.gnt_quit() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/finch/libgnt/pygnt/example/rss/gntrss.py Fri Aug 10 17:45:05 2007 +0000 @@ -0,0 +1,250 @@ +#!/usr/bin/env python + +""" +gr - An RSS-reader built using libgnt and feedparser. + +Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im> + +This application is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This application 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this application; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +USA +""" + +""" +This file deals with the rss parsing part (feedparser) of the application +""" + +import os +import tempfile, urllib2 +import feedparser +import gobject +import sys +import time + +## +# The FeedItem class. It will update emit 'delete' signal when it's +# destroyed. +## +class FeedItem(gobject.GObject): + __gproperties__ = { + 'unread' : (gobject.TYPE_BOOLEAN, 'read', + 'The unread state of the item.', + False, gobject.PARAM_READWRITE) + } + __gsignals__ = { + 'delete' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)) + } + def __init__(self, item, parent): + self.__gobject_init__() + try: + "Apparently some feed items don't have any dates in them" + self.date = item['date'] + self.date_parsed = item['date_parsed'] + except: + item['date'] = self.date = time.ctime() + self.date_parsed = feedparser._parse_date(self.date) + + self.title = item['title'] + sum = item['summary'] + self.summary = item['summary'].encode('utf8') + self.link = item['link'] + self.parent = parent + self.unread = True + + def remove(self): + self.emit('delete', self.parent) + if self.unread: + self.parent.set_property('unread', self.parent.unread - 1) + + def do_set_property(self, property, value): + if property.name == 'unread': + self.unread = value + + def mark_unread(self, unread): + if self.unread == unread: + return + self.set_property('unread', unread) + +gobject.type_register(FeedItem) + +def item_hash(item): + return str(item['date'] + item['title']) + +""" +The Feed class. It will update the 'link', 'title', 'desc' and 'items' +attributes if/when they are updated (triggering 'notify::<attr>' signal) + +TODO: + - Add a 'count' attribute + - Each feed will have a 'uidata', which will be its display window + - Look into 'category'. Is it something that feed defines, or the user? + - Have separate refresh times for each feed. + - Have 'priority' for each feed. (somewhat like category, perhaps?) +""" +class Feed(gobject.GObject): + __gproperties__ = { + 'link' : (gobject.TYPE_STRING, 'link', + 'The web page this feed is associated with.', + '...', gobject.PARAM_READWRITE), + 'title' : (gobject.TYPE_STRING, 'title', + 'The title of the feed.', + '...', gobject.PARAM_READWRITE), + 'desc' : (gobject.TYPE_STRING, 'description', + 'The description for the feed.', + '...', gobject.PARAM_READWRITE), + 'items' : (gobject.TYPE_POINTER, 'items', + 'The items in the feed.', gobject.PARAM_READWRITE), + 'unread' : (gobject.TYPE_INT, 'unread', + 'Number of unread items in the feed.', 0, 10000, 0, gobject.PARAM_READWRITE) + } + __gsignals__ = { + 'added' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)) + } + + def __init__(self, feed): + self.__gobject_init__() + url = feed['link'] + name = feed['name'] + self.url = url # The url of the feed itself + self.link = url # The web page associated with the feed + self.desc = url + self.title = (name, url)[not name] + self.customtitle = name + self.unread = 0 + self.items = [] + self.hash = {} + self.pending = False + self._refresh = {'time' : 30, 'id' : 0} + + def do_set_property(self, property, value): + if property.name == 'link': + self.link = value + elif property.name == 'desc': + self.desc = value + elif property.name == 'title': + self.title = value + elif property.name == 'unread': + self.unread = value + pass + + def set_result(self, result): + # XXX Look at result['bozo'] first, and emit some signal that the UI can use + # to indicate (dim the row?) that the feed has invalid XML format or something + + try: + channel = result['channel'] + self.set_property('link', channel['link']) + self.set_property('desc', channel['description']) + self.set_property('title', channel['title']) + items = result['items'] + except: + items = () + + tmp = {} + for item in self.items: + tmp[hash(item)] = item + + unread = self.unread + for item in items: + try: + exist = self.hash[item_hash(item)] + del tmp[hash(exist)] + except: + itm = FeedItem(item, self) + self.items.append(itm) + self.emit('added', itm) + self.hash[item_hash(item)] = itm + unread = unread + 1 + + if unread != self.unread: + self.set_property('unread', unread) + + for hv in tmp: + tmp[hv].remove() + "Also notify the UI about the count change" + + self.pending = False + return False + + def refresh(self): + if self.pending: + return + self.pending = True + FeedReader(self).run() + return True + + def mark_read(self): + for item in self.items: + item.mark_unread(False) + + def set_auto_refresh(self, auto): + if auto: + if self._refresh['id']: + return + if self._refresh['time'] < 1: + self._refresh['time'] = 1 + self.id = gobject.timeout_add(self._refresh['time'] * 1000 * 60, self.refresh) + else: + if not self._refresh['id']: + return + gobject.source_remove(self._refresh['id']) + self._refresh['id'] = 0 + +gobject.type_register(Feed) + +""" +The FeedReader updates a Feed. It fork()s off a child to avoid blocking. +""" +class FeedReader: + def __init__(self, feed): + self.feed = feed + + def reap_child(self, pid, status): + result = feedparser.parse(self.tmpfile.name) + self.tmpfile.close() + self.feed.set_result(result) + + def run(self): + self.tmpfile = tempfile.NamedTemporaryFile() + self.pid = os.fork() + if self.pid == 0: + tmp = urllib2.urlopen(self.feed.url) + content = tmp.read() + tmp.close() + self.tmpfile.write(content) + self.tmpfile.flush() + # Do NOT close tmpfile here + os._exit(os.EX_OK) + gobject.child_watch_add(self.pid, self.reap_child) + +feeds = [] +urls = ( + {'name': '/.', + 'link': "http://rss.slashdot.org/Slashdot/slashdot"}, + {'name': 'KernelTrap', + 'link': "http://kerneltrap.org/node/feed"}, + {'name': None, + 'link': "http://pidgin.im/rss.php"}, + {'name': "F1", + 'link': "http://www.formula1.com/rss/news/latest.rss"}, + {'name': "Freshmeat", + 'link': "http://www.pheedo.com/f/freshmeatnet_announcements_unix"}, + {'name': "Cricinfo", + 'link': "http://www.cricinfo.com/rss/livescores.xml"} +) + +for url in urls: + feed = Feed(url) + feeds.append(feed) +
--- a/finch/libgnt/pygnt/gendef.sh Fri Aug 10 17:30:59 2007 +0000 +++ b/finch/libgnt/pygnt/gendef.sh Fri Aug 10 17:45:05 2007 +0000 @@ -28,7 +28,7 @@ gnt.h" # Generate the def file -rm gnt.def +rm -f gnt.def for file in $FILES do python /usr/share/pygtk/2.0/codegen/h2def.py ../$file >> gnt.def @@ -43,6 +43,7 @@ GNT_TYPE_KEY_PRESS_MODE GNT_TYPE_ENTRY_FLAG GNT_TYPE_TEXT_FORMAT_FLAGS +GNT_TYPE_TEXT_VIEW_FLAG " for enum in $ENUMS
--- a/finch/libgnt/pygnt/gnt.override Fri Aug 10 17:30:59 2007 +0000 +++ b/finch/libgnt/pygnt/gnt.override Fri Aug 10 17:45:05 2007 +0000 @@ -29,8 +29,10 @@ #include "common.h" %% include + gntbox.override gntfilesel.override gnttree.override + gntwidget.override %% modulename gnt %% @@ -39,3 +41,154 @@ ignore-glob *_get_gtype %% +define set_flag +static PyObject * +_wrap_set_flag(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = {"flags", NULL}; + PyGObject *widget; + int flags; + + if (!PyArg_ParseTuple(args, "O!i:gnt.set_flag", &PyGntWidget_Type, &widget, + &flags)) { + return NULL; + } + + GNT_WIDGET_SET_FLAGS(widget->obj, flags); + + Py_INCREF(Py_None); + return Py_None; +} +%% +define unset_flag +static PyObject * +_wrap_unset_flag(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = {"flags", NULL}; + PyGObject *widget; + int flags; + + if (!PyArg_ParseTuple(args, "O!i:gnt.unset_flag", &PyGntWidget_Type, &widget, + &flags)) { + return NULL; + } + + GNT_WIDGET_UNSET_FLAGS(widget->obj, flags); + + Py_INCREF(Py_None); + return Py_None; +} +%% +define screen_size noargs +static PyObject * +_wrap_screen_size(PyObject *self) +{ + PyObject *list = PyList_New(0); + + if (list == NULL) + return NULL; + + PyList_Append(list, PyInt_FromLong((long)getmaxx(stdscr))); + PyList_Append(list, PyInt_FromLong((long)getmaxy(stdscr))); + + return list; +} +%% +override gnt_register_action +static GHashTable *actions; + + + +static PyObject * +_wrap_gnt_register_action(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = {"name", "callback", NULL}; + PyGObject *callback; + GClosure *closure; + char *name; + + if (!PyArg_ParseTuple(args, "sO:gnt.gnt_register_action", &name, &callback)) { + return NULL; + } + + if (!PyCallable_Check(callback)) { + PyErr_SetString(PyExc_TypeError, "the callback must be callable ... doh!"); + return NULL; + } + + gnt_register_action(name, callback->obj); + + Py_INCREF(Py_None); + return Py_None; +} +%% +define register_bindings + +static gboolean +pygnt_binding_callback(GntBindable *bindable, GList *list) +{ + PyObject *wrapper = pygobject_new(G_OBJECT(bindable)); + PyObject_CallMethod(wrapper, list->data, "O", Py_None); + Py_DECREF(wrapper); + return TRUE; +} + +static PyObject * +_wrap_register_bindings(PyObject *self, PyObject *args) +{ + PyTypeObject *class; + int pos = 0; + PyObject *key, *value, *gbindings; + GntBindableClass *bindable; + + if (!PyArg_ParseTuple(args, "O!:gnt.register_bindings", + &PyType_Type, &class)) { + /* Make sure it's a GntBindableClass subclass */ + PyErr_SetString(PyExc_TypeError, + "argument must be a GntBindable subclass"); + return NULL; + } + + gbindings = PyDict_GetItemString(class->tp_dict, "__gntbindings__"); + if (!gbindings) + goto end; + + if (!PyDict_Check(gbindings)) { + PyErr_SetString(PyExc_TypeError, + "__gntbindings__ attribute not a dict!"); + return NULL; + } + + bindable = g_type_class_ref(pyg_type_from_object((PyObject *)class)); + while (PyDict_Next(gbindings, &pos, &key, &value)) { + const char *trigger, *callback, *name; + GList *list = NULL; + + if (!PyString_Check(key)) { + PyErr_SetString(PyExc_TypeError, + "__gntbindings__ keys must be strings"); + g_type_class_unref(bindable); + return NULL; + } + name = PyString_AsString(key); + + if (!PyTuple_Check(value) || + !PyArg_ParseTuple(value, "ss", &callback, &trigger)) { + PyErr_SetString(PyExc_TypeError, + "__gntbindings__ values must be (callback, trigger) tupples"); + g_type_class_unref(bindable); + return NULL; + } + + gnt_bindable_class_register_action(bindable, name, pygnt_binding_callback, + trigger, g_strdup(callback), NULL); + } + if (gbindings) + PyDict_DelItemString(class->tp_dict, "__gntbindings__"); + g_type_class_unref(bindable); + +end: + Py_INCREF(Py_None); + return Py_None; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/finch/libgnt/pygnt/gntbox.override Fri Aug 10 17:45:05 2007 +0000 @@ -0,0 +1,38 @@ +/** + * pygnt- Python bindings for the GNT toolkit. + * Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im> + * + * gntbox.override: overrides for the box widget. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +%% +override gnt_box_add_widget kwargs +static PyObject * +_wrap_gnt_box_add_widget(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = { "widget", NULL }; + PyGObject *widget; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!:GntBox.add_widget", kwlist, &PyGntWidget_Type, &widget)) + return NULL; + + gnt_box_add_widget(GNT_BOX(self->obj), GNT_WIDGET(widget->obj)); + Py_INCREF(widget); + + Py_INCREF(Py_None); + return Py_None; +}
--- a/finch/libgnt/pygnt/gntmodule.c Fri Aug 10 17:30:59 2007 +0000 +++ b/finch/libgnt/pygnt/gntmodule.c Fri Aug 10 17:45:05 2007 +0000 @@ -9,11 +9,12 @@ PyObject *m, *d; init_pygobject (); - + m = Py_InitModule ("gnt", gnt_functions); d = PyModule_GetDict (m); gnt_register_classes (d); + gnt_add_constants(m, "GNT_"); if (PyErr_Occurred ()) { Py_FatalError ("can't initialise module sad");
--- a/finch/libgnt/pygnt/gnttree.override Fri Aug 10 17:30:59 2007 +0000 +++ b/finch/libgnt/pygnt/gnttree.override Fri Aug 10 17:45:05 2007 +0000 @@ -20,7 +20,7 @@ * USA */ %% -headrs +headers #include "common.h" %% ignore @@ -49,7 +49,7 @@ return NULL; } while (list) { - PyObject *obj = pyg_pointer_new(G_TYPE_POINTER, list->data); + PyObject *obj = list->data; PyList_Append(py_list, obj); Py_DECREF(obj); list = list->next; @@ -100,4 +100,79 @@ Py_INCREF(Py_None); return Py_None; } +%% +override gnt_tree_get_selection_data noargs +static PyObject * +_wrap_gnt_tree_get_selection_data(PyGObject *self) +{ + PyObject *ret = gnt_tree_get_selection_data(GNT_TREE(self->obj)); + if (!ret) + ret = Py_None; + Py_INCREF(ret); + return ret; +} +%% +override gnt_tree_change_text +static PyObject * +_wrap_gnt_tree_change_text(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = { "key", "colno", "text", NULL }; + char *text; + int colno; + gpointer key; + if (!PyArg_ParseTupleAndKeywords(args, kwargs,"Ois:GntTree.change_text", kwlist, &key, &colno, &text)) + return NULL; + + gnt_tree_change_text(GNT_TREE(self->obj), key, colno, text); + + Py_INCREF(Py_None); + return Py_None; +} +%% +override gnt_tree_set_row_flags +static PyObject * +_wrap_gnt_tree_set_row_flags(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = { "key", "flag", NULL }; + int flag; + gpointer key; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs,"Oi:GntTree.set_row_flags", kwlist, &key, &flag)) + return NULL; + + gnt_tree_set_row_flags(GNT_TREE(self->obj), key, flag); + + Py_INCREF(Py_None); + return Py_None; +} +%% +override gnt_tree_remove +static PyObject * +_wrap_gnt_tree_remove(PyGObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = { "key", NULL }; + gpointer key; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs,"O:GntTree.remove", kwlist, &key)) + return NULL; + + gnt_tree_remove(GNT_TREE(self->obj), key); + + Py_INCREF(Py_None); + return Py_None; +} +%% +override gnt_tree_set_selected +static PyObject * +_wrap_gnt_tree_set_selected(PyGObject *self, PyObject *args) +{ + gpointer key; + if (!PyArg_ParseTuple(args, "O:GntTree.set_selected", &key)) { + return NULL; + } + gnt_tree_set_selected(GNT_TREE(self->obj), key); + Py_INCREF(Py_None); + return Py_None; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/finch/libgnt/pygnt/gntwidget.override Fri Aug 10 17:45:05 2007 +0000 @@ -0,0 +1,36 @@ +/** + * pygnt- Python bindings for the GNT toolkit. + * Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im> + * + * gntwidget.override: overrides for generic widgets. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +%% +override gnt_widget_get_size args +static PyObject * +_wrap_gnt_widget_get_size(PyGObject *self) +{ + PyObject *list = PyList_New(0); + int x = 0, y = 0; + + gnt_widget_get_size(GNT_WIDGET(self->obj), &x, &y); + PyList_Append(list, PyInt_FromLong((long)x)); + PyList_Append(list, PyInt_FromLong((long)y)); + + return list; +} +
--- a/finch/libgnt/pygnt/test.py Fri Aug 10 17:30:59 2007 +0000 +++ b/finch/libgnt/pygnt/test.py Fri Aug 10 17:45:05 2007 +0000 @@ -1,18 +1,59 @@ #!/usr/bin/python +import gobject import gnt +class MyObject(gobject.GObject): + __gproperties__ = { + 'mytype': (gobject.TYPE_INT, 'mytype', 'the type of the object', + 0, 10000, 0, gobject.PARAM_READWRITE), + 'string': (gobject.TYPE_STRING, 'string property', 'the string', + None, gobject.PARAM_READWRITE), + 'gobject': (gobject.TYPE_OBJECT, 'object property', 'the object', + gobject.PARAM_READWRITE), + } + + def __init__(self, type = 'string', value = None): + self.__gobject_init__() + self.set_property(type, value) + + def do_set_property(self, pspec, value): + if pspec.name == 'string': + self.string = value + self.type = gobject.TYPE_STRING + elif pspec.name == 'gobject': + self.gobject = value + self.type = gobject.TYPE_OBJECT + else: + raise AttributeError, 'unknown property %s' % pspec.name + def do_get_property(self, pspec): + if pspec.name == 'string': + return self.string + elif pspec.name == 'gobject': + return self.gobject + elif pspec.name == 'mytype': + return self.type + else: + raise AttributeError, 'unknown property %s' % pspec.name +gobject.type_register(MyObject) + def button_activate(button, tree): - list = tree.get_selection_text_list() - str = "" - for i in list: - str = str + i - entry.set_text("clicked!!!" + str) + list = tree.get_selection_text_list() + ent = tree.get_selection_data() + if ent.type == gobject.TYPE_STRING: + str = "" + for i in list: + str = str + i + entry.set_text("clicked!!!" + str) + elif ent.type == gobject.TYPE_OBJECT: + ent.gobject.set_text("mwhahaha!!!") gnt.gnt_init() win = gnt.Window() entry = gnt.Entry("") +obj = MyObject() +obj.set_property('gobject', entry) win.add_widget(entry) win.set_title("Entry") @@ -27,12 +68,20 @@ # so random non-string values can be used as the key for a row in a GntTree! last = None for i in range(1, 100): - tree.add_row_after(i, [str(i), ""], None, i-1) -tree.add_row_after(entry, ["asd"], None, None) -tree.add_row_after("b", ["123", ""], entry, None) + key = MyObject('string', str(i)) + tree.add_row_after(key, [str(i)], None, last) + last = key + +tree.add_row_after(MyObject('gobject', entry), ["asd"], None, None) +tree.add_row_after(MyObject('string', "b"), ["123"], MyObject('gobject', entry), None) button.connect("activate", button_activate, tree) +tv = gnt.TextView() + +win.add_widget(tv) +tv.append_text_with_flags("What up!!", gnt.TEXT_FLAG_BOLD) + win.show() gnt.gnt_main()
--- a/libpurple/connection.h Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/connection.h Fri Aug 10 17:45:05 2007 +0000 @@ -60,15 +60,52 @@ #include "plugin.h" #include "status.h" +/** Connection UI operations. Used to notify the user of changes to + * connections, such as being disconnected, and to respond to the + * underlying network connection appearing and disappearing. UIs should + * call #purple_connections_set_ui_ops() with an instance of this struct. + * + * @see @ref ui-ops + */ typedef struct { - void (*connect_progress)(PurpleConnection *gc, const char *text, - size_t step, size_t step_count); + /** When an account is connecting, this operation is called to notify + * the UI of what is happening, as well as which @a step out of @a + * step_count has been reached (which might be displayed as a progress + * bar). + */ + void (*connect_progress)(PurpleConnection *gc, + const char *text, + size_t step, + size_t step_count); + /** Called when a connection is established (just before the + * @ref signed-on signal). + */ void (*connected)(PurpleConnection *gc); + /** Called when a connection is ended (between the @ref signing-off + * and @ref signed-off signals). + */ void (*disconnected)(PurpleConnection *gc); + /** Used to display connection-specific notices. (Pidgin's Gtk user + * interface implements this as a no-op; #purple_connection_notice(), + * which uses this operation, is not used by any of the protocols + * shipped with libpurple.) + */ void (*notice)(PurpleConnection *gc, const char *text); + /** Called when an error causes a connection to be disconnected. + * Called before #disconnected. + * @param text a localized error message. + */ void (*report_disconnect)(PurpleConnection *gc, const char *text); + /** Called when libpurple discovers that the computer's network + * connection is active. On Linux, this uses Network Manager if + * available; on Windows, it uses Win32's network change notification + * infrastructure. + */ void (*network_connected)(); + /** Called when libpurple discovers that the computer's network + * connection has gone away. + */ void (*network_disconnected)(); void (*_purple_reserved1)(void);
--- a/libpurple/conversation.h Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/conversation.h Fri Aug 10 17:45:05 2007 +0000 @@ -149,27 +149,74 @@ */ struct _PurpleConversationUiOps { + /** Called when @a conv is created (but before the @ref + * conversation-created signal is emitted). + */ void (*create_conversation)(PurpleConversation *conv); + + /** Called just before @a conv is freed. */ void (*destroy_conversation)(PurpleConversation *conv); + /** Write a message to a chat. If this field is @c NULL, libpurple will + * fall back to using #write_conv. + * @see purple_conv_chat_write() + */ void (*write_chat)(PurpleConversation *conv, const char *who, const char *message, PurpleMessageFlags flags, time_t mtime); + /** Write a message to an IM conversation. If this field is @c NULL, + * libpurple will fall back to using #write_conv. + * @see purple_conv_im_write() + */ void (*write_im)(PurpleConversation *conv, const char *who, const char *message, PurpleMessageFlags flags, time_t mtime); - void (*write_conv)(PurpleConversation *conv, const char *name, const char *alias, - const char *message, PurpleMessageFlags flags, + /** Write a message to a conversation. This is used rather than + * the chat- or im-specific ops for generic messages, such as system + * messages like "x is now know as y". + * @see purple_conversation_write() + */ + void (*write_conv)(PurpleConversation *conv, + const char *name, + const char *alias, + const char *message, + PurpleMessageFlags flags, time_t mtime); - void (*chat_add_users)(PurpleConversation *conv, GList *cbuddies, gboolean new_arrivals); - + /** Add @a cbuddies to a chat. + * @param cbuddies A @C GList of #PurpleConvChatBuddy structs. + * @param new_arrivals Whether join notices should be shown. + * (Join notices are actually written to the + * conversation by #purple_conv_chat_add_users().) + */ + void (*chat_add_users)(PurpleConversation *conv, + GList *cbuddies, + gboolean new_arrivals); + /** Rename the user in this chat named @a old_name to @a new_name. (The + * rename message is written to the conversation by libpurple.) + * @param new_alias @a new_name's new alias, if they have one. + * @see purple_conv_chat_add_users() + */ void (*chat_rename_user)(PurpleConversation *conv, const char *old_name, const char *new_name, const char *new_alias); + /** Remove @a users from a chat. + * @param users A @C GList of <tt>const char *</tt>s. + * @see purple_conv_chat_rename_user() + */ void (*chat_remove_users)(PurpleConversation *conv, GList *users); + /** Called when a user's flags are changed. + * @see purple_conv_chat_user_set_flags() + */ void (*chat_update_user)(PurpleConversation *conv, const char *user); + /** Present this conversation to the user; for example, by displaying + * the IM dialog. + */ void (*present)(PurpleConversation *conv); + /** If this UI has a concept of focus (as in a windowing system) and + * this conversation has the focus, return @c TRUE; otherwise, return + * @c FALSE. + */ gboolean (*has_focus)(PurpleConversation *conv); /* Custom Smileys */ @@ -178,6 +225,11 @@ const guchar *data, gsize size); void (*custom_smiley_close)(PurpleConversation *conv, const char *smile); + /** Prompt the user for confirmation to send @a message. This function + * should arrange for the message to be sent if the user accepts. If + * this field is @c NULL, libpurple will fall back to using + * #purple_request_action(). + */ void (*send_confirm)(PurpleConversation *conv, const char *message); void (*_purple_reserved1)(void);
--- a/libpurple/log.c Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/log.c Fri Aug 10 17:45:05 2007 +0000 @@ -718,9 +718,9 @@ if (tmp < start) g_string_append_len(newmsg, tmp, start - tmp); - idstr = g_datalist_get_data(&attributes, "id"); + if ((idstr = g_datalist_get_data(&attributes, "id")) != NULL) + imgid = atoi(idstr); - imgid = atoi(idstr); if (imgid != 0) { FILE *image_file; @@ -735,6 +735,7 @@ if (image == NULL) { /* This should never happen. */ + /* This *does* happen for failed Direct-IMs -DAA */ g_string_free(newmsg, TRUE); g_return_val_if_reached((char *)msg); }
--- a/libpurple/protocols/bonjour/bonjour.c Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/protocols/bonjour/bonjour.c Fri Aug 10 17:45:05 2007 +0000 @@ -138,6 +138,8 @@ return; } + bonjour_dns_sd_update_buddy_icon(bd->dns_sd_data); + /* Create a group for bonjour buddies */ bonjour_group = purple_group_new(BONJOUR_GROUP_NAME); purple_blist_add_group(bonjour_group, NULL); @@ -283,17 +285,33 @@ bb->conversation = NULL; } +static +void bonjour_set_buddy_icon(PurpleConnection *conn, PurpleStoredImage *img) +{ + BonjourData *bd = conn->proto_data; + bonjour_dns_sd_update_buddy_icon(bd->dns_sd_data); +} + + static char * bonjour_status_text(PurpleBuddy *buddy) { - PurplePresence *presence; + const PurplePresence *presence; + const PurpleStatus *status; + const char *message; + gchar *ret = NULL; presence = purple_buddy_get_presence(buddy); + status = purple_presence_get_active_status(presence); - if (purple_presence_is_online(presence) && !purple_presence_is_available(presence)) - return g_strdup(_("Away")); + message = purple_status_get_attr_string(status, "message"); - return NULL; + if (message != NULL) { + ret = g_markup_escape_text(message, -1); + purple_util_chrreplace(ret, '\n', ' '); + } + + return ret; } static void @@ -301,6 +319,7 @@ { PurplePresence *presence; PurpleStatus *status; + BonjourBuddy *bb = buddy->proto_data; const char *status_description; const char *message; @@ -318,6 +337,23 @@ purple_notify_user_info_add_pair(user_info, _("Status"), status_description); if (message != NULL) purple_notify_user_info_add_pair(user_info, _("Message"), message); + + /* Only show first/last name if there is a nickname set (to avoid duplication) */ + if (bb->nick != NULL) { + if (bb->first != NULL) + purple_notify_user_info_add_pair(user_info, _("First name"), bb->first); + if (bb->first != NULL) + purple_notify_user_info_add_pair(user_info, _("Last name"), bb->last); + } + + if (bb->email != NULL) + purple_notify_user_info_add_pair(user_info, _("E-Mail"), bb->email); + + if (bb->AIM != NULL) + purple_notify_user_info_add_pair(user_info, _("AIM Account"), bb->AIM); + + if (bb->jid!= NULL) + purple_notify_user_info_add_pair(user_info, _("XMPP Account"), bb->jid); } static gboolean @@ -339,10 +375,9 @@ OPT_PROTO_NO_PASSWORD, NULL, /* user_splits */ NULL, /* protocol_options */ - /* {"png", 0, 0, 96, 96, 0, PURPLE_ICON_SCALE_DISPLAY}, */ /* icon_spec */ - NO_BUDDY_ICONS, /* not yet */ /* icon_spec */ + {"png,gif,jpeg", 0, 0, 96, 96, 65535, PURPLE_ICON_SCALE_DISPLAY}, /* icon_spec */ bonjour_list_icon, /* list_icon */ - NULL, /* list_emblem */ + NULL, /* list_emblem */ bonjour_status_text, /* status_text */ bonjour_tooltip_text, /* tooltip_text */ bonjour_status_types, /* status_types */ @@ -384,7 +419,7 @@ NULL, /* buddy_free */ bonjour_convo_closed, /* convo_closed */ NULL, /* normalize */ - NULL, /* set_buddy_icon */ + bonjour_set_buddy_icon, /* set_buddy_icon */ NULL, /* remove_group */ NULL, /* get_cb_real_name */ NULL, /* set_chat_topic */ @@ -412,11 +447,11 @@ PURPLE_PLUGIN_MAGIC, PURPLE_MAJOR_VERSION, PURPLE_MINOR_VERSION, - PURPLE_PLUGIN_PROTOCOL, /**< type */ + PURPLE_PLUGIN_PROTOCOL, /**< type */ NULL, /**< ui_requirement */ 0, /**< flags */ NULL, /**< dependencies */ - PURPLE_PRIORITY_DEFAULT, /**< priority */ + PURPLE_PRIORITY_DEFAULT, /**< priority */ "prpl-bonjour", /**< id */ "Bonjour", /**< name */ @@ -426,10 +461,10 @@ /** description */ N_("Bonjour Protocol Plugin"), NULL, /**< author */ - PURPLE_WEBSITE, /**< homepage */ + PURPLE_WEBSITE, /**< homepage */ NULL, /**< load */ - plugin_unload, /**< unload */ + plugin_unload, /**< unload */ NULL, /**< destroy */ NULL, /**< ui_info */ @@ -533,7 +568,7 @@ { default_firstname = g_strndup(fullname, splitpoint - fullname); tmp = &splitpoint[1]; - + /* The last name may be followed by a comma and additional data. * Only use the last name itself. */
--- a/libpurple/protocols/bonjour/buddy.c Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/protocols/bonjour/buddy.c Fri Aug 10 17:45:05 2007 +0000 @@ -18,6 +18,7 @@ #include <stdlib.h> #include "internal.h" +#include "cipher.h" #include "buddy.h" #include "account.h" #include "blist.h" @@ -41,6 +42,26 @@ return buddy; } +#define _B_CLR(x) g_free(x); x = NULL; + +void clear_bonjour_buddy_values(BonjourBuddy *buddy) { + + _B_CLR(buddy->first) + _B_CLR(buddy->email); + _B_CLR(buddy->ext); + _B_CLR(buddy->jid); + _B_CLR(buddy->last); + _B_CLR(buddy->msg); + _B_CLR(buddy->nick); + _B_CLR(buddy->node); + _B_CLR(buddy->phsh); + _B_CLR(buddy->status); + _B_CLR(buddy->vc); + _B_CLR(buddy->ver); + _B_CLR(buddy->AIM); + +} + void set_bonjour_buddy_value(BonjourBuddy* buddy, const char *record_key, const char *value, uint32_t len){ gchar **fld = NULL; @@ -106,11 +127,10 @@ PurpleBuddy *buddy; PurpleGroup *group; PurpleAccount *account = bonjour_buddy->account; - const char *status_id, *first, *last, *old_hash, *new_hash; - gchar *alias = NULL; + const char *status_id, *old_hash, *new_hash; /* Translate between the Bonjour status and the Purple status */ - if (g_ascii_strcasecmp("dnd", bonjour_buddy->status) == 0) + if (bonjour_buddy->status != NULL && g_ascii_strcasecmp("dnd", bonjour_buddy->status) == 0) status_id = BONJOUR_STATUS_ID_AWAY; else status_id = BONJOUR_STATUS_ID_AVAILABLE; @@ -138,15 +158,21 @@ } /* Create the alias for the buddy using the first and the last name */ - first = bonjour_buddy->first; - last = bonjour_buddy->last; - if ((first && *first) || (last && *last)) - alias = g_strdup_printf("%s%s%s", - (first && *first ? first : ""), - (first && *first && last && *last ? " " : ""), - (last && *last ? last : "")); - serv_got_alias(purple_account_get_connection(account), buddy->name, alias); - g_free(alias); + if (bonjour_buddy->nick) + serv_got_alias(purple_account_get_connection(account), buddy->name, bonjour_buddy->nick); + else { + gchar *alias = NULL; + const char *first, *last; + first = bonjour_buddy->first; + last = bonjour_buddy->last; + if ((first && *first) || (last && *last)) + alias = g_strdup_printf("%s%s%s", + (first && *first ? first : ""), + (first && *first && last && *last ? " " : ""), + (last && *last ? last : "")); + serv_got_alias(purple_account_get_connection(account), buddy->name, alias); + g_free(alias); + } /* Set the user's status */ if (bonjour_buddy->msg != NULL) @@ -166,12 +192,46 @@ new_hash = (bonjour_buddy->phsh && *(bonjour_buddy->phsh)) ? bonjour_buddy->phsh : NULL; if (new_hash && (!old_hash || strcmp(old_hash, new_hash) != 0)) { /* Look up the new icon data */ + /* TODO: Make sure the hash assigned to the retrieved buddy icon is the same + * as what we looked up. */ bonjour_dns_sd_retrieve_buddy_icon(bonjour_buddy); - } else + } else if (!new_hash) purple_buddy_icons_set_for_user(account, buddy->name, NULL, 0, NULL); } /** + * We got the buddy icon data; deal with it + */ +void bonjour_buddy_got_buddy_icon(BonjourBuddy *buddy, gconstpointer data, gsize len) { + /* Recalculate the hash instead of using the current phsh to make sure it is accurate for the icon. */ + int i; + gchar *enc; + char *p, hash[41]; + unsigned char hashval[20]; + + if (data == NULL || len == 0) + return; + + enc = purple_base64_encode(data, len); + + purple_cipher_digest_region("sha1", data, + len, sizeof(hashval), + hashval, NULL); + + p = hash; + for(i=0; i<20; i++, p+=2) + snprintf(p, 3, "%02x", hashval[i]); + + purple_debug_info("bonjour", "Got buddy icon for %s icon hash='%s' phsh='%s'.\n", buddy->name, + hash, buddy->phsh ? buddy->phsh : "(null)"); + + purple_buddy_icons_set_for_user(buddy->account, buddy->name, + g_memdup(data, len), len, hash); + + g_free(enc); +} + +/** * Deletes a buddy from memory. */ void @@ -179,8 +239,6 @@ { g_free(buddy->name); g_free(buddy->ip); - g_free(buddy->full_service_name); - g_free(buddy->first); g_free(buddy->phsh); g_free(buddy->status);
--- a/libpurple/protocols/bonjour/buddy.h Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/protocols/bonjour/buddy.h Fri Aug 10 17:45:05 2007 +0000 @@ -29,7 +29,6 @@ gchar *name; /* TODO: Remove and just use the hostname */ gchar *ip; - gchar *full_service_name; gint port_p2pj; gchar *first; @@ -76,9 +75,15 @@ BonjourBuddy *bonjour_buddy_new(const gchar *name, PurpleAccount *account); /** + * Clear any existing values from the buddy. + * This is called before updating so that we can notice removals + */ +void clear_bonjour_buddy_values(BonjourBuddy *buddy); + +/** * Sets a value in the BonjourBuddy struct, destroying the old value */ -void set_bonjour_buddy_value(BonjourBuddy* buddy, const char *record_key, const char *value, uint32_t len); +void set_bonjour_buddy_value(BonjourBuddy *buddy, const char *record_key, const char *value, uint32_t len); /** * Check if all the compulsory buddy data is present. @@ -91,6 +96,11 @@ void bonjour_buddy_add_to_purple(BonjourBuddy *buddy); /** + * We got the buddy icon data; deal with it + */ +void bonjour_buddy_got_buddy_icon(BonjourBuddy *buddy, gconstpointer data, gsize len); + +/** * Deletes a buddy from memory. */ void bonjour_buddy_delete(BonjourBuddy *buddy);
--- a/libpurple/protocols/bonjour/issues.txt Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/protocols/bonjour/issues.txt Fri Aug 10 17:45:05 2007 +0000 @@ -3,6 +3,5 @@ ========================================== * Status changes don't work -* Avatars * File transfers * Typing notifications
--- a/libpurple/protocols/bonjour/mdns_avahi.c Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/protocols/bonjour/mdns_avahi.c Fri Aug 10 17:45:05 2007 +0000 @@ -19,6 +19,7 @@ #include "mdns_interface.h" #include "debug.h" #include "buddy.h" +#include "bonjour.h" #include <avahi-client/client.h> #include <avahi-client/lookup.h> @@ -32,14 +33,25 @@ #include <avahi-glib/glib-malloc.h> #include <avahi-glib/glib-watch.h> +/* For some reason, this is missing from the Avahi type defines */ +#ifndef AVAHI_DNS_TYPE_NULL +#define AVAHI_DNS_TYPE_NULL 0x0A +#endif + /* data used by avahi bonjour implementation */ typedef struct _avahi_session_impl_data { AvahiClient *client; AvahiGLibPoll *glib_poll; AvahiServiceBrowser *sb; AvahiEntryGroup *group; + AvahiEntryGroup *buddy_icon_group; } AvahiSessionImplData; +typedef struct _avahi_buddy_impl_data { + AvahiServiceResolver *resolver; + AvahiRecordBrowser *buddy_icon_rec_browser; +} AvahiBuddyImplData; + static void _resolver_callback(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, AvahiResolverEvent event, const char *name, const char *type, const char *domain, @@ -59,11 +71,14 @@ case AVAHI_RESOLVER_FAILURE: purple_debug_error("bonjour", "_resolve_callback - Failure: %s\n", avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r)))); + avahi_service_resolver_free(r); break; case AVAHI_RESOLVER_FOUND: /* create a buddy record */ buddy = bonjour_buddy_new(name, account); + ((AvahiBuddyImplData *)buddy->mdns_impl_data)->resolver = r; + /* Get the ip as a string */ buddy->ip = g_malloc(AVAHI_ADDRESS_STR_MAX); avahi_address_snprint(buddy->ip, AVAHI_ADDRESS_STR_MAX, a); @@ -71,6 +86,7 @@ buddy->port_p2pj = port; /* Obtain the parameters from the text_record */ + clear_bonjour_buddy_values(buddy); l = txt; while (l != NULL) { ret = avahi_string_list_get_pair(l, &key, &value, &size); @@ -95,7 +111,6 @@ purple_debug_info("bonjour", "Unrecognized Service Resolver event: %d.\n", event); } - avahi_service_resolver_free(r); } static void @@ -145,6 +160,30 @@ } static void +_buddy_icon_group_cb(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) { + BonjourDnsSd *data = userdata; + AvahiSessionImplData *idata = data->mdns_impl_data; + + g_return_if_fail(g == idata->buddy_icon_group || idata->buddy_icon_group == NULL); + + switch(state) { + case AVAHI_ENTRY_GROUP_ESTABLISHED: + purple_debug_info("bonjour", "Successfully registered buddy icon data.\n"); + case AVAHI_ENTRY_GROUP_COLLISION: + purple_debug_error("bonjour", "Collision registering buddy icon data.\n"); + break; + case AVAHI_ENTRY_GROUP_FAILURE: + purple_debug_error("bonjour", "Error registering buddy icon data: %s\n.", + avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g)))); + break; + case AVAHI_ENTRY_GROUP_UNCOMMITED: + case AVAHI_ENTRY_GROUP_REGISTERING: + break; + } + +} + +static void _entry_group_cb(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) { AvahiSessionImplData *idata = userdata; @@ -170,6 +209,31 @@ } +static void +_buddy_icon_record_cb(AvahiRecordBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, + AvahiBrowserEvent event, const char *name, uint16_t clazz, uint16_t type, + const void *rdata, size_t size, AvahiLookupResultFlags flags, void *userdata) { + BonjourBuddy *buddy = userdata; + AvahiBuddyImplData *idata = buddy->mdns_impl_data; + + switch (event) { + case AVAHI_BROWSER_NEW: + bonjour_buddy_got_buddy_icon(buddy, rdata, size); + break; + case AVAHI_BROWSER_REMOVE: + case AVAHI_BROWSER_CACHE_EXHAUSTED: + case AVAHI_BROWSER_ALL_FOR_NOW: + case AVAHI_BROWSER_FAILURE: + purple_debug_error("bonjour", "Error rerieving buddy icon record: %s\n", + avahi_strerror(avahi_client_errno(avahi_record_browser_get_client(b)))); + break; + } + + /* Stop listening */ + avahi_record_browser_free(idata->buddy_icon_rec_browser); + idata->buddy_icon_rec_browser = NULL; +} + /**************************** * mdns_interface functions * ****************************/ @@ -203,10 +267,8 @@ return TRUE; } -gboolean _mdns_publish(BonjourDnsSd *data, PublishType type) { +gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records) { int publish_result = 0; - char portstring[6]; - const char *jid, *aim, *email; AvahiSessionImplData *idata = data->mdns_impl_data; AvahiStringList *lst = NULL; @@ -223,44 +285,11 @@ } } - /* Convert the port to a string */ - snprintf(portstring, sizeof(portstring), "%d", data->port_p2pj); - - jid = purple_account_get_string(data->account, "jid", NULL); - aim = purple_account_get_string(data->account, "AIM", NULL); - email = purple_account_get_string(data->account, "email", NULL); - - /* We should try to follow XEP-0174, but some clients have "issues", so we humor them. - * See http://telepathy.freedesktop.org/wiki/SalutInteroperability - */ - - /* Needed by iChat */ - lst = avahi_string_list_add_pair(lst,"txtvers", "1"); - /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ - lst = avahi_string_list_add_pair(lst, "1st", data->first); - /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ - lst = avahi_string_list_add_pair(lst, "last", data->last); - /* Needed by Adium */ - lst = avahi_string_list_add_pair(lst, "port.p2pj", portstring); - /* Needed by iChat, Gaim/Pidgin <= 2.0.1 */ - lst = avahi_string_list_add_pair(lst, "status", data->status); - /* Currently always set to "!" since we don't support AV and wont ever be in a conference */ - lst = avahi_string_list_add_pair(lst, "vc", data->vc); - lst = avahi_string_list_add_pair(lst, "ver", VERSION); - if (email != NULL && *email != '\0') - lst = avahi_string_list_add_pair(lst, "email", email); - if (jid != NULL && *jid != '\0') - lst = avahi_string_list_add_pair(lst, "jid", jid); - /* Nonstandard, but used by iChat */ - if (aim != NULL && *aim != '\0') - lst = avahi_string_list_add_pair(lst, "AIM", aim); - if (data->msg != NULL && *data->msg != '\0') - lst = avahi_string_list_add_pair(lst, "msg", data->msg); - if (data->phsh != NULL && *data->phsh != '\0') - lst = avahi_string_list_add_pair(lst, "phsh", data->phsh); - - /* TODO: ext, nick, node */ - + while (records) { + PurpleKeyValuePair *kvp = records->data; + lst = avahi_string_list_add_pair(lst, kvp->key, kvp->value); + records = records->next; + } /* Publish the service */ switch (type) { @@ -290,7 +319,8 @@ return FALSE; } - if ((publish_result = avahi_entry_group_commit(idata->group)) < 0) { + if (type == PUBLISH_START + && (publish_result = avahi_entry_group_commit(idata->group)) < 0) { purple_debug_error("bonjour", "Failed to commit " ICHAT_SERVICE " service. Error: %s\n", avahi_strerror(publish_result)); @@ -317,9 +347,71 @@ return TRUE; } -/* This is done differently than with Howl/Apple Bonjour */ -guint _mdns_register_to_mainloop(BonjourDnsSd *data) { - return 0; +gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len) { + AvahiSessionImplData *idata = data->mdns_impl_data; + + if (idata == NULL || idata->client == NULL) + return FALSE; + + if (avatar_data != NULL) { + gboolean new_group = FALSE; + gchar *svc_name; + int ret; + AvahiPublishFlags flags = 0; + + if (idata->buddy_icon_group == NULL) { + purple_debug_info("bonjour", "Setting new buddy icon.\n"); + new_group = TRUE; + + idata->buddy_icon_group = avahi_entry_group_new(idata->client, + _buddy_icon_group_cb, data); + } else { + purple_debug_info("bonjour", "Updating existing buddy icon.\n"); + flags |= AVAHI_PUBLISH_UPDATE; + } + + if (idata->buddy_icon_group == NULL) { + purple_debug_error("bonjour", + "Unable to initialize the buddy icon group (%s).\n", + avahi_strerror(avahi_client_errno(idata->client))); + return FALSE; + } + + svc_name = g_strdup_printf("%s." ICHAT_SERVICE "local", + purple_account_get_username(data->account)); + + ret = avahi_entry_group_add_record(idata->buddy_icon_group, AVAHI_IF_UNSPEC, + AVAHI_PROTO_UNSPEC, flags, svc_name, + AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_NULL, 120, avatar_data, avatar_len); + + g_free(svc_name); + + if (ret < 0) { + purple_debug_error("bonjour", + "Failed to register buddy icon. Error: %s\n", avahi_strerror(ret)); + if (new_group) { + avahi_entry_group_free(idata->buddy_icon_group); + idata->buddy_icon_group = NULL; + } + return FALSE; + } + + if (new_group && (ret = avahi_entry_group_commit(idata->buddy_icon_group)) < 0) { + purple_debug_error("bonjour", + "Failed to commit buddy icon group. Error: %s\n", avahi_strerror(ret)); + if (new_group) { + avahi_entry_group_free(idata->buddy_icon_group); + idata->buddy_icon_group = NULL; + } + return FALSE; + } + } else if (idata->buddy_icon_group != NULL) { + purple_debug_info("bonjour", "Removing existing buddy icon.\n"); + avahi_entry_group_free(idata->buddy_icon_group); + idata->buddy_icon_group = NULL; + } + + return TRUE; } void _mdns_stop(BonjourDnsSd *data) { @@ -340,10 +432,50 @@ } void _mdns_init_buddy(BonjourBuddy *buddy) { + buddy->mdns_impl_data = g_new0(AvahiBuddyImplData, 1); } void _mdns_delete_buddy(BonjourBuddy *buddy) { + AvahiBuddyImplData *idata = buddy->mdns_impl_data; + + g_return_if_fail(idata != NULL); + + if (idata->buddy_icon_rec_browser != NULL) + avahi_record_browser_free(idata->buddy_icon_rec_browser); + + if (idata->resolver != NULL) + avahi_service_resolver_free(idata->resolver); + + g_free(idata); + + buddy->mdns_impl_data = NULL; } -void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy) { +void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy) { + PurpleConnection *conn = purple_account_get_connection(buddy->account); + BonjourData *bd = conn->proto_data; + AvahiSessionImplData *session_idata = bd->dns_sd_data->mdns_impl_data; + AvahiBuddyImplData *idata = buddy->mdns_impl_data; + gchar *name; + + g_return_if_fail(idata != NULL); + + if (idata->buddy_icon_rec_browser != NULL) + avahi_record_browser_free(idata->buddy_icon_rec_browser); + + purple_debug_info("bonjour", "Retrieving buddy icon for '%s'.\n", buddy->name); + + name = g_strdup_printf("%s." ICHAT_SERVICE "local", buddy->name); + idata->buddy_icon_rec_browser = avahi_record_browser_new(session_idata->client, AVAHI_IF_UNSPEC, + AVAHI_PROTO_UNSPEC, name, AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_NULL, 0, + _buddy_icon_record_cb, buddy); + g_free(name); + + if (!idata->buddy_icon_rec_browser) { + purple_debug_error("bonjour", + "Unable to initialize buddy icon record browser. Error: %s\n.", + avahi_strerror(avahi_client_errno(session_idata->client))); + } + } +
--- a/libpurple/protocols/bonjour/mdns_common.c Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/protocols/bonjour/mdns_common.c Fri Aug 10 17:45:05 2007 +0000 @@ -17,30 +17,27 @@ #include <string.h> #include "internal.h" +#include "cipher.h" +#include "debug.h" + #include "mdns_common.h" #include "mdns_interface.h" #include "bonjour.h" #include "buddy.h" -#include "debug.h" /** * Allocate space for the dns-sd data. */ -BonjourDnsSd * -bonjour_dns_sd_new() -{ +BonjourDnsSd * bonjour_dns_sd_new() { BonjourDnsSd *data = g_new0(BonjourDnsSd, 1); - return data; } /** * Deallocate the space of the dns-sd data. */ -void -bonjour_dns_sd_free(BonjourDnsSd *data) -{ +void bonjour_dns_sd_free(BonjourDnsSd *data) { g_free(data->first); g_free(data->last); g_free(data->phsh); @@ -50,12 +47,90 @@ g_free(data); } +static GSList *generate_presence_txt_records(BonjourDnsSd *data) { + GSList *ret = NULL; + PurpleKeyValuePair *kvp; + char portstring[6]; + const char *jid, *aim, *email; + + /* Convert the port to a string */ + snprintf(portstring, sizeof(portstring), "%d", data->port_p2pj); + + jid = purple_account_get_string(data->account, "jid", NULL); + aim = purple_account_get_string(data->account, "AIM", NULL); + email = purple_account_get_string(data->account, "email", NULL); + +#define _M_ADD_R(k, v) \ + kvp = g_new0(PurpleKeyValuePair, 1); \ + kvp->key = g_strdup(k); \ + kvp->value = g_strdup(v); \ + ret = g_slist_prepend(ret, kvp); \ + + /* We should try to follow XEP-0174, but some clients have "issues", so we humor them. + * See http://telepathy.freedesktop.org/wiki/SalutInteroperability + */ + + /* Needed by iChat */ + _M_ADD_R("txtvers", "1") + /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ + _M_ADD_R("1st", data->first) + /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ + _M_ADD_R("last", data->last) + /* Needed by Adium */ + _M_ADD_R("port.p2pj", portstring) + /* Needed by iChat, Gaim/Pidgin <= 2.0.1 */ + _M_ADD_R("status", data->status) + _M_ADD_R("node", "libpurple") + _M_ADD_R("ver", VERSION) + /* Currently always set to "!" since we don't support AV and wont ever be in a conference */ + _M_ADD_R("vc", data->vc) + if (email != NULL && *email != '\0') { + _M_ADD_R("email", email) + } + if (jid != NULL && *jid != '\0') { + _M_ADD_R("jid", jid) + } + /* Nonstandard, but used by iChat */ + if (aim != NULL && *aim != '\0') { + _M_ADD_R("AIM", aim) + } + if (data->msg != NULL && *data->msg != '\0') { + _M_ADD_R("msg", data->msg) + } + if (data->phsh != NULL && *data->phsh != '\0') { + _M_ADD_R("phsh", data->phsh) + } + + /* TODO: ext, nick */ + return ret; +} + +static void free_presence_txt_records(GSList *lst) { + PurpleKeyValuePair *kvp; + while(lst) { + kvp = lst->data; + g_free(kvp->key); + g_free(kvp->value); + g_free(kvp); + lst = g_slist_remove(lst, lst->data); + } +} + +static gboolean publish_presence(BonjourDnsSd *data, PublishType type) { + GSList *txt_records; + gboolean ret; + + txt_records = generate_presence_txt_records(data); + ret = _mdns_publish(data, type, txt_records); + free_presence_txt_records(txt_records); + + return ret; +} + /** * Send a new dns-sd packet updating our status. */ -void -bonjour_dns_sd_send_status(BonjourDnsSd *data, const char *status, const char *status_message) -{ +void bonjour_dns_sd_send_status(BonjourDnsSd *data, const char *status, const char *status_message) { g_free(data->status); g_free(data->msg); @@ -63,26 +138,78 @@ data->msg = g_strdup(status_message); /* Update our text record with the new status */ - _mdns_publish(data, PUBLISH_UPDATE); /* <--We must control the errors */ + publish_presence(data, PUBLISH_UPDATE); +} + +/** + * Retrieve the buddy icon blob + */ +void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy) { + _mdns_retrieve_buddy_icon(buddy); +} + +void bonjour_dns_sd_update_buddy_icon(BonjourDnsSd *data) { + PurpleStoredImage *img; + + if ((img = purple_buddy_icons_find_account_icon(data->account))) { + gconstpointer avatar_data; + gsize avatar_len; + + avatar_data = purple_imgstore_get_data(img); + avatar_len = purple_imgstore_get_size(img); + + if (_mdns_set_buddy_icon_data(data, avatar_data, avatar_len)) { + int i; + gchar *enc; + char *p, hash[41]; + unsigned char hashval[20]; + + enc = purple_base64_encode(avatar_data, avatar_len); + + purple_cipher_digest_region("sha1", avatar_data, + avatar_len, sizeof(hashval), + hashval, NULL); + + p = hash; + for(i=0; i<20; i++, p+=2) + snprintf(p, 3, "%02x", hashval[i]); + + g_free(data->phsh); + data->phsh = g_strdup(hash); + + g_free(enc); + + /* Update our TXT record */ + publish_presence(data, PUBLISH_UPDATE); + } + + purple_imgstore_unref(img); + } else { + /* We need to do this regardless of whether data->phsh is set so that we + * cancel any icons that are currently in the process of being set */ + _mdns_set_buddy_icon_data(data, NULL, 0); + if (data->phsh != NULL) { + /* Clear the buddy icon */ + g_free(data->phsh); + data->phsh = NULL; + /* Update our TXT record */ + publish_presence(data, PUBLISH_UPDATE); + } + } } /** * Advertise our presence within the dns-sd daemon and start browsing * for other bonjour peers. */ -gboolean -bonjour_dns_sd_start(BonjourDnsSd *data) -{ - PurpleConnection *gc; - - gc = purple_account_get_connection(data->account); +gboolean bonjour_dns_sd_start(BonjourDnsSd *data) { /* Initialize the dns-sd data and session */ if (!_mdns_init_session(data)) return FALSE; /* Publish our bonjour IM client at the mDNS daemon */ - if (!_mdns_publish(data, PUBLISH_START)) + if (!publish_presence(data, PUBLISH_START)) return FALSE; /* Advise the daemon that we are waiting for connections */ @@ -91,11 +218,6 @@ return FALSE; } - - /* Get the socket that communicates with the mDNS daemon and bind it to a */ - /* callback that will handle the dns_sd packets */ - gc->inpa = _mdns_register_to_mainloop(data); - return TRUE; } @@ -103,14 +225,6 @@ * Unregister the "_presence._tcp" service at the mDNS daemon. */ -void -bonjour_dns_sd_stop(BonjourDnsSd *data) -{ - PurpleConnection *gc; - +void bonjour_dns_sd_stop(BonjourDnsSd *data) { _mdns_stop(data); - - gc = purple_account_get_connection(data->account); - if (gc->inpa > 0) - purple_input_remove(gc->inpa); }
--- a/libpurple/protocols/bonjour/mdns_common.h Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/protocols/bonjour/mdns_common.h Fri Aug 10 17:45:05 2007 +0000 @@ -42,6 +42,11 @@ void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy); /** + * Deal with a buddy icon update + */ +void bonjour_dns_sd_update_buddy_icon(BonjourDnsSd *data); + +/** * Advertise our presence within the dns-sd daemon and start * browsing for other bonjour peers. */
--- a/libpurple/protocols/bonjour/mdns_howl.c Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/protocols/bonjour/mdns_howl.c Fri Aug 10 17:45:05 2007 +0000 @@ -26,6 +26,7 @@ typedef struct _howl_impl_data { sw_discovery session; sw_discovery_oid session_id; + guint session_handler; } HowlSessionImplData; static sw_result HOWL_API @@ -84,6 +85,7 @@ /* Obtain the parameters from the text_record */ if ((text_record_len > 0) && (text_record) && (*text_record != '\0')) { + clear_bonjour_buddy_values(buddy); sw_text_record_iterator_init(&iterator, text_record, text_record_len); while (sw_text_record_iterator_next(iterator, key, (sw_octet *)value, &value_length) == SW_OKAY) set_bonjour_buddy_value(buddy, key, value, value_length); @@ -192,11 +194,9 @@ } -gboolean _mdns_publish(BonjourDnsSd *data, PublishType type) { +gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records) { sw_text_record dns_data; sw_result publish_result = SW_OKAY; - char portstring[6]; - const char *jid, *aim, *email; HowlSessionImplData *idata = data->mdns_impl_data; g_return_val_if_fail(idata != NULL, FALSE); @@ -207,43 +207,11 @@ return FALSE; } - /* Convert the port to a string */ - snprintf(portstring, sizeof(portstring), "%d", data->port_p2pj); - - jid = purple_account_get_string(data->account, "jid", NULL); - aim = purple_account_get_string(data->account, "AIM", NULL); - email = purple_account_get_string(data->account, "email", NULL); - - /* We should try to follow XEP-0174, but some clients have "issues", so we humor them. - * See http://telepathy.freedesktop.org/wiki/SalutInteroperability - */ - - /* Needed by iChat */ - sw_text_record_add_key_and_string_value(dns_data, "txtvers", "1"); - /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ - sw_text_record_add_key_and_string_value(dns_data, "1st", data->first); - /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ - sw_text_record_add_key_and_string_value(dns_data, "last", data->last); - /* Needed by Adium */ - sw_text_record_add_key_and_string_value(dns_data, "port.p2pj", portstring); - /* Needed by iChat, Gaim/Pidgin <= 2.0.1 */ - sw_text_record_add_key_and_string_value(dns_data, "status", data->status); - /* Currently always set to "!" since we don't support AV and wont ever be in a conference */ - sw_text_record_add_key_and_string_value(dns_data, "vc", data->vc); - sw_text_record_add_key_and_string_value(dns_data, "ver", VERSION); - if (email != NULL && *email != '\0') - sw_text_record_add_key_and_string_value(dns_data, "email", email); - if (jid != NULL && *jid != '\0') - sw_text_record_add_key_and_string_value(dns_data, "jid", jid); - /* Nonstandard, but used by iChat */ - if (aim != NULL && *aim != '\0') - sw_text_record_add_key_and_string_value(dns_data, "AIM", aim); - if (data->msg != NULL && *data->msg != '\0') - sw_text_record_add_key_and_string_value(dns_data, "msg", data->msg); - if (data->phsh != NULL && *data->phsh != '\0') - sw_text_record_add_key_and_string_value(dns_data, "phsh", data->phsh); - - /* TODO: ext, nick, node */ + while (records) { + PurpleKeyValuePair *kvp = records->data; + sw_text_record_add_key_and_string_value(dns_data, kvp->key, kvp->value); + records = records->next; + } /* Publish the service */ switch (type) { @@ -276,17 +244,18 @@ g_return_val_if_fail(idata != NULL, FALSE); - return (sw_discovery_browse(idata->session, 0, ICHAT_SERVICE, NULL, _browser_reply, - data->account, &session_id) == SW_OKAY); + if (sw_discovery_browse(idata->session, 0, ICHAT_SERVICE, NULL, _browser_reply, + data->account, &session_id) == SW_OKAY) { + idata->session_handler = purple_input_add(sw_discovery_socket(idata->session), + PURPLE_INPUT_READ, _mdns_handle_event, idata->session); + return TRUE; + } + + return FALSE; } -guint _mdns_register_to_mainloop(BonjourDnsSd *data) { - HowlSessionImplData *idata = data->mdns_impl_data; - - g_return_val_if_fail(idata != NULL, 0); - - return purple_input_add(sw_discovery_socket(idata->session), - PURPLE_INPUT_READ, _mdns_handle_event, idata->session); +gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len) { + return FALSE; } void _mdns_stop(BonjourDnsSd *data) { @@ -297,6 +266,8 @@ sw_discovery_cancel(idata->session, idata->session_id); + purple_input_remove(idata->session_handler); + /* TODO: should this really be g_free()'d ??? */ g_free(idata->session); @@ -311,5 +282,7 @@ void _mdns_delete_buddy(BonjourBuddy *buddy) { } -void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy) { +void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy) { } + +
--- a/libpurple/protocols/bonjour/mdns_interface.h Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/protocols/bonjour/mdns_interface.h Fri Aug 10 17:45:05 2007 +0000 @@ -22,19 +22,18 @@ gboolean _mdns_init_session(BonjourDnsSd *data); -gboolean _mdns_publish(BonjourDnsSd *data, PublishType type); +gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records); gboolean _mdns_browse(BonjourDnsSd *data); -guint _mdns_register_to_mainloop(BonjourDnsSd *data); +void _mdns_stop(BonjourDnsSd *data); -void _mdns_stop(BonjourDnsSd *data); +gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len); void _mdns_init_buddy(BonjourBuddy *buddy); void _mdns_delete_buddy(BonjourBuddy *buddy); -/* This doesn't quite belong here, but there really isn't any shared functionality */ -void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy); +void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy); #endif
--- a/libpurple/protocols/bonjour/mdns_win32.c Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/protocols/bonjour/mdns_win32.c Fri Aug 10 17:45:05 2007 +0000 @@ -21,12 +21,14 @@ #include "mdns_interface.h" #include "dns_sd_proxy.h" #include "dnsquery.h" +#include "mdns_common.h" /* data structure for the resolve callback */ typedef struct _ResolveCallbackArgs { DNSServiceRef resolver; guint resolver_handler; + gchar *full_service_name; PurpleDnsQueryData *query; @@ -35,10 +37,12 @@ /* data used by win32 bonjour implementation */ typedef struct _win32_session_impl_data { - DNSServiceRef advertisement; - DNSServiceRef browser; + DNSServiceRef presence_svc; + DNSServiceRef browser_svc; + DNSRecordRef buddy_icon_rec; - guint advertisement_handler; /* hack... windows bonjour is broken, so we have to have this */ + guint presence_handler; + guint browser_handler; } Win32SessionImplData; typedef struct _win32_buddy_impl_data { @@ -60,6 +64,7 @@ uint8_t txt_len; int i; + clear_bonjour_buddy_values(buddy); for (i = 0; buddy_TXT_records[i] != NULL; i++) { txt_entry = TXTRecordGetValuePtr(record_len, record, buddy_TXT_records[i], &txt_len); if (txt_entry != NULL) @@ -68,7 +73,7 @@ } static void DNSSD_API -_mdns_text_record_query_callback(DNSServiceRef DNSServiceRef, DNSServiceFlags flags, +_mdns_record_query_callback(DNSServiceRef DNSServiceRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char *fullname, uint16_t rrtype, uint16_t rrclass, uint16_t rdlen, const void *rdata, uint32_t ttl, void *context) @@ -90,8 +95,7 @@ g_return_if_fail(idata != NULL); - purple_buddy_icons_set_for_user(buddy->account, buddy->name, - g_memdup(rdata, rdlen), rdlen, buddy->phsh); + bonjour_buddy_got_buddy_icon(buddy, rdata, rdlen); /* We've got what we need; stop listening */ purple_input_remove(idata->null_query_handler); @@ -120,14 +124,16 @@ /* finally, set up the continuous txt record watcher, and add the buddy to purple */ - if (kDNSServiceErr_NoError == DNSServiceQueryRecord(&idata->txt_query, 0, 0, buddy->full_service_name, - kDNSServiceType_TXT, kDNSServiceClass_IN, _mdns_text_record_query_callback, buddy)) { - int fd = DNSServiceRefSockFD(idata->txt_query); - idata->txt_query_handler = purple_input_add(fd, PURPLE_INPUT_READ, _mdns_handle_event, idata->txt_query); + if (kDNSServiceErr_NoError == DNSServiceQueryRecord(&idata->txt_query, kDNSServiceFlagsLongLivedQuery, + kDNSServiceInterfaceIndexAny, args->full_service_name, kDNSServiceType_TXT, + kDNSServiceClass_IN, _mdns_record_query_callback, buddy)) { + + purple_debug_info("bonjour", "Found buddy %s at %s:%d\n", buddy->name, buddy->ip, buddy->port_p2pj); + + idata->txt_query_handler = purple_input_add(DNSServiceRefSockFD(idata->txt_query), + PURPLE_INPUT_READ, _mdns_handle_event, idata->txt_query); bonjour_buddy_add_to_purple(buddy); - - purple_debug_info("bonjour", "Found buddy %s at %s:%d\n", buddy->name, buddy->ip, buddy->port_p2pj); } else bonjour_buddy_delete(buddy); @@ -138,6 +144,7 @@ /* free the remaining args memory */ purple_dnsquery_destroy(args->query); + g_free(args->full_service_name); g_free(args); } @@ -165,13 +172,14 @@ _mdns_parse_text_record(args->buddy, txtRecord, txtLen); /* set more arguments, and start the host resolver */ - args->buddy->full_service_name = g_strdup(fullname); + args->full_service_name = g_strdup(fullname); if (!(args->query = purple_dnsquery_a(hosttarget, port, _mdns_resolve_host_callback, args))) { purple_debug_error("bonjour", "service resolver - host resolution failed.\n"); bonjour_buddy_delete(args->buddy); + g_free(args->full_service_name); g_free(args); } } @@ -180,11 +188,11 @@ static void DNSSD_API _mdns_service_register_callback(DNSServiceRef sdRef, DNSServiceFlags flags, DNSServiceErrorType errorCode, - const char *name, const char *regtype, const char *domain, void *context) -{ - /* we don't actually care about anything said in this callback - this is only here because Bonjour for windows is broken */ + const char *name, const char *regtype, const char *domain, void *context) { + + /* TODO: deal with collision */ if (kDNSServiceErr_NoError != errorCode) - purple_debug_error("bonjour", "service advertisement - callback error.\n"); + purple_debug_error("bonjour", "service advertisement - callback error (%d).\n", errorCode); else purple_debug_info("bonjour", "service advertisement - callback.\n"); } @@ -235,61 +243,23 @@ return TRUE; } -gboolean _mdns_publish(BonjourDnsSd *data, PublishType type) { +gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records) { TXTRecordRef dns_data; - char portstring[6]; gboolean ret = TRUE; - const char *jid, *aim, *email; - DNSServiceErrorType set_ret; + DNSServiceErrorType set_ret = kDNSServiceErr_NoError; Win32SessionImplData *idata = data->mdns_impl_data; g_return_val_if_fail(idata != NULL, FALSE); TXTRecordCreate(&dns_data, 256, NULL); - /* Convert the port to a string */ - snprintf(portstring, sizeof(portstring), "%d", data->port_p2pj); - - jid = purple_account_get_string(data->account, "jid", NULL); - aim = purple_account_get_string(data->account, "AIM", NULL); - email = purple_account_get_string(data->account, "email", NULL); - - /* We should try to follow XEP-0174, but some clients have "issues", so we humor them. - * See http://telepathy.freedesktop.org/wiki/SalutInteroperability - */ - - /* Needed by iChat */ - set_ret = TXTRecordSetValue(&dns_data, "txtvers", 1, "1"); - /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ - if (set_ret == kDNSServiceErr_NoError) - set_ret = TXTRecordSetValue(&dns_data, "1st", strlen(data->first), data->first); - /* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */ - if (set_ret == kDNSServiceErr_NoError) - set_ret = TXTRecordSetValue(&dns_data, "last", strlen(data->last), data->last); - /* Needed by Adium */ - if (set_ret == kDNSServiceErr_NoError) - set_ret = TXTRecordSetValue(&dns_data, "port.p2pj", strlen(portstring), portstring); - /* Needed by iChat, Gaim/Pidgin <= 2.0.1 */ - if (set_ret == kDNSServiceErr_NoError) - set_ret = TXTRecordSetValue(&dns_data, "status", strlen(data->status), data->status); - if (set_ret == kDNSServiceErr_NoError) - set_ret = TXTRecordSetValue(&dns_data, "ver", strlen(VERSION), VERSION); - /* Currently always set to "!" since we don't support AV and wont ever be in a conference */ - if (set_ret == kDNSServiceErr_NoError) - set_ret = TXTRecordSetValue(&dns_data, "vc", strlen(data->vc), data->vc); - if (set_ret == kDNSServiceErr_NoError && email != NULL && *email != '\0') - set_ret = TXTRecordSetValue(&dns_data, "email", strlen(email), email); - if (set_ret == kDNSServiceErr_NoError && jid != NULL && *jid != '\0') - set_ret = TXTRecordSetValue(&dns_data, "jid", strlen(jid), jid); - /* Nonstandard, but used by iChat */ - if (set_ret == kDNSServiceErr_NoError && aim != NULL && *aim != '\0') - set_ret = TXTRecordSetValue(&dns_data, "AIM", strlen(aim), aim); - if (set_ret == kDNSServiceErr_NoError && data->msg != NULL && *data->msg != '\0') - set_ret = TXTRecordSetValue(&dns_data, "msg", strlen(data->msg), data->msg); - if (set_ret == kDNSServiceErr_NoError && data->phsh != NULL && *data->phsh != '\0') - set_ret = TXTRecordSetValue(&dns_data, "phsh", strlen(data->phsh), data->phsh); - - /* TODO: ext, nick, node */ + while (records) { + PurpleKeyValuePair *kvp = records->data; + set_ret = TXTRecordSetValue(&dns_data, kvp->key, strlen(kvp->value), kvp->value); + if (set_ret != kDNSServiceErr_NoError) + break; + records = records->next; + } if (set_ret != kDNSServiceErr_NoError) { purple_debug_error("bonjour", "Unable to allocate memory for text record.\n"); @@ -301,14 +271,15 @@ switch (type) { case PUBLISH_START: - purple_debug_info("bonjour", "Registering service on port %d\n", data->port_p2pj); - err = DNSServiceRegister(&idata->advertisement, 0, 0, purple_account_get_username(data->account), ICHAT_SERVICE, + purple_debug_info("bonjour", "Registering presence on port %d\n", data->port_p2pj); + err = DNSServiceRegister(&idata->presence_svc, 0, 0, purple_account_get_username(data->account), ICHAT_SERVICE, NULL, NULL, htons(data->port_p2pj), TXTRecordGetLength(&dns_data), TXTRecordGetBytesPtr(&dns_data), _mdns_service_register_callback, NULL); break; case PUBLISH_UPDATE: - err = DNSServiceUpdateRecord(idata->advertisement, NULL, 0, TXTRecordGetLength(&dns_data), TXTRecordGetBytesPtr(&dns_data), 0); + purple_debug_info("bonjour", "Updating presence.\n"); + err = DNSServiceUpdateRecord(idata->presence_svc, NULL, 0, TXTRecordGetLength(&dns_data), TXTRecordGetBytesPtr(&dns_data), 0); break; } @@ -316,9 +287,12 @@ purple_debug_error("bonjour", "Failed to publish presence service.\n"); ret = FALSE; } else if (type == PUBLISH_START) { - /* hack: Bonjour on windows is broken. We don't care about the callback but we have to listen anyway */ - gint fd = DNSServiceRefSockFD(idata->advertisement); - idata->advertisement_handler = purple_input_add(fd, PURPLE_INPUT_READ, _mdns_handle_event, idata->advertisement); + /* We need to do this because according to the Apple docs: + * "the client is responsible for ensuring that DNSServiceProcessResult() is called + * whenever there is a reply from the daemon - the daemon may terminate its connection + * with a client that does not process the daemon's responses */ + idata->presence_handler = purple_input_add(DNSServiceRefSockFD(idata->presence_svc), + PURPLE_INPUT_READ, _mdns_handle_event, idata->presence_svc); } } @@ -332,37 +306,64 @@ g_return_val_if_fail(idata != NULL, FALSE); - return (DNSServiceBrowse(&idata->browser, 0, 0, ICHAT_SERVICE, NULL, + if (DNSServiceBrowse(&idata->browser_svc, 0, 0, ICHAT_SERVICE, NULL, _mdns_service_browse_callback, data->account) - == kDNSServiceErr_NoError); -} + == kDNSServiceErr_NoError) { + idata->browser_handler = purple_input_add(DNSServiceRefSockFD(idata->browser_svc), + PURPLE_INPUT_READ, _mdns_handle_event, idata->browser_svc); + return TRUE; + } -guint _mdns_register_to_mainloop(BonjourDnsSd *data) { - Win32SessionImplData *idata = data->mdns_impl_data; - - g_return_val_if_fail(idata != NULL, 0); - - return purple_input_add(DNSServiceRefSockFD(idata->browser), - PURPLE_INPUT_READ, _mdns_handle_event, idata->browser); + return FALSE; } void _mdns_stop(BonjourDnsSd *data) { Win32SessionImplData *idata = data->mdns_impl_data; - if (idata == NULL || idata->advertisement == NULL || idata->browser == NULL) + if (idata == NULL) return; - /* hack: for win32, we need to stop listening to the advertisement pipe too */ - purple_input_remove(idata->advertisement_handler); + if (idata->presence_svc != NULL) { + purple_input_remove(idata->presence_handler); + DNSServiceRefDeallocate(idata->presence_svc); + } - DNSServiceRefDeallocate(idata->advertisement); - DNSServiceRefDeallocate(idata->browser); + if (idata->browser_svc != NULL) { + purple_input_remove(idata->browser_handler); + DNSServiceRefDeallocate(idata->browser_svc); + } g_free(idata); data->mdns_impl_data = NULL; } +gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len) { + Win32SessionImplData *idata = data->mdns_impl_data; + DNSServiceErrorType err = kDNSServiceErr_NoError; + + g_return_val_if_fail(idata != NULL, FALSE); + + if (avatar_data != NULL && idata->buddy_icon_rec == NULL) { + purple_debug_info("bonjour", "Setting new buddy icon.\n"); + err = DNSServiceAddRecord(idata->presence_svc, &idata->buddy_icon_rec, + 0, kDNSServiceType_NULL, avatar_len, avatar_data, 0); + } else if (avatar_data != NULL) { + purple_debug_info("bonjour", "Updating existing buddy icon.\n"); + err = DNSServiceUpdateRecord(idata->presence_svc, idata->buddy_icon_rec, + 0, avatar_len, avatar_data, 0); + } else if (idata->buddy_icon_rec != NULL) { + purple_debug_info("bonjour", "Removing existing buddy icon.\n"); + DNSServiceRemoveRecord(idata->presence_svc, idata->buddy_icon_rec, 0); + idata->buddy_icon_rec = NULL; + } + + if (err != kDNSServiceErr_NoError) + purple_debug_error("bonjour", "Error (%d) setting buddy icon record.\n", err); + + return (err == kDNSServiceErr_NoError); +} + void _mdns_init_buddy(BonjourBuddy *buddy) { buddy->mdns_impl_data = g_new0(Win32BuddyImplData, 1); } @@ -387,8 +388,9 @@ buddy->mdns_impl_data = NULL; } -void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy) { +void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy) { Win32BuddyImplData *idata = buddy->mdns_impl_data; + char svc_name[kDNSServiceMaxDomainName]; g_return_if_fail(idata != NULL); @@ -400,10 +402,11 @@ idata->null_query = NULL; } - if (kDNSServiceErr_NoError == DNSServiceQueryRecord(&idata->null_query, 0, 0, buddy->full_service_name, - kDNSServiceType_NULL, kDNSServiceClass_IN, _mdns_text_record_query_callback, buddy)) { - int fd = DNSServiceRefSockFD(idata->null_query); - idata->null_query_handler = purple_input_add(fd, PURPLE_INPUT_READ, _mdns_handle_event, idata->null_query); + DNSServiceConstructFullName(svc_name, buddy->name, ICHAT_SERVICE, "local"); + if (kDNSServiceErr_NoError == DNSServiceQueryRecord(&idata->null_query, 0, kDNSServiceInterfaceIndexAny, svc_name, + kDNSServiceType_NULL, kDNSServiceClass_IN, _mdns_record_query_callback, buddy)) { + idata->null_query_handler = purple_input_add(DNSServiceRefSockFD(idata->null_query), + PURPLE_INPUT_READ, _mdns_handle_event, idata->null_query); } }
--- a/libpurple/protocols/jabber/auth.c Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/protocols/jabber/auth.c Fri Aug 10 17:45:05 2007 +0000 @@ -296,7 +296,7 @@ purple_request_yes_no(js->gc, _("Plaintext Authentication"), _("Plaintext Authentication"), msg, - 2, js->gc->account, NULL, NULL, NULL, + 2, js->gc->account, NULL, NULL, js->gc->account, allow_cyrus_plaintext_auth, disallow_plaintext_auth); g_free(msg);
--- a/libpurple/protocols/jabber/jabber.c Fri Aug 10 17:30:59 2007 +0000 +++ b/libpurple/protocols/jabber/jabber.c Fri Aug 10 17:45:05 2007 +0000 @@ -1718,7 +1718,7 @@ static PurpleCmdRet jabber_cmd_chat_role(PurpleConversation *conv, const char *cmd, char **args, char **error, void *data) { - JabberChat *chat; + JabberChat *chat = jabber_chat_find_by_conv(conv); if (!chat || !args || !args[0] || !args[1]) return PURPLE_CMD_RET_FAILED; @@ -1731,8 +1731,6 @@ return PURPLE_CMD_RET_FAILED; } - chat = jabber_chat_find_by_conv(conv); - if (!jabber_chat_role_user(chat, args[0], args[1])) { *error = g_strdup_printf(_("Unable to set role \"%s\" for user: %s"), args[1], args[0]);
--- a/pidgin/gtkconv.c Fri Aug 10 17:30:59 2007 +0000 +++ b/pidgin/gtkconv.c Fri Aug 10 17:45:05 2007 +0000 @@ -6320,13 +6320,13 @@ style = "color=\"#c4a000\""; } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_NICK) { atk_object_set_description(accessibility_obj, _("Nick Said")); - style = "color=\"#204a87\" style=\"italic\" weight=\"bold\""; + style = "color=\"#204a87\" weight=\"bold\""; } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT) { atk_object_set_description(accessibility_obj, _("Unread Messages")); style = "color=\"#cc0000\" weight=\"bold\""; } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) { atk_object_set_description(accessibility_obj, _("New Event")); - style = "color=\"#888a85\" style=\"italic\""; + style = "color=\"#888a85\" weight=\"bold\""; } else { style = ""; } @@ -8337,11 +8337,6 @@ if (gdk_window_get_state(w->window) & GDK_WINDOW_STATE_MAXIMIZED) return FALSE; - /* don't save if nothing changed */ - if (x == purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x") && - y == purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y")) - return FALSE; /* carry on normally */ - /* don't save off-screen positioning */ if (x + event->width < 0 || y + event->height < 0 || @@ -8389,10 +8384,10 @@ static void pidgin_conv_restore_position(PidginWindow *win) { pidgin_conv_set_position_size(win, - purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/x"), - purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/y"), - purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/width"), - purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/height")); + purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"), + purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"), + purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"), + purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height")); } PidginWindow * @@ -9029,6 +9024,7 @@ PidginWindow *win; win = pidgin_conv_window_new(); + g_signal_connect(G_OBJECT(win->window), "configure_event", G_CALLBACK(gtk_conv_configure_cb), NULL);
--- a/pidgin/gtkdocklet.c Fri Aug 10 17:30:59 2007 +0000 +++ b/pidgin/gtkdocklet.c Fri Aug 10 17:45:05 2007 +0000 @@ -636,7 +636,7 @@ purple_prefs_add_none(PIDGIN_PREFS_ROOT "/docklet"); purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/docklet/blink", FALSE); - purple_prefs_add_string(PIDGIN_PREFS_ROOT "/docklet/show", "pending"); + purple_prefs_add_string(PIDGIN_PREFS_ROOT "/docklet/show", "always"); purple_prefs_connect_callback(docklet_handle, PIDGIN_PREFS_ROOT "/docklet/show", docklet_show_pref_changed_cb, NULL);
--- a/pidgin/gtkimhtmltoolbar.c Fri Aug 10 17:30:59 2007 +0000 +++ b/pidgin/gtkimhtmltoolbar.c Fri Aug 10 17:45:05 2007 +0000 @@ -939,7 +939,7 @@ *y -= widget->allocation.height; } -static void pidgin_menu_clicked(GtkWidget *button, GdkEventButton *event, GtkMenu *menu) +static void pidgin_menu_clicked(GtkWidget *button, GtkMenu *menu) { gtk_widget_show_all(GTK_WIDGET(menu)); gtk_menu_popup(menu, NULL, NULL, menu_position_func, button, 0, gtk_get_current_event_time()); @@ -1096,6 +1096,13 @@ g_signal_handlers_unblock_by_func(G_OBJECT(item), G_CALLBACK(gtk_button_clicked), button); } +static void +enable_markup(GtkWidget *widget, gpointer null) +{ + if (GTK_IS_LABEL(widget)) + g_object_set(G_OBJECT(widget), "use-markup", TRUE, NULL); +} + static void gtk_imhtmltoolbar_init (GtkIMHtmlToolbar *toolbar) { GtkWidget *hbox = GTK_WIDGET(toolbar); @@ -1114,14 +1121,17 @@ GtkWidget **button; gboolean check; } buttons[] = { - {_("_Bold"), &toolbar->bold, TRUE}, - {_("_Italic"), &toolbar->italic, TRUE}, - {_("_Underline"), &toolbar->underline, TRUE}, - {_("_Larger"), &toolbar->larger_size, TRUE}, + {_("<b>_Bold</b>"), &toolbar->bold, TRUE}, + {_("<i>_Italic</i>"), &toolbar->italic, TRUE}, + {_("<u>_Underline</u>"), &toolbar->underline, TRUE}, + {_("<span size='larger'>_Larger</span>"), &toolbar->larger_size, TRUE}, #if 0 {_("_Normal"), &toolbar->normal_size, TRUE}, #endif - {_("_Smaller"), &toolbar->smaller_size, TRUE}, + {_("<span size='smaller'>_Smaller</span>"), &toolbar->smaller_size, TRUE}, + /* If we want to show the formatting for the following items, we would + * need to update them when formatting changes. The above items don't need + * no updating nor nothin' */ {_("_Font face"), &toolbar->font, TRUE}, {_("Foreground _color"), &toolbar->fgcolor, TRUE}, {_("Bac_kground color"), &toolbar->bgcolor, TRUE}, @@ -1175,9 +1185,11 @@ gtk_menu_shell_append(GTK_MENU_SHELL(font_menu), menuitem); g_signal_connect(G_OBJECT(old), "notify::sensitive", G_CALLBACK(button_sensitiveness_changed), menuitem); + gtk_container_foreach(GTK_CONTAINER(menuitem), (GtkCallback)enable_markup, NULL); } - g_signal_connect(G_OBJECT(font_button), "button-press-event", G_CALLBACK(pidgin_menu_clicked), font_menu); + g_signal_connect_swapped(G_OBJECT(font_button), "button-press-event", G_CALLBACK(gtk_widget_activate), font_button); + g_signal_connect(G_OBJECT(font_button), "activate", G_CALLBACK(pidgin_menu_clicked), font_menu); g_signal_connect(G_OBJECT(font_menu), "deactivate", G_CALLBACK(pidgin_menu_deactivate), font_button); /* Sep */ @@ -1218,7 +1230,8 @@ g_signal_connect(G_OBJECT(toolbar->link), "notify::sensitive", G_CALLBACK(button_sensitiveness_changed), menuitem); - g_signal_connect(G_OBJECT(insert_button), "button-press-event", G_CALLBACK(pidgin_menu_clicked), insert_menu); + g_signal_connect_swapped(G_OBJECT(insert_button), "button-press-event", G_CALLBACK(gtk_widget_activate), insert_button); + g_signal_connect(G_OBJECT(insert_button), "activate", G_CALLBACK(pidgin_menu_clicked), insert_menu); g_signal_connect(G_OBJECT(insert_menu), "deactivate", G_CALLBACK(pidgin_menu_deactivate), insert_button); toolbar->sml = NULL; }
--- a/pidgin/gtknotify.c Fri Aug 10 17:30:59 2007 +0000 +++ b/pidgin/gtknotify.c Fri Aug 10 17:45:05 2007 +0000 @@ -274,6 +274,7 @@ gtk_label_set_markup(GTK_LABEL(label), label_text); gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); + gtk_label_set_selectable(GTK_LABEL(label), TRUE); gtk_misc_set_alignment(GTK_MISC(label), 0, 0); gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0); @@ -609,6 +610,7 @@ gtk_label_set_markup(GTK_LABEL(label), label_text); gtk_label_set_line_wrap(GTK_LABEL(label), TRUE); + gtk_label_set_selectable(GTK_LABEL(label), TRUE); gtk_misc_set_alignment(GTK_MISC(label), 0, 0); gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0); gtk_widget_show(label); @@ -626,6 +628,7 @@ button = gtk_button_new_from_stock(GTK_STOCK_CLOSE); gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0); gtk_widget_show(button); + gtk_widget_grab_focus(button); g_signal_connect_swapped(G_OBJECT(button), "clicked", G_CALLBACK(gtk_widget_destroy), window);