X-Git-Url: http://cgit.babelmonkeys.de/?a=blobdiff_plain;f=src%2Fgui%2Fgtk%2FJubGtkRosterUI.m;h=5711ee68dfe0a21eeb73f0da2e3fe4305493c238;hb=2403af8101a00532124246578afb88c1c8e34c14;hp=ad54fd6462ec820c136309dac1f1327f0e17b471;hpb=8571a0b0bc73062951d6ba55cc17e0ae2fc87cfc;p=jubjub.git diff --git a/src/gui/gtk/JubGtkRosterUI.m b/src/gui/gtk/JubGtkRosterUI.m index ad54fd6..5711ee6 100644 --- a/src/gui/gtk/JubGtkRosterUI.m +++ b/src/gui/gtk/JubGtkRosterUI.m @@ -1,5 +1,6 @@ #import #include +#include #import "JubGtkRosterUI.h" #import "JubGObjectMap.h" @@ -9,12 +10,13 @@ static void roster_row_activated(GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, gpointer data) { - JubGtkRosterUI *roster = data; + OFAutoreleasePool *pool; + XMPPContact *contact; + JubChatClient *client = data; GtkTreeIter row_iter; GtkTreeModel *tree_model; gchar *jid_s; - XMPPJID *jid; - OFAutoreleasePool *pool; + OFString *jid; tree_model = gtk_tree_view_get_model(tree_view); gtk_tree_model_get_iter(tree_model, &row_iter, path); @@ -24,31 +26,23 @@ static void roster_row_activated(GtkTreeView *tree_view, GtkTreePath *path, if (!jid_s) return; pool = [OFAutoreleasePool new]; - jid = [XMPPJID JIDWithString: [OFString stringWithUTF8String: jid_s]]; - [roster performSelectorOnMainThread: @selector(chatForJID:) - withObject: jid + jid = [OFString stringWithUTF8String: jid_s]; + contact = [client.contactManager.contacts objectForKey: jid]; + + [client performSelectorOnMainThread: @selector(chatForContact:) + withObject: contact waitUntilDone: NO]; [pool release]; } static void presence_changed(GtkComboBox *combo_box, gpointer data) { - XMPPPresence *pres; - XMPPConnection *connection = data; + JubChatClient *client = data; OFAutoreleasePool *pool = [OFAutoreleasePool new]; - const char *status = gtk_combo_box_get_active_id(combo_box); - if (!strcmp(status, "unavailable")) - pres = [XMPPPresence presenceWithType: @"unavailable"]; - else { - pres = [XMPPPresence presence]; - if (strcmp(status, "available")) - [pres setShow: @(status)]; - } - - [connection sendStanza: pres]; + [client sendPresenceWithStatus: @(status)]; [pool release]; } @@ -56,80 +50,93 @@ static void presence_changed(GtkComboBox *combo_box, gpointer data) static gboolean filter_roster_by_presence(GtkTreeModel *model, GtkTreeIter *iter, gpointer data) { - char *jid_s; - OFString *jid; - OFCountedSet *presences = data; - - gtk_tree_model_get(model, iter, 1, &jid_s, -1); + bool *showOffline = data; + char *status; + gtk_tree_model_get(model, iter, 2, &status, -1); - // Groups have no JID - if (!jid_s) + // Groups have no status + if (!status) return TRUE; - jid = [[OFString alloc] initWithUTF8String: jid_s]; + if (!*showOffline && !strcmp(status, "unavailable")) + return FALSE; - g_free(jid_s); + return TRUE; +} - int num = [presences countForObject: jid]; - if (num) { - [jid release]; - return TRUE; - } else { - [jid release]; - return FALSE; - } +static void menu_show_offline(GtkCheckMenuItem *checkmenuitem, gpointer data) +{ + JubGtkRosterUI *ui = data; + ui.showOffline = gtk_check_menu_item_get_active(checkmenuitem); +} + +static void dialog_response_callback(GtkDialog *dialog, gint response_id, + gpointer user_data) +{ + void (^block)(gint) = user_data; + block(response_id); + [block release]; } @implementation JubGtkRosterUI -- initWithClient: (JubChatClient*)client; +- initWithClient: (JubChatClient*)client { self = [super init]; @try { + GtkCheckMenuItem *show_offline_menu_item; GtkTreeView *roster_view; GtkBuilder *builder; - groupMap = [[OFMapTable alloc] + _groupMap = [[OFMapTable alloc] initWithKeyFunctions: keyFunctions valueFunctions: rowRefFunctions]; - contactMap = [[OFMutableDictionary alloc] init]; - chatMap = [[OFMutableDictionary alloc] init]; - presences = [[OFCountedSet alloc] init]; - connection = [client.connection retain]; + _subDialogMap = [[OFMapTable alloc] + initWithKeyFunctions: keyFunctions + valueFunctions: gObjectFunctions]; + _contactMap = [[OFMutableDictionary alloc] init]; + _client = [client retain]; - [connection addDelegate: self]; - [client.roster addDelegate: self]; + [_client.contactManager addDelegate: self]; + [_client.avatarManager setDelegate: self]; builder = gtk_builder_new(); gtk_builder_add_from_file(builder, "data/gtk/roster.ui", NULL); gtk_builder_connect_signals(builder, NULL); - roster_window = + _roster_window = GTK_WIDGET(gtk_builder_get_object(builder, "RosterWindow")); - gtk_widget_show(roster_window); + gtk_widget_show(_roster_window); - roster_model = GTK_TREE_STORE(gtk_builder_get_object(builder, + _roster_model = GTK_TREE_STORE(gtk_builder_get_object(builder, "RosterTreeStore")); - roster_filter = GTK_TREE_MODEL_FILTER( + _roster_filter = GTK_TREE_MODEL_FILTER( gtk_builder_get_object(builder, "RosterTreeModelFilter")); - gtk_tree_model_filter_set_visible_func(roster_filter, - filter_roster_by_presence, presences, NULL); + gtk_tree_model_filter_set_visible_func(_roster_filter, + filter_roster_by_presence, &_showOffline, NULL); roster_view = GTK_TREE_VIEW(gtk_builder_get_object(builder, "RosterTreeView")); g_signal_connect(roster_view, "row_activated", - G_CALLBACK(roster_row_activated), self); + G_CALLBACK(roster_row_activated), client); - presence_combo = GTK_COMBO_BOX(gtk_builder_get_object(builder, + _presence_combo = GTK_COMBO_BOX(gtk_builder_get_object(builder, "PresenceComboBox")); - presence_combo_changed_handler_id = - g_signal_connect(presence_combo, "changed", - G_CALLBACK(presence_changed), connection); + _presence_combo_changed_handler_id = + g_signal_connect(_presence_combo, "changed", + G_CALLBACK(presence_changed), client); + + show_offline_menu_item = + GTK_CHECK_MENU_ITEM(gtk_builder_get_object(builder, + "showOfflineCheckMenuItem")); + + g_signal_connect(show_offline_menu_item, "toggled", + G_CALLBACK(menu_show_offline), self); g_object_unref(G_OBJECT(builder)); } @catch (id e) { @@ -142,228 +149,343 @@ static gboolean filter_roster_by_presence(GtkTreeModel *model, - (void)dealloc { - [groupMap release]; - [contactMap release]; - [chatMap release]; - [presences release]; - [connection release]; + [_client.avatarManager setDelegate: nil]; + [_client.contactManager removeDelegate: self]; + [_subDialogMap release]; + [_groupMap release]; + [_contactMap release]; + [_client release]; - gtk_widget_destroy(roster_window); + gtk_widget_destroy(_roster_window); [super dealloc]; } -/* Presence handling */ -- (void)connection: (XMPPConnection*)connection - didReceivePresence: (XMPPPresence*)presence -{ - if ([presence.type isEqual: @"available"]) - [presences addObject: [presence.from bareJID]]; - else if ([presence.type isEqual: @"unavailable"]) - [presences removeObject: [presence.from bareJID]]; - - g_idle_add_block(^{ - gtk_tree_model_filter_refilter(roster_filter); - }); -} - -// FIXME: This needs to move somewhere else -- (JubGtkChatUI*)chatForJID: (XMPPJID*)jid -{ - OFAutoreleasePool *pool = [OFAutoreleasePool new]; - JubGtkChatUI *chat = - [chatMap objectForKey: [jid bareJID]]; - if (chat == nil) { - OFString * title = [@"Chat with " stringByAppendingString: - [jid bareJID]]; - - chat = [[[JubGtkChatUI alloc] - initWithTitle: title - closeBlock: ^{ - [chatMap removeObjectForKey: [jid bareJID]]; - } - sendBlock: ^(OFString *text) { - XMPPMessage *msg = - [XMPPMessage messageWithType: @"chat"]; - msg.to = jid; - msg.body = text; - [connection sendStanza: msg]; - } - ] autorelease]; - - [chatMap setObject: chat - forKey: [jid bareJID]]; - } - - [pool release]; - - return chat; -} - -- (void)connection: (XMPPConnection*)connection - didReceiveMessage: (XMPPMessage*)message +- (void)Jub_closeSubscribeDialogForRosterItem: (XMPPRosterItem*)rosterItem { - JubGtkChatUI *chat = [self chatForJID: message.from]; - [chat addMessage: message.body - sender: [message.from bareJID]]; + // Close subscripton dialogs, answered on another client + GtkDialog *dialog; + OFString *subscription = rosterItem.subscription; + OFString *bareJID = [rosterItem.JID bareJID]; + + if (([subscription isEqual: @"from"] || + [subscription isEqual: @"both"]) && + (dialog = [_subDialogMap valueForKey: bareJID]) != NULL) + gtk_dialog_response(dialog, GTK_RESPONSE_NONE); } /* Roster Delegate methods */ - (void)Jub_addRosterItem: (XMPPRosterItem*)item group: (OFString*)group { - g_idle_add_block(^{ - GtkTreeIter group_iter, contact_iter; - GtkTreeRowReference *group_ref, *contact_ref; - GtkTreePath *group_path, *contact_path; - OFMapTable *contactRows = - [contactMap objectForKey: [item.JID bareJID]]; - - group_ref = [groupMap valueForKey: group]; - - if (!group_ref) { - // Create new group row - gtk_tree_store_append(roster_model, &group_iter, NULL); - gtk_tree_store_set(roster_model, &group_iter, - 0, [group UTF8String], -1); - - group_path = gtk_tree_model_get_path(GTK_TREE_MODEL( - roster_model), &group_iter); - - group_ref = gtk_tree_row_reference_new(GTK_TREE_MODEL( - roster_model), group_path); - - [groupMap setValue: group_ref - forKey: group]; - } else { - // Get iter for existing group row - group_path = gtk_tree_row_reference_get_path(group_ref); - - gtk_tree_model_get_iter(GTK_TREE_MODEL(roster_model), - &group_iter, group_path); - } - gtk_tree_path_free(group_path); - - // Create new contact row - gtk_tree_store_append(roster_model, &contact_iter, &group_iter); - if (item.name) - gtk_tree_store_set(roster_model, &contact_iter, - 0, [item.name UTF8String], - 1, [[item.JID bareJID] UTF8String], -1); - else - gtk_tree_store_set(roster_model, &contact_iter, - 0, [item.JID.node UTF8String], - 1, [[item.JID bareJID] UTF8String], -1); + GtkTreeIter group_iter, contact_iter; + GtkTreeRowReference *group_ref, *contact_ref; + GtkTreePath *group_path, *contact_path; + OFString *bareJID = [item.JID bareJID]; + OFMapTable *contactRows; + + if (!(contactRows = [_contactMap objectForKey: bareJID])) { + contactRows = [OFMapTable + mapTableWithKeyFunctions: keyFunctions + valueFunctions: rowRefFunctions]; + + [_contactMap setObject: contactRows + forKey: bareJID]; + } - contact_path = gtk_tree_model_get_path(GTK_TREE_MODEL( - roster_model), &contact_iter); + group_ref = [_groupMap valueForKey: group]; - contact_ref = gtk_tree_row_reference_new(GTK_TREE_MODEL( - roster_model), contact_path); + if (!group_ref) { + // Create new group row + gtk_tree_store_append(_roster_model, &group_iter, NULL); + gtk_tree_store_set(_roster_model, &group_iter, + 0, [group UTF8String], -1); - gtk_tree_path_free(contact_path); + group_path = gtk_tree_model_get_path(GTK_TREE_MODEL( + _roster_model), &group_iter); - [contactRows setValue: contact_ref - forKey: group]; - }); + group_ref = gtk_tree_row_reference_new(GTK_TREE_MODEL( + _roster_model), group_path); + + [_groupMap setValue: group_ref + forKey: group]; + } else { + // Get iter for existing group row + group_path = gtk_tree_row_reference_get_path(group_ref); + + gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model), + &group_iter, group_path); + } + gtk_tree_path_free(group_path); + + // Create new contact row + gtk_tree_store_append(_roster_model, &contact_iter, &group_iter); + if (item.name) + gtk_tree_store_set(_roster_model, &contact_iter, + 0, [item.name UTF8String], + 1, [bareJID UTF8String], + 2, "unavailable", -1); + else if (item.JID.node) + gtk_tree_store_set(_roster_model, &contact_iter, + 0, [item.JID.node UTF8String], + 1, [bareJID UTF8String], + 2, "unavailable", -1); + else + gtk_tree_store_set(_roster_model, &contact_iter, + 0, [item.JID.domain UTF8String], + 1, [bareJID UTF8String], + 2, "unavailable", -1); + + contact_path = gtk_tree_model_get_path(GTK_TREE_MODEL(_roster_model), + &contact_iter); + + contact_ref = gtk_tree_row_reference_new(GTK_TREE_MODEL(_roster_model), + contact_path); + + gtk_tree_path_free(contact_path); + + [contactRows setValue: contact_ref + forKey: group]; } - (void)Jub_removeRosterItem: (XMPPRosterItem*)item group: (OFString*)group { - g_idle_add_block(^{ - GtkTreeIter contact_iter, group_iter; - GtkTreePath *contact_path, *group_path; - GtkTreeRowReference *contact_ref, *group_ref; - OFMapTable *contactRows = - [contactMap objectForKey: [item.JID bareJID]]; + GtkTreeIter contact_iter, group_iter; + GtkTreePath *contact_path, *group_path; + GtkTreeRowReference *contact_ref, *group_ref; + OFString *bareJID = [item.JID bareJID]; + OFMapTable *contactRows = [_contactMap objectForKey: bareJID]; + + contact_ref = [contactRows valueForKey: group]; + contact_path = gtk_tree_row_reference_get_path(contact_ref); + gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model), &contact_iter, + contact_path); + gtk_tree_path_free(contact_path); + + gtk_tree_store_remove(_roster_model, &contact_iter); + + group_ref = [_groupMap valueForKey: group]; + group_path = gtk_tree_row_reference_get_path(group_ref); + gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model), &group_iter, + group_path); + + if (!gtk_tree_model_iter_has_child(GTK_TREE_MODEL(_roster_model), + &group_iter)) { + gtk_tree_store_remove(_roster_model, &group_iter); + [_groupMap removeValueForKey: group]; + } - contact_ref = [contactRows valueForKey: group]; - contact_path = gtk_tree_row_reference_get_path(contact_ref); - gtk_tree_model_get_iter(GTK_TREE_MODEL(roster_model), - &contact_iter, contact_path); + gtk_tree_path_free(group_path); - gtk_tree_store_remove(roster_model, &contact_iter); + [contactRows removeValueForKey: group]; + if([contactRows count] == 0) + [_contactMap removeObjectForKey: bareJID]; +} - group_ref = [groupMap valueForKey: group]; - group_path = gtk_tree_row_reference_get_path(group_ref); - gtk_tree_model_get_iter(GTK_TREE_MODEL(roster_model), - &group_iter, group_path); +- (void)contactManager: (XMPPContactManager*)manager + didAddContact: (XMPPContact*)contact +{ + XMPPRosterItem *rosterItem = contact.rosterItem; + OFArray *groups = rosterItem.groups;; - if (!gtk_tree_model_iter_has_child(GTK_TREE_MODEL(roster_model), - &group_iter)) { - gtk_tree_store_remove(roster_model, &group_iter); - [groupMap removeValueForKey: group]; - } + if (groups == nil) + groups = @[ @"General" ]; - gtk_tree_path_free(group_path); - }); + for (OFString *group in groups) + [self performSelectorOnGLibThread: @selector( + Jub_addRosterItem:group:) + withObject: rosterItem + withObject: group]; + + [self Jub_closeSubscribeDialogForRosterItem: rosterItem]; } -- (void)rosterWasReceived: (XMPPRoster*)roster_ +- (void)contactManager: (XMPPContactManager*)manager + didRemoveContact: (XMPPContact*)contact { - of_log(@"Handling roster"); - [[roster_ rosterItems] enumerateKeysAndObjectsUsingBlock: - ^(OFString *bareJID, XMPPRosterItem *item, BOOL *stop) { - OFArray *groups; - OFMapTable *contactRows = [OFMapTable - mapTableWithKeyFunctions: keyFunctions - valueFunctions: rowRefFunctions]; + XMPPRosterItem *rosterItem = contact.rosterItem; + OFArray *groups = rosterItem.groups; - [contactMap setObject: contactRows - forKey: bareJID]; + if (groups == nil) + groups = @[ @"General" ]; - if (item.groups != nil) - groups = item.groups; - else - groups = @[@"General"]; + for (OFString *group in groups) + [self performSelectorOnGLibThread: @selector( + Jub_removeRosterItem:group:) + withObject: rosterItem + withObject: group]; +} - for (OFString *group in groups) - [self Jub_addRosterItem: item - group: group]; - }]; +- (void)contactManager: (XMPPContactManager*)manager + didReceiveSubscriptionRequest: (XMPPPresence*)presence +{ + XMPPJID *JID = presence.from; + OFString *bareJID = [JID bareJID]; + OFString *message = [OFString stringWithFormat: @"%@ would like " + @"to subscribe to your presence.", bareJID]; + g_idle_add_block(^{ + GtkWidget *dialog, *content_area, *label; + + if ([_subDialogMap valueForKey: JID] != NULL) + return; + + dialog = gtk_dialog_new_with_buttons("Subscription Request", + GTK_WINDOW(_roster_window), GTK_DIALOG_DESTROY_WITH_PARENT, + "Accept", GTK_RESPONSE_ACCEPT, + "Deny", GTK_RESPONSE_REJECT, NULL); + + content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(label), [message UTF8String]); + gtk_container_add(GTK_CONTAINER(content_area), label); + + g_signal_connect(dialog, "response", + G_CALLBACK(dialog_response_callback), + [^(gint response_id) { + if (response_id == GTK_RESPONSE_ACCEPT) + [manager sendSubscribedToJID: JID]; + else if (response_id == GTK_RESPONSE_REJECT) + [manager sendUnsubscribedToJID: JID]; + [_subDialogMap removeValueForKey: bareJID]; + gtk_widget_destroy(GTK_WIDGET(dialog)); + } copy]); + + [_subDialogMap setValue: dialog + forKey: bareJID]; + + gtk_widget_show_all(dialog); + }); } -- (void)roster: (XMPPRoster*)roster_ - didReceiveRosterItem: (XMPPRosterItem*)item +- (void)contact: (XMPPContact*)contact + willUpdateWithRosterItem: (XMPPRosterItem*)rosterItem; { - OFArray *groups; - XMPPRosterItem *oldItem = - [roster_.rosterItems objectForKey: [item.JID bareJID]]; + [self Jub_closeSubscribeDialogForRosterItem: rosterItem]; - if (oldItem) { - if (oldItem.groups != nil) - groups = oldItem.groups; - else - groups = @[@"General"]; + // Remove contact from old set of groups + XMPPRosterItem *oldItem = contact.rosterItem; + OFArray *groups = oldItem.groups; - for (OFString *group in groups) - [self Jub_removeRosterItem: oldItem - group: group]; + if (groups == nil) + groups = @[ @"General" ]; - [contactMap removeObjectForKey: [item.JID bareJID]]; - } + for (OFString *group in groups) + [self performSelectorOnGLibThread: @selector( + Jub_removeRosterItem:group:) + withObject: oldItem + withObject: group]; - if (![item.subscription isEqual: @"remove"]) { - OFMapTable *contactRows = [OFMapTable - mapTableWithKeyFunctions: keyFunctions - valueFunctions: rowRefFunctions]; + // Add contact to new set of groups + groups = rosterItem.groups; - [contactMap setObject: contactRows - forKey: [item.JID bareJID]]; + if (groups == nil) + groups = @[ @"General" ]; - if (item.groups != nil) - groups = item.groups; - else - groups = @[@"General"]; + for (OFString *group in groups) + [self performSelectorOnGLibThread: @selector( + Jub_addRosterItem:group:) + withObject: rosterItem + withObject: group]; +} - for (OFString *group in groups) - [self Jub_addRosterItem: item - group: group]; - } +- (void)contact: (XMPPContact*)contact + didSendPresence: (XMPPPresence*)presence +{ + OFDictionary *allPresences = [contact presences]; + XMPPPresence *highPresence = [[[allPresences allObjects] sortedArray] + lastObject]; + OFMutableString *tooltip = + [OFMutableString stringWithString: @"Resources:"]; + + [allPresences enumerateKeysAndObjectsUsingBlock: + ^(OFString *resource, XMPPPresence *pres, bool *stop) { + [tooltip appendString: @"\n"]; + [tooltip appendString: resource]; + if ([pres.type isEqual: @"available"]) { + if (pres.show != nil) + [tooltip appendFormat: @" (%@)", pres.show]; + else + [tooltip appendString: @" (available)"]; + } else + [tooltip appendString: @" (unavailable)"]; + + if (pres.status) + [tooltip appendFormat: @": %@", pres.status]; + }]; + + g_idle_add_block(^{ + GtkTreeIter iter; + GtkTreePath *path; + GtkTreeRowReference *ref; + OFString *bareJID = [contact.rosterItem.JID bareJID]; + OFMapTable *contactRows = [_contactMap objectForKey: bareJID]; + OFArray *groups = contact.rosterItem.groups;; + + if (groups == nil) + groups = @[ @"General" ]; + + for (OFString *group in groups) { + ref = [contactRows valueForKey: group]; + path = gtk_tree_row_reference_get_path(ref); + gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model), + &iter, path); + gtk_tree_path_free(path); + + if ([highPresence.type isEqual: @"available"]) { + if (highPresence.show != nil) + gtk_tree_store_set(_roster_model, &iter, + 2, [highPresence.show UTF8String], + -1); + else + gtk_tree_store_set(_roster_model, &iter, + 2, "available", -1); + } else + gtk_tree_store_set(_roster_model, &iter, + 2, "unavailable", -1); + + gtk_tree_store_set(_roster_model, &iter, + 3, [tooltip UTF8String], -1); + } + + gtk_tree_model_filter_refilter(_roster_filter); + }); } -- (void)client: (JubChatClient*)client +- (void)contact: (XMPPContact*)contact + didSetAvatar: (OFString*)avatarFile +{ + of_log(@"Got an avatar from %@", contact.rosterItem.JID); + g_idle_add_block(^{ + GtkTreeIter iter; + GtkTreePath *path; + GtkTreeRowReference *ref; + OFString *bareJID = [contact.rosterItem.JID bareJID]; + OFMapTable *contactRows = [_contactMap objectForKey: bareJID]; + OFArray *groups = contact.rosterItem.groups;; + + GdkPixbuf *avatar = + gdk_pixbuf_new_from_file_at_size([avatarFile UTF8String], + 32, 32, NULL); + + if (groups == nil) + groups = @[ @"General" ]; + + for (OFString *group in groups) { + ref = [contactRows valueForKey: group]; + path = gtk_tree_row_reference_get_path(ref); + gtk_tree_model_get_iter(GTK_TREE_MODEL(_roster_model), + &iter, path); + gtk_tree_path_free(path); + + gtk_tree_store_set(_roster_model, &iter, + 4, avatar, -1); + } + g_object_unref(G_OBJECT(avatar)); + }); +} + +- (void)client: (JubChatClient*)client_ didChangePresence: (XMPPPresence*)presence { OFString *tooltip = @""; @@ -380,25 +502,38 @@ static gboolean filter_roster_by_presence(GtkTreeModel *model, g_idle_add_block(^{ // Block the PresenceComboBox's changed handler, so it doesn't // fire and resend presence - g_signal_handler_block(presence_combo, - presence_combo_changed_handler_id); + g_signal_handler_block(_presence_combo, + _presence_combo_changed_handler_id); if ([presence.type isEqual: @"unavailable"]) - gtk_combo_box_set_active_id(presence_combo, + gtk_combo_box_set_active_id(_presence_combo, "unavailable"); else if (show == nil) - gtk_combo_box_set_active_id(presence_combo, + gtk_combo_box_set_active_id(_presence_combo, "available"); else - gtk_combo_box_set_active_id(presence_combo, + gtk_combo_box_set_active_id(_presence_combo, [show UTF8String]); // Unblock the changed handler - g_signal_handler_unblock(presence_combo, - presence_combo_changed_handler_id); + g_signal_handler_unblock(_presence_combo, + _presence_combo_changed_handler_id); - gtk_widget_set_tooltip_markup(GTK_WIDGET(presence_combo), + gtk_widget_set_tooltip_markup(GTK_WIDGET(_presence_combo), [tooltip UTF8String]); }); } + +- (bool)showOffline +{ + OF_GETTER(_showOffline, YES); +} + +- (void)setShowOffline: (bool)showOffline +{ + _showOffline = showOffline; + g_idle_add_block(^{ + gtk_tree_model_filter_refilter(_roster_filter); + }); +} @end