Update list of wide characters
[centerim5.git] / src / BuddyListNode.cpp
blob2c5694f410f3bc7f03f76f253d944d490cb3964d
1 // Copyright (C) 2007 Mark Pustjens <pustjens@dds.nl>
2 // Copyright (C) 2010-2015 Petr Pavlu <setup@dagobah.cz>
3 //
4 // This file is part of CenterIM.
5 //
6 // CenterIM is free software; you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation; either version 2 of the License, or
9 // (at your option) any later version.
11 // CenterIM is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with CenterIM. If not, see <http://www.gnu.org/licenses/>.
19 #include "BuddyListNode.h"
21 #include "BuddyList.h"
22 #include "Log.h"
23 #include "Utils.h"
25 #include "gettext.h"
26 #include <cppconsui/ColorScheme.h>
28 BuddyListNode *BuddyListNode::createNode(PurpleBlistNode *node)
30 PurpleBlistNodeType type = purple_blist_node_get_type(node);
31 if (type == PURPLE_BLIST_BUDDY_NODE)
32 return new BuddyListBuddy(node);
33 if (type == PURPLE_BLIST_CHAT_NODE)
34 return new BuddyListChat(node);
35 if (type == PURPLE_BLIST_CONTACT_NODE)
36 return new BuddyListContact(node);
37 if (type == PURPLE_BLIST_GROUP_NODE)
38 return new BuddyListGroup(node);
40 LOG->error(_("Unhandled buddy list node '%d'."), type);
41 return nullptr;
44 void BuddyListNode::setParent(CppConsUI::Container &parent)
46 Button::setParent(parent);
48 treeview_ = dynamic_cast<CppConsUI::TreeView *>(&parent);
49 g_assert(treeview_ != nullptr);
52 void BuddyListNode::setRefNode(CppConsUI::TreeView::NodeReference n)
54 ref_ = n;
55 treeview_->setCollapsed(ref_, true);
58 void BuddyListNode::update()
60 // Cache the last_activity time.
61 last_activity_ = purple_blist_node_get_int(blist_node_, "last_activity");
63 BuddyListNode *parent_node = getParentNode();
64 // The parent could have changed, so re-parent the node.
65 if (parent_node != nullptr)
66 treeview_->setNodeParent(ref_, parent_node->getRefNode());
69 void BuddyListNode::sortIn()
71 CppConsUI::TreeView::NodeReference parent_ref;
72 if (purple_blist_node_get_parent(blist_node_) != nullptr) {
73 // This blist node has got a logical (libpurple) parent, check if it is
74 // possible to find also a cim node.
75 BuddyListNode *parent_node = getParentNode();
76 if (parent_node != nullptr)
77 parent_ref = parent_node->getRefNode();
78 else {
79 // there shouldn't be a cim node only if the flat mode is active
80 g_assert(BUDDYLIST->getListMode() == BuddyList::LIST_FLAT);
82 parent_ref = treeview_->getRootNode();
85 else {
86 if (PURPLE_BLIST_NODE_IS_GROUP(blist_node_)) {
87 // Groups do not have parent nodes.
88 parent_ref = treeview_->getRootNode();
90 else {
91 // When the new_node() callback is called for a contact/chat/buddy (and
92 // this method is called as a part of that callback) then the node does
93 // not have any parent set yet. In such a case, simply return.
94 return;
98 // Do the insertion sort. It should be fast enough here because nodes are
99 // usually already sorted and only one node is in a wrong position, so it kind
100 // of runs in O(n).
101 CppConsUI::TreeView::SiblingIterator i = parent_ref.end();
102 --i;
103 while (true) {
104 // sref is a node that we want to sort in.
105 CppConsUI::TreeView::SiblingIterator sref = i;
107 // Calculate a stop condition.
108 bool stop_flag;
109 if (i != parent_ref.begin()) {
110 stop_flag = false;
111 --i;
113 else
114 stop_flag = true;
116 BuddyListNode *swidget = dynamic_cast<BuddyListNode *>(sref->getWidget());
117 g_assert(swidget != nullptr);
118 CppConsUI::TreeView::SiblingIterator j = sref;
119 ++j;
120 while (j != parent_ref.end()) {
121 BuddyListNode *n = dynamic_cast<BuddyListNode *>(j->getWidget());
122 g_assert(n != nullptr);
124 if (swidget->lessOrEqual(*n)) {
125 treeview_->moveNodeBefore(sref, j);
126 break;
128 ++j;
130 // the node is last in the list
131 if (j == parent_ref.end())
132 treeview_->moveNodeAfter(sref, --j);
134 if (stop_flag)
135 break;
139 BuddyListNode *BuddyListNode::getParentNode() const
141 PurpleBlistNode *parent = purple_blist_node_get_parent(blist_node_);
142 if (parent == nullptr)
143 return nullptr;
145 return reinterpret_cast<BuddyListNode *>(
146 purple_blist_node_get_ui_data(parent));
149 BuddyListNode::ContextMenu::ContextMenu(BuddyListNode &parent_node)
150 : MenuWindow(parent_node, AUTOSIZE, AUTOSIZE), parent_node_(&parent_node)
154 void BuddyListNode::ContextMenu::onMenuAction(
155 Button & /*activator*/, PurpleCallback callback, void *data)
157 g_assert(callback != nullptr);
159 typedef void (*TypedCallback)(void *, void *);
160 TypedCallback real_callback = reinterpret_cast<TypedCallback>(callback);
161 real_callback(parent_node_->getPurpleBlistNode(), data);
163 close();
166 void BuddyListNode::ContextMenu::appendMenuAction(
167 MenuWindow &menu, PurpleMenuAction *act)
169 if (act == nullptr) {
170 menu.appendSeparator();
171 return;
174 if (act->children == nullptr) {
175 if (act->callback != nullptr)
176 menu.appendItem(
177 act->label, sigc::bind(sigc::mem_fun(this, &ContextMenu::onMenuAction),
178 act->callback, act->data));
179 else {
180 // TODO display non-focusable widget?
183 else {
184 auto submenu = new MenuWindow(0, 0, AUTOSIZE, AUTOSIZE);
185 menu.appendSubMenu(act->label, *submenu);
187 for (GList *l = act->children; l != nullptr; l = l->next) {
188 PurpleMenuAction *act = reinterpret_cast<PurpleMenuAction *>(l->data);
189 appendMenuAction(*submenu, act);
192 // Free memory associated with the children.
193 g_list_free(act->children);
194 act->children = nullptr;
197 // Free the menu action.
198 purple_menu_action_free(act);
201 void BuddyListNode::ContextMenu::appendProtocolMenu(PurpleConnection *gc)
203 PurplePluginProtocolInfo *prpl_info =
204 PURPLE_PLUGIN_PROTOCOL_INFO(purple_connection_get_prpl(gc));
205 if (prpl_info == nullptr || prpl_info->blist_node_menu == nullptr)
206 return;
208 GList *ll = prpl_info->blist_node_menu(parent_node_->getPurpleBlistNode());
209 for (GList *l = ll; l != nullptr; l = l->next) {
210 PurpleMenuAction *act = reinterpret_cast<PurpleMenuAction *>(l->data);
211 appendMenuAction(*this, act);
214 if (ll != nullptr) {
215 // Append a separator because some items were added.
216 appendSeparator();
219 g_list_free(ll);
222 void BuddyListNode::ContextMenu::appendExtendedMenu()
224 GList *ll =
225 purple_blist_node_get_extended_menu(parent_node_->getPurpleBlistNode());
226 for (GList *l = ll; l != nullptr; l = l->next) {
227 PurpleMenuAction *act = reinterpret_cast<PurpleMenuAction *>(l->data);
228 appendMenuAction(*this, act);
231 if (ll != nullptr) {
232 // Append a separator because some items were added.
233 appendSeparator();
236 g_list_free(ll);
239 BuddyListNode::BuddyListNode(PurpleBlistNode *node)
240 : treeview_(nullptr), blist_node_(node), last_activity_(0)
242 purple_blist_node_set_ui_data(blist_node_, this);
243 signal_activate.connect(sigc::mem_fun(this, &BuddyListNode::onActivate));
244 declareBindables();
247 BuddyListNode::~BuddyListNode()
249 purple_blist_node_set_ui_data(blist_node_, nullptr);
252 bool BuddyListNode::lessOrEqualByType(const BuddyListNode &other) const
254 // group < contact < buddy < chat < other.
255 PurpleBlistNodeType t1 = purple_blist_node_get_type(blist_node_);
256 PurpleBlistNodeType t2 = purple_blist_node_get_type(other.blist_node_);
257 return t1 <= t2;
260 bool BuddyListNode::lessOrEqualByBuddySort(
261 PurpleBuddy *left, PurpleBuddy *right) const
263 BuddyList::BuddySortMode mode = BUDDYLIST->getBuddySortMode();
264 int a, b;
266 switch (mode) {
267 case BuddyList::BUDDY_SORT_BY_NAME:
268 break;
269 case BuddyList::BUDDY_SORT_BY_STATUS:
270 a = getBuddyStatusWeight(left);
271 b = getBuddyStatusWeight(right);
272 if (a != b)
273 return a > b;
274 break;
275 case BuddyList::BUDDY_SORT_BY_ACTIVITY: {
276 // Compare buddies according to their last activity.
278 // It is possible that a blist node will not have the ui_data set. For
279 // instance, this happens when libpurple informs the program that a blist
280 // node is about to be removed. At that point, an associated BuddyListNode
281 // is destroyed, a parent node is updated and the parent tries to update its
282 // position according to its priority buddy. This buddy will not have the
283 // ui_data set because the BuddyListNode has been already freed.
285 // In such a case, the cached value cannot be obtained and value 0 will be
286 // used instead.
287 BuddyListNode *bnode_left = reinterpret_cast<BuddyListNode *>(
288 purple_blist_node_get_ui_data(PURPLE_BLIST_NODE(left)));
289 BuddyListNode *bnode_right = reinterpret_cast<BuddyListNode *>(
290 purple_blist_node_get_ui_data(PURPLE_BLIST_NODE(right)));
291 a = bnode_left ? bnode_left->last_activity_ : 0;
292 b = bnode_right ? bnode_right->last_activity_ : 0;
293 if (a != b)
294 return a > b;
295 } break;
297 return g_utf8_collate(
298 purple_buddy_get_alias(left), purple_buddy_get_alias(right)) <= 0;
301 const char *BuddyListNode::getBuddyStatus(PurpleBuddy *buddy) const
303 if (!purple_account_is_connected(purple_buddy_get_account(buddy)))
304 return "";
306 PurplePresence *presence = purple_buddy_get_presence(buddy);
307 PurpleStatus *status = purple_presence_get_active_status(presence);
308 return Utils::getStatusIndicator(status);
311 int BuddyListNode::getBuddyStatusWeight(PurpleBuddy *buddy) const
313 if (!purple_account_is_connected(purple_buddy_get_account(buddy)))
314 return 0;
316 PurplePresence *presence = purple_buddy_get_presence(buddy);
317 PurpleStatus *status = purple_presence_get_active_status(presence);
318 PurpleStatusType *status_type = purple_status_get_type(status);
319 PurpleStatusPrimitive prim = purple_status_type_get_primitive(status_type);
321 switch (prim) {
322 case PURPLE_STATUS_OFFLINE:
323 return 0;
324 default:
325 return 1;
326 case PURPLE_STATUS_UNSET:
327 return 2;
328 case PURPLE_STATUS_UNAVAILABLE:
329 return 3;
330 case PURPLE_STATUS_AWAY:
331 return 4;
332 case PURPLE_STATUS_EXTENDED_AWAY:
333 return 5;
334 case PURPLE_STATUS_MOBILE:
335 return 6;
336 case PURPLE_STATUS_MOOD:
337 return 7;
338 case PURPLE_STATUS_TUNE:
339 return 8;
340 case PURPLE_STATUS_INVISIBLE:
341 return 9;
342 case PURPLE_STATUS_AVAILABLE:
343 return 10;
347 int BuddyListNode::getColorSchemeByBuddy(int base_scheme, PurpleBuddy *buddy)
349 g_assert(base_scheme == CenterIM::SCHEME_BUDDYLISTBUDDY ||
350 base_scheme == CenterIM::SCHEME_BUDDYLISTCONTACT);
352 bool scheme_buddy = (base_scheme == CenterIM::SCHEME_BUDDYLISTBUDDY);
354 if (!purple_account_is_connected(purple_buddy_get_account(buddy)))
355 return scheme_buddy ? CenterIM::SCHEME_BUDDYLISTBUDDY_OFFLINE
356 : CenterIM::SCHEME_BUDDYLISTCONTACT_OFFLINE;
358 PurplePresence *presence = purple_buddy_get_presence(buddy);
359 PurpleStatus *status = purple_presence_get_active_status(presence);
360 PurpleStatusType *status_type = purple_status_get_type(status);
361 PurpleStatusPrimitive prim = purple_status_type_get_primitive(status_type);
363 switch (prim) {
364 case PURPLE_STATUS_UNSET:
365 case PURPLE_STATUS_OFFLINE:
366 default:
367 return scheme_buddy ? CenterIM::SCHEME_BUDDYLISTBUDDY_OFFLINE
368 : CenterIM::SCHEME_BUDDYLISTCONTACT_OFFLINE;
369 case PURPLE_STATUS_AVAILABLE:
370 case PURPLE_STATUS_MOBILE:
371 case PURPLE_STATUS_TUNE:
372 case PURPLE_STATUS_MOOD:
373 return scheme_buddy ? CenterIM::SCHEME_BUDDYLISTBUDDY_ONLINE
374 : CenterIM::SCHEME_BUDDYLISTCONTACT_ONLINE;
375 case PURPLE_STATUS_AWAY:
376 case PURPLE_STATUS_EXTENDED_AWAY:
377 return scheme_buddy ? CenterIM::SCHEME_BUDDYLISTBUDDY_AWAY
378 : CenterIM::SCHEME_BUDDYLISTCONTACT_AWAY;
379 case PURPLE_STATUS_UNAVAILABLE:
380 case PURPLE_STATUS_INVISIBLE:
381 return scheme_buddy ? CenterIM::SCHEME_BUDDYLISTBUDDY_NA
382 : CenterIM::SCHEME_BUDDYLISTCONTACT_NA;
386 void BuddyListNode::updateFilterVisibility(const char *name)
388 if (!isVisible())
389 return;
391 const char *filter = BUDDYLIST->getFilterString();
392 if (filter[0] == '\0')
393 return;
395 // Filtering is active.
396 setVisibility(purple_strcasestr(name, filter));
399 void BuddyListNode::retrieveUserInfoForName(
400 PurpleConnection *gc, const char *name) const
402 PurpleNotifyUserInfo *info = purple_notify_user_info_new();
403 purple_notify_user_info_add_pair(info, _("Information"), _("Retrieving..."));
404 purple_notify_userinfo(gc, name, info, nullptr, nullptr);
405 purple_notify_user_info_destroy(info);
406 serv_get_info(gc, name);
409 void BuddyListNode::actionOpenContextMenu()
411 openContextMenu();
414 void BuddyListNode::declareBindables()
416 declareBindable("buddylist", "contextmenu",
417 sigc::mem_fun(this, &BuddyListNode::actionOpenContextMenu),
418 InputProcessor::BINDABLE_NORMAL);
421 bool BuddyListBuddy::lessOrEqual(const BuddyListNode &other) const
423 const BuddyListBuddy *o = dynamic_cast<const BuddyListBuddy *>(&other);
424 if (o != nullptr)
425 return lessOrEqualByBuddySort(buddy_, o->buddy_);
426 return lessOrEqualByType(other);
429 void BuddyListBuddy::update()
431 BuddyListNode::update();
433 const char *status = getBuddyStatus(buddy_);
434 const char *alias = purple_buddy_get_alias(buddy_);
435 if (status[0] != '\0') {
436 char *text = g_strdup_printf("%s %s", status, alias);
437 setText(text);
438 g_free(text);
440 else
441 setText(alias);
443 sortIn();
445 updateColorScheme();
447 if (!purple_account_is_connected(purple_buddy_get_account(buddy_))) {
448 // Hide if account is offline.
449 setVisibility(false);
451 else
452 setVisibility(status[0] != '\0' || BUDDYLIST->getShowOfflineBuddiesPref());
454 updateFilterVisibility(alias);
457 void BuddyListBuddy::onActivate(Button & /*activator*/)
459 PurpleAccount *account = purple_buddy_get_account(buddy_);
460 const char *name = purple_buddy_get_name(buddy_);
461 PurpleConversation *conv =
462 purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, name, account);
464 if (conv == nullptr)
465 conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, account, name);
466 purple_conversation_present(conv);
469 const char *BuddyListBuddy::toString() const
471 return purple_buddy_get_alias(buddy_);
474 void BuddyListBuddy::retrieveUserInfo()
476 PurpleConnection *gc =
477 purple_account_get_connection(purple_buddy_get_account(buddy_));
478 retrieveUserInfoForName(gc, purple_buddy_get_name(buddy_));
481 BuddyListBuddy::BuddyContextMenu::BuddyContextMenu(BuddyListBuddy &parent_buddy)
482 : ContextMenu(parent_buddy), parent_buddy_(&parent_buddy)
484 appendProtocolMenu(purple_account_get_connection(
485 purple_buddy_get_account(parent_buddy_->getPurpleBuddy())));
486 appendExtendedMenu();
488 appendItem(
489 _("Information..."), sigc::mem_fun(this, &BuddyContextMenu::onInformation));
490 appendItem(
491 _("Alias..."), sigc::mem_fun(this, &BuddyContextMenu::onChangeAlias));
492 appendItem(_("Delete..."), sigc::mem_fun(this, &BuddyContextMenu::onRemove));
495 void BuddyListBuddy::BuddyContextMenu::onInformation(Button & /*activator*/)
497 parent_buddy_->retrieveUserInfo();
498 close();
501 void BuddyListBuddy::BuddyContextMenu::changeAliasResponseHandler(
502 CppConsUI::InputDialog &activator,
503 CppConsUI::AbstractDialog::ResponseType response)
505 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
506 return;
508 PurpleBuddy *buddy = parent_buddy_->getPurpleBuddy();
509 purple_blist_alias_buddy(buddy, activator.getText());
510 serv_alias_buddy(buddy);
512 // Close context menu.
513 close();
516 void BuddyListBuddy::BuddyContextMenu::onChangeAlias(Button & /*activator*/)
518 PurpleBuddy *buddy = parent_buddy_->getPurpleBuddy();
519 auto dialog =
520 new CppConsUI::InputDialog(_("Alias"), purple_buddy_get_alias(buddy));
521 dialog->signal_response.connect(
522 sigc::mem_fun(this, &BuddyContextMenu::changeAliasResponseHandler));
523 dialog->show();
526 void BuddyListBuddy::BuddyContextMenu::removeResponseHandler(
527 CppConsUI::MessageDialog & /*activator*/,
528 CppConsUI::AbstractDialog::ResponseType response)
530 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
531 return;
533 PurpleBuddy *buddy = parent_buddy_->getPurpleBuddy();
534 purple_account_remove_buddy(
535 purple_buddy_get_account(buddy), buddy, purple_buddy_get_group(buddy));
537 // Close the context menu before the buddy is deleted because its deletion can
538 // lead to destruction of this object.
539 close();
541 purple_blist_remove_buddy(buddy);
544 void BuddyListBuddy::BuddyContextMenu::onRemove(Button & /*activator*/)
546 PurpleBuddy *buddy = parent_buddy_->getPurpleBuddy();
547 char *msg = g_strdup_printf(
548 _("Are you sure you want to delete buddy %s from the list?"),
549 purple_buddy_get_alias(buddy));
550 auto dialog = new CppConsUI::MessageDialog(_("Buddy deletion"), msg);
551 g_free(msg);
552 dialog->signal_response.connect(
553 sigc::mem_fun(this, &BuddyContextMenu::removeResponseHandler));
554 dialog->show();
557 int BuddyListBuddy::getAttributes(
558 int property, int subproperty, int *attrs, CppConsUI::Error &error) const
560 if (BUDDYLIST->getColorizationMode() != BuddyList::COLOR_BY_ACCOUNT ||
561 property != CppConsUI::ColorScheme::PROPERTY_BUTTON_NORMAL)
562 return Button::getAttributes(property, subproperty, attrs, error);
564 // TODO Implement caching for these two properties.
565 PurpleAccount *account = purple_buddy_get_account(buddy_);
566 int fg = purple_account_get_ui_int(account, "centerim5",
567 "buddylist-foreground-color", CppConsUI::Curses::Color::DEFAULT);
568 int bg = purple_account_get_ui_int(account, "centerim5",
569 "buddylist-background-color", CppConsUI::Curses::Color::DEFAULT);
571 CppConsUI::ColorScheme::Color c(fg, bg);
572 return COLORSCHEME->getColorPair(c, attrs, error);
575 void BuddyListBuddy::openContextMenu()
577 ContextMenu *w = new BuddyContextMenu(*this);
578 w->show();
581 BuddyListBuddy::BuddyListBuddy(PurpleBlistNode *node) : BuddyListNode(node)
583 setColorScheme(CenterIM::SCHEME_BUDDYLISTBUDDY);
585 buddy_ = PURPLE_BUDDY(blist_node_);
588 void BuddyListBuddy::updateColorScheme()
590 switch (BUDDYLIST->getColorizationMode()) {
591 case BuddyList::COLOR_BY_STATUS:
592 setColorScheme(
593 getColorSchemeByBuddy(CenterIM::SCHEME_BUDDYLISTBUDDY, buddy_));
594 break;
595 default:
596 // Note: COLOR_BY_ACCOUNT case is handled by
597 // BuddyListBuddy::getAttributes().
598 setColorScheme(CenterIM::SCHEME_BUDDYLISTBUDDY);
599 break;
603 bool BuddyListChat::lessOrEqual(const BuddyListNode &other) const
605 const BuddyListChat *o = dynamic_cast<const BuddyListChat *>(&other);
606 if (o != nullptr)
607 return g_utf8_collate(
608 purple_chat_get_name(chat_), purple_chat_get_name(o->chat_)) <= 0;
609 return lessOrEqualByType(other);
612 void BuddyListChat::update()
614 BuddyListNode::update();
616 const char *name = purple_chat_get_name(chat_);
617 setText(name);
619 sortIn();
621 // Hide if account is offline.
622 setVisibility(purple_account_is_connected(purple_chat_get_account(chat_)));
624 updateFilterVisibility(name);
627 void BuddyListChat::onActivate(Button & /*activator*/)
629 PurpleAccount *account = purple_chat_get_account(chat_);
630 PurplePluginProtocolInfo *prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(
631 purple_find_prpl(purple_account_get_protocol_id(account)));
632 GHashTable *components = purple_chat_get_components(chat_);
634 char *chat_name = nullptr;
635 if (prpl_info != nullptr && prpl_info->get_chat_name != nullptr)
636 chat_name = prpl_info->get_chat_name(components);
638 const char *name;
639 if (chat_name != nullptr)
640 name = chat_name;
641 else
642 name = purple_chat_get_name(chat_);
644 PurpleConversation *conv =
645 purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT, name, account);
646 if (conv != nullptr)
647 purple_conversation_present(conv);
649 serv_join_chat(purple_account_get_connection(account), components);
651 g_free(chat_name);
654 const char *BuddyListChat::toString() const
656 return purple_chat_get_name(chat_);
659 BuddyListChat::ChatContextMenu::ChatContextMenu(BuddyListChat &parent_chat)
660 : ContextMenu(parent_chat), parent_chat_(&parent_chat)
662 appendProtocolMenu(purple_account_get_connection(
663 purple_chat_get_account(parent_chat_->getPurpleChat())));
664 appendExtendedMenu();
666 appendItem(
667 _("Alias..."), sigc::mem_fun(this, &ChatContextMenu::onChangeAlias));
668 appendItem(_("Delete..."), sigc::mem_fun(this, &ChatContextMenu::onRemove));
671 void BuddyListChat::ChatContextMenu::changeAliasResponseHandler(
672 CppConsUI::InputDialog &activator,
673 CppConsUI::AbstractDialog::ResponseType response)
675 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
676 return;
678 PurpleChat *chat = parent_chat_->getPurpleChat();
679 purple_blist_alias_chat(chat, activator.getText());
681 // Close context menu.
682 close();
685 void BuddyListChat::ChatContextMenu::onChangeAlias(Button & /*activator*/)
687 PurpleChat *chat = parent_chat_->getPurpleChat();
688 auto dialog =
689 new CppConsUI::InputDialog(_("Alias"), purple_chat_get_name(chat));
690 dialog->signal_response.connect(
691 sigc::mem_fun(this, &ChatContextMenu::changeAliasResponseHandler));
692 dialog->show();
695 void BuddyListChat::ChatContextMenu::removeResponseHandler(
696 CppConsUI::MessageDialog & /*activator*/,
697 CppConsUI::AbstractDialog::ResponseType response)
699 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
700 return;
702 PurpleChat *chat = parent_chat_->getPurpleChat();
704 // Close the context menu before the chat is deleted because its deletion can
705 // lead to destruction of this object.
706 close();
708 purple_blist_remove_chat(chat);
711 void BuddyListChat::ChatContextMenu::onRemove(Button & /*activator*/)
713 PurpleChat *chat = parent_chat_->getPurpleChat();
714 char *msg =
715 g_strdup_printf(_("Are you sure you want to delete chat %s from the list?"),
716 purple_chat_get_name(chat));
717 auto dialog = new CppConsUI::MessageDialog(_("Chat deletion"), msg);
718 g_free(msg);
719 dialog->signal_response.connect(
720 sigc::mem_fun(this, &ChatContextMenu::removeResponseHandler));
721 dialog->show();
724 void BuddyListChat::openContextMenu()
726 ContextMenu *w = new ChatContextMenu(*this);
727 w->show();
730 BuddyListChat::BuddyListChat(PurpleBlistNode *node) : BuddyListNode(node)
732 setColorScheme(CenterIM::SCHEME_BUDDYLISTCHAT);
734 chat_ = PURPLE_CHAT(blist_node_);
737 bool BuddyListContact::lessOrEqual(const BuddyListNode &other) const
739 const BuddyListContact *o = dynamic_cast<const BuddyListContact *>(&other);
740 if (o != nullptr) {
741 PurpleBuddy *left = purple_contact_get_priority_buddy(contact_);
742 PurpleBuddy *right = purple_contact_get_priority_buddy(o->contact_);
743 return lessOrEqualByBuddySort(left, right);
745 return lessOrEqualByType(other);
748 void BuddyListContact::update()
750 BuddyListNode::update();
752 PurpleBuddy *buddy = purple_contact_get_priority_buddy(contact_);
753 if (buddy == nullptr) {
754 // The contact does not have any associated buddy, ignore it until it gets a
755 // buddy assigned.
756 setText("*Contact*");
757 setVisibility(false);
758 return;
761 // Format contact size.
762 char *size;
763 if (contact_->currentsize > 1)
764 size = g_strdup_printf(" (%d)", contact_->currentsize);
765 else
766 size = nullptr;
768 // Format contact label.
769 const char *alias = purple_contact_get_alias(contact_);
770 const char *status = getBuddyStatus(buddy);
771 char *text;
772 if (status[0] != '\0')
773 text = g_strdup_printf("%s %s%s", status, alias, size ? size : "");
774 else
775 text = g_strdup_printf("%s%s", alias, size ? size : "");
776 setText(text);
777 g_free(size);
778 g_free(text);
780 sortIn();
782 updateColorScheme();
784 if (!purple_account_is_connected(purple_buddy_get_account(buddy))) {
785 // Hide if account is offline.
786 setVisibility(false);
788 else
789 setVisibility(status[0] != '\0' || BUDDYLIST->getShowOfflineBuddiesPref());
791 updateFilterVisibility(alias);
794 void BuddyListContact::onActivate(Button &activator)
796 PurpleBuddy *buddy = purple_contact_get_priority_buddy(contact_);
797 BuddyListNode *bnode = reinterpret_cast<BuddyListNode *>(
798 purple_blist_node_get_ui_data(PURPLE_BLIST_NODE(buddy)));
799 if (bnode != nullptr)
800 bnode->onActivate(activator);
803 const char *BuddyListContact::toString() const
805 return purple_contact_get_alias(contact_);
808 void BuddyListContact::setRefNode(CppConsUI::TreeView::NodeReference n)
810 BuddyListNode::setRefNode(n);
811 treeview_->setNodeStyle(n, CppConsUI::TreeView::STYLE_VOID);
814 void BuddyListContact::retrieveUserInfo()
816 PurpleBuddy *buddy = purple_contact_get_priority_buddy(contact_);
817 PurpleConnection *gc =
818 purple_account_get_connection(purple_buddy_get_account(buddy));
819 retrieveUserInfoForName(gc, purple_buddy_get_name(buddy));
822 BuddyListContact::ContactContextMenu::ContactContextMenu(
823 BuddyListContact &parent_contact)
824 : ContextMenu(parent_contact), parent_contact_(&parent_contact)
826 appendExtendedMenu();
828 if (parent_contact_->isCollapsed())
829 appendItem(_("Expand"),
830 sigc::bind(sigc::mem_fun(this, &ContactContextMenu::onExpandRequest),
831 true));
832 else
833 appendItem(_("Collapse"),
834 sigc::bind(sigc::mem_fun(this, &ContactContextMenu::onExpandRequest),
835 false));
837 appendItem(_("Information..."),
838 sigc::mem_fun(this, &ContactContextMenu::onInformation));
839 appendItem(
840 _("Alias..."), sigc::mem_fun(this, &ContactContextMenu::onChangeAlias));
841 appendItem(
842 _("Delete..."), sigc::mem_fun(this, &ContactContextMenu::onRemove));
844 auto groups = new CppConsUI::MenuWindow(*this, AUTOSIZE, AUTOSIZE);
846 for (PurpleBlistNode *node = purple_blist_get_root(); node != nullptr;
847 node = purple_blist_node_get_sibling_next(node)) {
848 if (!PURPLE_BLIST_NODE_IS_GROUP(node))
849 continue;
851 PurpleGroup *group = PURPLE_GROUP(node);
852 CppConsUI::Button *button = groups->appendItem(purple_group_get_name(group),
853 sigc::bind(sigc::mem_fun(this, &ContactContextMenu::onMoveTo), group));
854 if (purple_contact_get_group(parent_contact_->getPurpleContact()) == group)
855 button->grabFocus();
858 appendSubMenu(_("Move to..."), *groups);
861 void BuddyListContact::ContactContextMenu::onExpandRequest(
862 Button & /*activator*/, bool expand)
864 parent_contact_->setCollapsed(!expand);
865 close();
868 void BuddyListContact::ContactContextMenu::onInformation(Button & /*activator*/)
870 parent_contact_->retrieveUserInfo();
871 close();
874 void BuddyListContact::ContactContextMenu::changeAliasResponseHandler(
875 CppConsUI::InputDialog &activator,
876 CppConsUI::AbstractDialog::ResponseType response)
878 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
879 return;
881 PurpleContact *contact = parent_contact_->getPurpleContact();
882 if (contact->alias != nullptr)
883 purple_blist_alias_contact(contact, activator.getText());
884 else {
885 PurpleBuddy *buddy = purple_contact_get_priority_buddy(contact);
886 purple_blist_alias_buddy(buddy, activator.getText());
887 serv_alias_buddy(buddy);
890 // Close context menu.
891 close();
894 void BuddyListContact::ContactContextMenu::onChangeAlias(Button & /*activator*/)
896 PurpleContact *contact = parent_contact_->getPurpleContact();
897 auto dialog =
898 new CppConsUI::InputDialog(_("Alias"), purple_contact_get_alias(contact));
899 dialog->signal_response.connect(
900 sigc::mem_fun(this, &ContactContextMenu::changeAliasResponseHandler));
901 dialog->show();
904 void BuddyListContact::ContactContextMenu::removeResponseHandler(
905 CppConsUI::MessageDialog & /*activator*/,
906 CppConsUI::AbstractDialog::ResponseType response)
908 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
909 return;
911 // Based on gtkdialogs.c:pidgin_dialogs_remove_contact_cb().
912 PurpleContact *contact = parent_contact_->getPurpleContact();
913 PurpleBlistNode *cnode = PURPLE_BLIST_NODE(contact);
914 PurpleGroup *group = purple_contact_get_group(contact);
916 for (PurpleBlistNode *bnode = purple_blist_node_get_first_child(cnode);
917 bnode != nullptr; bnode = purple_blist_node_get_sibling_next(bnode)) {
918 PurpleBuddy *buddy = PURPLE_BUDDY(bnode);
919 PurpleAccount *account = purple_buddy_get_account(buddy);
920 if (purple_account_is_connected(account))
921 purple_account_remove_buddy(account, buddy, group);
924 // Close the context menu before the contact is deleted because its deletion
925 // can lead to destruction of this object.
926 close();
928 purple_blist_remove_contact(contact);
931 void BuddyListContact::ContactContextMenu::onRemove(Button & /*activator*/)
933 PurpleContact *contact = parent_contact_->getPurpleContact();
934 char *msg = g_strdup_printf(
935 _("Are you sure you want to delete contact %s from the list?"),
936 purple_contact_get_alias(contact));
937 auto dialog = new CppConsUI::MessageDialog(_("Contact deletion"), msg);
938 g_free(msg);
939 dialog->signal_response.connect(
940 sigc::mem_fun(this, &ContactContextMenu::removeResponseHandler));
941 dialog->show();
944 void BuddyListContact::ContactContextMenu::onMoveTo(
945 Button & /*activator*/, PurpleGroup *group)
947 PurpleContact *contact = parent_contact_->getPurpleContact();
948 close();
950 purple_blist_add_contact(contact, group, nullptr);
953 int BuddyListContact::getAttributes(
954 int property, int subproperty, int *attrs, CppConsUI::Error &error) const
956 if (BUDDYLIST->getColorizationMode() != BuddyList::COLOR_BY_ACCOUNT ||
957 property != CppConsUI::ColorScheme::PROPERTY_BUTTON_NORMAL)
958 return Button::getAttributes(property, subproperty, attrs, error);
960 // TODO Implement caching for these two properties.
961 PurpleAccount *account =
962 purple_buddy_get_account(purple_contact_get_priority_buddy(contact_));
963 int fg = purple_account_get_ui_int(account, "centerim5",
964 "buddylist-foreground-color", CppConsUI::Curses::Color::DEFAULT);
965 int bg = purple_account_get_ui_int(account, "centerim5",
966 "buddylist-background-color", CppConsUI::Curses::Color::DEFAULT);
968 CppConsUI::ColorScheme::Color c(fg, bg);
969 return COLORSCHEME->getColorPair(c, attrs, error);
972 void BuddyListContact::openContextMenu()
974 ContextMenu *w = new ContactContextMenu(*this);
975 w->show();
978 BuddyListContact::BuddyListContact(PurpleBlistNode *node) : BuddyListNode(node)
980 setColorScheme(CenterIM::SCHEME_BUDDYLISTCONTACT);
982 contact_ = PURPLE_CONTACT(blist_node_);
985 void BuddyListContact::updateColorScheme()
987 switch (BUDDYLIST->getColorizationMode()) {
988 case BuddyList::COLOR_BY_STATUS: {
989 PurpleBuddy *buddy = purple_contact_get_priority_buddy(contact_);
990 setColorScheme(
991 getColorSchemeByBuddy(CenterIM::SCHEME_BUDDYLISTCONTACT, buddy));
992 break;
994 default:
995 // Note: COLOR_BY_ACCOUNT case is handled by
996 // BuddyListContact::getAttributes().
997 setColorScheme(CenterIM::SCHEME_BUDDYLISTCONTACT);
998 break;
1002 bool BuddyListGroup::lessOrEqual(const BuddyListNode &other) const
1004 // If the groups are not sorted but ordered manually then this method is not
1005 // used.
1007 const BuddyListGroup *o = dynamic_cast<const BuddyListGroup *>(&other);
1008 if (o != nullptr)
1009 return g_utf8_collate(purple_group_get_name(group_),
1010 purple_group_get_name(o->group_)) <= 0;
1011 return lessOrEqualByType(other);
1014 void BuddyListGroup::update()
1016 BuddyListNode::update();
1018 setText(purple_group_get_name(group_));
1020 // Sort in the group.
1021 BuddyList::GroupSortMode mode = BUDDYLIST->getGroupSortMode();
1022 switch (mode) {
1023 case BuddyList::GROUP_SORT_BY_USER: {
1024 // Note that the sorting below works even if there was a contact/chat/buddy
1025 // node that is attached at the root level of the blist treeview. This
1026 // happens when such a node was just created (the new_node() callback was
1027 // called) but the node does not have any parent yet.
1029 PurpleBlistNode *prev = purple_blist_node_get_sibling_prev(blist_node_);
1031 if (prev != nullptr) {
1032 // It better be a group node.
1033 g_assert(PURPLE_BLIST_NODE_IS_GROUP(prev));
1035 BuddyListNode *bnode =
1036 reinterpret_cast<BuddyListNode *>(purple_blist_node_get_ui_data(prev));
1037 // There has to be ui_data set for all group nodes!
1038 g_assert(bnode != nullptr);
1040 treeview_->moveNodeAfter(ref_, bnode->getRefNode());
1042 else {
1043 // The group is the first one in the list.
1044 CppConsUI::TreeView::NodeReference parent_ref = treeview_->getRootNode();
1045 treeview_->moveNodeBefore(ref_, parent_ref.begin());
1047 } break;
1048 case BuddyList::GROUP_SORT_BY_NAME:
1049 sortIn();
1050 break;
1053 bool vis = true;
1054 if (!BUDDYLIST->getShowEmptyGroupsPref())
1055 vis = purple_blist_get_group_size(group_, FALSE);
1056 setVisibility(vis);
1059 void BuddyListGroup::onActivate(Button & /*activator*/)
1061 treeview_->toggleCollapsed(ref_);
1062 purple_blist_node_set_bool(blist_node_, "collapsed", ref_->isCollapsed());
1065 const char *BuddyListGroup::toString() const
1067 return purple_group_get_name(group_);
1070 void BuddyListGroup::setRefNode(CppConsUI::TreeView::NodeReference n)
1072 BuddyListNode::setRefNode(n);
1073 initCollapsedState();
1076 void BuddyListGroup::initCollapsedState()
1078 // This cannot be done when the purple_blist_load() function was called
1079 // because node settings are unavailable at that time.
1080 treeview_->setCollapsed(
1081 ref_, purple_blist_node_get_bool(blist_node_, "collapsed"));
1084 BuddyListGroup::GroupContextMenu::GroupContextMenu(BuddyListGroup &parent_group)
1085 : ContextMenu(parent_group), parent_group_(&parent_group)
1087 appendExtendedMenu();
1089 appendItem(_("Rename..."), sigc::mem_fun(this, &GroupContextMenu::onRename));
1090 appendItem(_("Delete..."), sigc::mem_fun(this, &GroupContextMenu::onRemove));
1092 if (BUDDYLIST->getGroupSortMode() == BuddyList::GROUP_SORT_BY_USER) {
1093 // If the manual sorting is enabled then show a menu item and a submenu for
1094 // group moving.
1095 auto groups = new CppConsUI::MenuWindow(*this, AUTOSIZE, AUTOSIZE);
1097 groups->appendItem(_("-Top-"),
1098 sigc::bind(sigc::mem_fun(this, &GroupContextMenu::onMoveAfter),
1099 static_cast<PurpleGroup *>(nullptr)));
1100 for (PurpleBlistNode *node = purple_blist_get_root(); node != nullptr;
1101 node = purple_blist_node_get_sibling_next(node)) {
1102 if (!PURPLE_BLIST_NODE_IS_GROUP(node))
1103 continue;
1105 PurpleGroup *group = PURPLE_GROUP(node);
1106 groups->appendItem(purple_group_get_name(group),
1107 sigc::bind(sigc::mem_fun(this, &GroupContextMenu::onMoveAfter), group));
1110 appendSubMenu(_("Move after..."), *groups);
1114 void BuddyListGroup::GroupContextMenu::renameResponseHandler(
1115 CppConsUI::InputDialog &activator,
1116 CppConsUI::AbstractDialog::ResponseType response)
1118 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
1119 return;
1121 const char *name = activator.getText();
1122 PurpleGroup *group = parent_group_->getPurpleGroup();
1123 PurpleGroup *other = purple_find_group(name);
1124 if (other != nullptr &&
1125 !purple_utf8_strcasecmp(name, purple_group_get_name(group))) {
1126 LOG->message(_("Specified group is already in the list."));
1127 // TODO Add group merging. Note that purple_blist_rename_group() can do the
1128 // merging.
1130 else
1131 purple_blist_rename_group(group, name);
1133 // Close context menu.
1134 close();
1137 void BuddyListGroup::GroupContextMenu::onRename(Button & /*activator*/)
1139 PurpleGroup *group = parent_group_->getPurpleGroup();
1140 auto dialog =
1141 new CppConsUI::InputDialog(_("Rename"), purple_group_get_name(group));
1142 dialog->signal_response.connect(
1143 sigc::mem_fun(this, &GroupContextMenu::renameResponseHandler));
1144 dialog->show();
1147 void BuddyListGroup::GroupContextMenu::removeResponseHandler(
1148 CppConsUI::MessageDialog & /*activator*/,
1149 CppConsUI::AbstractDialog::ResponseType response)
1151 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
1152 return;
1154 // Based on gtkdialogs.c:pidgin_dialogs_remove_group_cb().
1155 PurpleGroup *group = parent_group_->getPurpleGroup();
1156 PurpleBlistNode *cnode =
1157 purple_blist_node_get_first_child(PURPLE_BLIST_NODE(group));
1158 while (cnode != nullptr) {
1159 if (PURPLE_BLIST_NODE_IS_CONTACT(cnode)) {
1160 PurpleBlistNode *bnode = purple_blist_node_get_first_child(cnode);
1161 cnode = purple_blist_node_get_sibling_next(cnode);
1162 while (bnode != nullptr)
1163 if (PURPLE_BLIST_NODE_IS_BUDDY(bnode)) {
1164 PurpleBuddy *buddy = PURPLE_BUDDY(bnode);
1165 PurpleAccount *account = purple_buddy_get_account(buddy);
1166 bnode = purple_blist_node_get_sibling_next(bnode);
1167 if (purple_account_is_connected(account)) {
1168 purple_account_remove_buddy(account, buddy, group);
1169 purple_blist_remove_buddy(buddy);
1172 else
1173 bnode = purple_blist_node_get_sibling_next(bnode);
1175 else if (PURPLE_BLIST_NODE_IS_CHAT(cnode)) {
1176 PurpleChat *chat = PURPLE_CHAT(cnode);
1177 cnode = purple_blist_node_get_sibling_next(cnode);
1178 purple_blist_remove_chat(chat);
1180 else
1181 cnode = purple_blist_node_get_sibling_next(cnode);
1184 // Close the context menu before the group is deleted because its deletion can
1185 // lead to destruction of this object.
1186 close();
1188 purple_blist_remove_group(group);
1191 void BuddyListGroup::GroupContextMenu::onRemove(Button & /*activator*/)
1193 PurpleGroup *group = parent_group_->getPurpleGroup();
1194 char *msg = g_strdup_printf(
1195 _("Are you sure you want to delete group %s from the list?"),
1196 purple_group_get_name(group));
1197 auto dialog = new CppConsUI::MessageDialog(_("Group deletion"), msg);
1198 g_free(msg);
1199 dialog->signal_response.connect(
1200 sigc::mem_fun(this, &GroupContextMenu::removeResponseHandler));
1201 dialog->show();
1204 void BuddyListGroup::GroupContextMenu::onMoveAfter(
1205 Button & /*activator*/, PurpleGroup *group)
1207 PurpleGroup *moved_group = parent_group_->getPurpleGroup();
1208 close();
1210 purple_blist_add_group(moved_group, PURPLE_BLIST_NODE(group));
1213 void BuddyListGroup::openContextMenu()
1215 ContextMenu *w = new GroupContextMenu(*this);
1216 w->show();
1219 BuddyListGroup::BuddyListGroup(PurpleBlistNode *node) : BuddyListNode(node)
1221 setColorScheme(CenterIM::SCHEME_BUDDYLISTGROUP);
1223 group_ = PURPLE_GROUP(blist_node_);
1226 // vim: set tabstop=2 shiftwidth=2 textwidth=80 expandtab: