Use pkg-config to find ncursesw
[centerim5.git] / src / BuddyListNode.cpp
blob0427cd24714bbfda2cba6fc7e7ecff7672e0733a
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 BuddyListBuddy::draw()
597 setColorScheme(CenterIM::SCHEME_BUDDYLISTBUDDY);
598 break;
602 bool BuddyListChat::lessOrEqual(const BuddyListNode &other) const
604 const BuddyListChat *o = dynamic_cast<const BuddyListChat *>(&other);
605 if (o != nullptr)
606 return g_utf8_collate(
607 purple_chat_get_name(chat_), purple_chat_get_name(o->chat_)) <= 0;
608 return lessOrEqualByType(other);
611 void BuddyListChat::update()
613 BuddyListNode::update();
615 const char *name = purple_chat_get_name(chat_);
616 setText(name);
618 sortIn();
620 // Hide if account is offline.
621 setVisibility(purple_account_is_connected(purple_chat_get_account(chat_)));
623 updateFilterVisibility(name);
626 void BuddyListChat::onActivate(Button & /*activator*/)
628 PurpleAccount *account = purple_chat_get_account(chat_);
629 PurplePluginProtocolInfo *prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(
630 purple_find_prpl(purple_account_get_protocol_id(account)));
631 GHashTable *components = purple_chat_get_components(chat_);
633 char *chat_name = nullptr;
634 if (prpl_info != nullptr && prpl_info->get_chat_name != nullptr)
635 chat_name = prpl_info->get_chat_name(components);
637 const char *name;
638 if (chat_name != nullptr)
639 name = chat_name;
640 else
641 name = purple_chat_get_name(chat_);
643 PurpleConversation *conv =
644 purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT, name, account);
645 if (conv != nullptr)
646 purple_conversation_present(conv);
648 serv_join_chat(purple_account_get_connection(account), components);
650 g_free(chat_name);
653 const char *BuddyListChat::toString() const
655 return purple_chat_get_name(chat_);
658 BuddyListChat::ChatContextMenu::ChatContextMenu(BuddyListChat &parent_chat)
659 : ContextMenu(parent_chat), parent_chat_(&parent_chat)
661 appendProtocolMenu(purple_account_get_connection(
662 purple_chat_get_account(parent_chat_->getPurpleChat())));
663 appendExtendedMenu();
665 appendItem(
666 _("Alias..."), sigc::mem_fun(this, &ChatContextMenu::onChangeAlias));
667 appendItem(_("Delete..."), sigc::mem_fun(this, &ChatContextMenu::onRemove));
670 void BuddyListChat::ChatContextMenu::changeAliasResponseHandler(
671 CppConsUI::InputDialog &activator,
672 CppConsUI::AbstractDialog::ResponseType response)
674 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
675 return;
677 PurpleChat *chat = parent_chat_->getPurpleChat();
678 purple_blist_alias_chat(chat, activator.getText());
680 // Close context menu.
681 close();
684 void BuddyListChat::ChatContextMenu::onChangeAlias(Button & /*activator*/)
686 PurpleChat *chat = parent_chat_->getPurpleChat();
687 auto dialog =
688 new CppConsUI::InputDialog(_("Alias"), purple_chat_get_name(chat));
689 dialog->signal_response.connect(
690 sigc::mem_fun(this, &ChatContextMenu::changeAliasResponseHandler));
691 dialog->show();
694 void BuddyListChat::ChatContextMenu::removeResponseHandler(
695 CppConsUI::MessageDialog & /*activator*/,
696 CppConsUI::AbstractDialog::ResponseType response)
698 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
699 return;
701 PurpleChat *chat = parent_chat_->getPurpleChat();
703 // Close the context menu before the chat is deleted because its deletion can
704 // lead to destruction of this object.
705 close();
707 purple_blist_remove_chat(chat);
710 void BuddyListChat::ChatContextMenu::onRemove(Button & /*activator*/)
712 PurpleChat *chat = parent_chat_->getPurpleChat();
713 char *msg =
714 g_strdup_printf(_("Are you sure you want to delete chat %s from the list?"),
715 purple_chat_get_name(chat));
716 auto dialog = new CppConsUI::MessageDialog(_("Chat deletion"), msg);
717 g_free(msg);
718 dialog->signal_response.connect(
719 sigc::mem_fun(this, &ChatContextMenu::removeResponseHandler));
720 dialog->show();
723 void BuddyListChat::openContextMenu()
725 ContextMenu *w = new ChatContextMenu(*this);
726 w->show();
729 BuddyListChat::BuddyListChat(PurpleBlistNode *node) : BuddyListNode(node)
731 setColorScheme(CenterIM::SCHEME_BUDDYLISTCHAT);
733 chat_ = PURPLE_CHAT(blist_node_);
736 bool BuddyListContact::lessOrEqual(const BuddyListNode &other) const
738 const BuddyListContact *o = dynamic_cast<const BuddyListContact *>(&other);
739 if (o != nullptr) {
740 PurpleBuddy *left = purple_contact_get_priority_buddy(contact_);
741 PurpleBuddy *right = purple_contact_get_priority_buddy(o->contact_);
742 return lessOrEqualByBuddySort(left, right);
744 return lessOrEqualByType(other);
747 void BuddyListContact::update()
749 BuddyListNode::update();
751 PurpleBuddy *buddy = purple_contact_get_priority_buddy(contact_);
752 if (buddy == nullptr) {
753 // The contact does not have any associated buddy, ignore it until it gets a
754 // buddy assigned.
755 setText("*Contact*");
756 setVisibility(false);
757 return;
760 // Format contact size.
761 char *size;
762 if (contact_->currentsize > 1)
763 size = g_strdup_printf(" (%d)", contact_->currentsize);
764 else
765 size = nullptr;
767 // Format contact label.
768 const char *alias = purple_contact_get_alias(contact_);
769 const char *status = getBuddyStatus(buddy);
770 char *text;
771 if (status[0] != '\0')
772 text = g_strdup_printf("%s %s%s", status, alias, size ? size : "");
773 else
774 text = g_strdup_printf("%s%s", alias, size ? size : "");
775 setText(text);
776 g_free(size);
777 g_free(text);
779 sortIn();
781 updateColorScheme();
783 if (!purple_account_is_connected(purple_buddy_get_account(buddy))) {
784 // Hide if account is offline.
785 setVisibility(false);
787 else
788 setVisibility(status[0] != '\0' || BUDDYLIST->getShowOfflineBuddiesPref());
790 updateFilterVisibility(alias);
793 void BuddyListContact::onActivate(Button &activator)
795 PurpleBuddy *buddy = purple_contact_get_priority_buddy(contact_);
796 BuddyListNode *bnode = reinterpret_cast<BuddyListNode *>(
797 purple_blist_node_get_ui_data(PURPLE_BLIST_NODE(buddy)));
798 if (bnode != nullptr)
799 bnode->onActivate(activator);
802 const char *BuddyListContact::toString() const
804 return purple_contact_get_alias(contact_);
807 void BuddyListContact::setRefNode(CppConsUI::TreeView::NodeReference n)
809 BuddyListNode::setRefNode(n);
810 treeview_->setNodeStyle(n, CppConsUI::TreeView::STYLE_VOID);
813 void BuddyListContact::retrieveUserInfo()
815 PurpleBuddy *buddy = purple_contact_get_priority_buddy(contact_);
816 PurpleConnection *gc =
817 purple_account_get_connection(purple_buddy_get_account(buddy));
818 retrieveUserInfoForName(gc, purple_buddy_get_name(buddy));
821 BuddyListContact::ContactContextMenu::ContactContextMenu(
822 BuddyListContact &parent_contact)
823 : ContextMenu(parent_contact), parent_contact_(&parent_contact)
825 appendExtendedMenu();
827 if (parent_contact_->isCollapsed())
828 appendItem(_("Expand"),
829 sigc::bind(sigc::mem_fun(this, &ContactContextMenu::onExpandRequest),
830 true));
831 else
832 appendItem(_("Collapse"),
833 sigc::bind(sigc::mem_fun(this, &ContactContextMenu::onExpandRequest),
834 false));
836 appendItem(_("Information..."),
837 sigc::mem_fun(this, &ContactContextMenu::onInformation));
838 appendItem(
839 _("Alias..."), sigc::mem_fun(this, &ContactContextMenu::onChangeAlias));
840 appendItem(
841 _("Delete..."), sigc::mem_fun(this, &ContactContextMenu::onRemove));
843 auto groups = new CppConsUI::MenuWindow(*this, AUTOSIZE, AUTOSIZE);
845 for (PurpleBlistNode *node = purple_blist_get_root(); node != nullptr;
846 node = purple_blist_node_get_sibling_next(node)) {
847 if (!PURPLE_BLIST_NODE_IS_GROUP(node))
848 continue;
850 PurpleGroup *group = PURPLE_GROUP(node);
851 CppConsUI::Button *button = groups->appendItem(purple_group_get_name(group),
852 sigc::bind(sigc::mem_fun(this, &ContactContextMenu::onMoveTo), group));
853 if (purple_contact_get_group(parent_contact_->getPurpleContact()) == group)
854 button->grabFocus();
857 appendSubMenu(_("Move to..."), *groups);
860 void BuddyListContact::ContactContextMenu::onExpandRequest(
861 Button & /*activator*/, bool expand)
863 parent_contact_->setCollapsed(!expand);
864 close();
867 void BuddyListContact::ContactContextMenu::onInformation(Button & /*activator*/)
869 parent_contact_->retrieveUserInfo();
870 close();
873 void BuddyListContact::ContactContextMenu::changeAliasResponseHandler(
874 CppConsUI::InputDialog &activator,
875 CppConsUI::AbstractDialog::ResponseType response)
877 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
878 return;
880 PurpleContact *contact = parent_contact_->getPurpleContact();
881 if (contact->alias != nullptr)
882 purple_blist_alias_contact(contact, activator.getText());
883 else {
884 PurpleBuddy *buddy = purple_contact_get_priority_buddy(contact);
885 purple_blist_alias_buddy(buddy, activator.getText());
886 serv_alias_buddy(buddy);
889 // Close context menu.
890 close();
893 void BuddyListContact::ContactContextMenu::onChangeAlias(Button & /*activator*/)
895 PurpleContact *contact = parent_contact_->getPurpleContact();
896 auto dialog =
897 new CppConsUI::InputDialog(_("Alias"), purple_contact_get_alias(contact));
898 dialog->signal_response.connect(
899 sigc::mem_fun(this, &ContactContextMenu::changeAliasResponseHandler));
900 dialog->show();
903 void BuddyListContact::ContactContextMenu::removeResponseHandler(
904 CppConsUI::MessageDialog & /*activator*/,
905 CppConsUI::AbstractDialog::ResponseType response)
907 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
908 return;
910 // Based on gtkdialogs.c:pidgin_dialogs_remove_contact_cb().
911 PurpleContact *contact = parent_contact_->getPurpleContact();
912 PurpleBlistNode *cnode = PURPLE_BLIST_NODE(contact);
913 PurpleGroup *group = purple_contact_get_group(contact);
915 for (PurpleBlistNode *bnode = purple_blist_node_get_first_child(cnode);
916 bnode != nullptr; bnode = purple_blist_node_get_sibling_next(bnode)) {
917 PurpleBuddy *buddy = PURPLE_BUDDY(bnode);
918 PurpleAccount *account = purple_buddy_get_account(buddy);
919 if (purple_account_is_connected(account))
920 purple_account_remove_buddy(account, buddy, group);
923 // Close the context menu before the contact is deleted because its deletion
924 // can lead to destruction of this object.
925 close();
927 purple_blist_remove_contact(contact);
930 void BuddyListContact::ContactContextMenu::onRemove(Button & /*activator*/)
932 PurpleContact *contact = parent_contact_->getPurpleContact();
933 char *msg = g_strdup_printf(
934 _("Are you sure you want to delete contact %s from the list?"),
935 purple_contact_get_alias(contact));
936 auto dialog = new CppConsUI::MessageDialog(_("Contact deletion"), msg);
937 g_free(msg);
938 dialog->signal_response.connect(
939 sigc::mem_fun(this, &ContactContextMenu::removeResponseHandler));
940 dialog->show();
943 void BuddyListContact::ContactContextMenu::onMoveTo(
944 Button & /*activator*/, PurpleGroup *group)
946 PurpleContact *contact = parent_contact_->getPurpleContact();
947 close();
949 purple_blist_add_contact(contact, group, nullptr);
952 int BuddyListContact::getAttributes(
953 int property, int subproperty, int *attrs, CppConsUI::Error &error) const
955 if (BUDDYLIST->getColorizationMode() != BuddyList::COLOR_BY_ACCOUNT ||
956 property != CppConsUI::ColorScheme::PROPERTY_BUTTON_NORMAL)
957 return Button::getAttributes(property, subproperty, attrs, error);
959 // TODO Implement caching for these two properties.
960 PurpleAccount *account =
961 purple_buddy_get_account(purple_contact_get_priority_buddy(contact_));
962 int fg = purple_account_get_ui_int(account, "centerim5",
963 "buddylist-foreground-color", CppConsUI::Curses::Color::DEFAULT);
964 int bg = purple_account_get_ui_int(account, "centerim5",
965 "buddylist-background-color", CppConsUI::Curses::Color::DEFAULT);
967 CppConsUI::ColorScheme::Color c(fg, bg);
968 return COLORSCHEME->getColorPair(c, attrs, error);
971 void BuddyListContact::openContextMenu()
973 ContextMenu *w = new ContactContextMenu(*this);
974 w->show();
977 BuddyListContact::BuddyListContact(PurpleBlistNode *node) : BuddyListNode(node)
979 setColorScheme(CenterIM::SCHEME_BUDDYLISTCONTACT);
981 contact_ = PURPLE_CONTACT(blist_node_);
984 void BuddyListContact::updateColorScheme()
986 switch (BUDDYLIST->getColorizationMode()) {
987 case BuddyList::COLOR_BY_STATUS: {
988 PurpleBuddy *buddy = purple_contact_get_priority_buddy(contact_);
989 setColorScheme(
990 getColorSchemeByBuddy(CenterIM::SCHEME_BUDDYLISTCONTACT, buddy));
991 break;
993 default:
994 // Note: COLOR_BY_ACCOUNT case is handled by BuddyListContact::draw().
995 setColorScheme(CenterIM::SCHEME_BUDDYLISTCONTACT);
996 break;
1000 bool BuddyListGroup::lessOrEqual(const BuddyListNode &other) const
1002 // If the groups are not sorted but ordered manually then this method is not
1003 // used.
1005 const BuddyListGroup *o = dynamic_cast<const BuddyListGroup *>(&other);
1006 if (o != nullptr)
1007 return g_utf8_collate(purple_group_get_name(group_),
1008 purple_group_get_name(o->group_)) <= 0;
1009 return lessOrEqualByType(other);
1012 void BuddyListGroup::update()
1014 BuddyListNode::update();
1016 setText(purple_group_get_name(group_));
1018 // Sort in the group.
1019 BuddyList::GroupSortMode mode = BUDDYLIST->getGroupSortMode();
1020 switch (mode) {
1021 case BuddyList::GROUP_SORT_BY_USER: {
1022 // Note that the sorting below works even if there was a contact/chat/buddy
1023 // node that is attached at the root level of the blist treeview. This
1024 // happens when such a node was just created (the new_node() callback was
1025 // called) but the node does not have any parent yet.
1027 PurpleBlistNode *prev = purple_blist_node_get_sibling_prev(blist_node_);
1029 if (prev != nullptr) {
1030 // It better be a group node.
1031 g_assert(PURPLE_BLIST_NODE_IS_GROUP(prev));
1033 BuddyListNode *bnode =
1034 reinterpret_cast<BuddyListNode *>(purple_blist_node_get_ui_data(prev));
1035 // There has to be ui_data set for all group nodes!
1036 g_assert(bnode != nullptr);
1038 treeview_->moveNodeAfter(ref_, bnode->getRefNode());
1040 else {
1041 // The group is the first one in the list.
1042 CppConsUI::TreeView::NodeReference parent_ref = treeview_->getRootNode();
1043 treeview_->moveNodeBefore(ref_, parent_ref.begin());
1045 } break;
1046 case BuddyList::GROUP_SORT_BY_NAME:
1047 sortIn();
1048 break;
1051 bool vis = true;
1052 if (!BUDDYLIST->getShowEmptyGroupsPref())
1053 vis = purple_blist_get_group_size(group_, FALSE);
1054 setVisibility(vis);
1057 void BuddyListGroup::onActivate(Button & /*activator*/)
1059 treeview_->toggleCollapsed(ref_);
1060 purple_blist_node_set_bool(blist_node_, "collapsed", ref_->isCollapsed());
1063 const char *BuddyListGroup::toString() const
1065 return purple_group_get_name(group_);
1068 void BuddyListGroup::setRefNode(CppConsUI::TreeView::NodeReference n)
1070 BuddyListNode::setRefNode(n);
1071 initCollapsedState();
1074 void BuddyListGroup::initCollapsedState()
1076 // This cannot be done when the purple_blist_load() function was called
1077 // because node settings are unavailable at that time.
1078 treeview_->setCollapsed(
1079 ref_, purple_blist_node_get_bool(blist_node_, "collapsed"));
1082 BuddyListGroup::GroupContextMenu::GroupContextMenu(BuddyListGroup &parent_group)
1083 : ContextMenu(parent_group), parent_group_(&parent_group)
1085 appendExtendedMenu();
1087 appendItem(_("Rename..."), sigc::mem_fun(this, &GroupContextMenu::onRename));
1088 appendItem(_("Delete..."), sigc::mem_fun(this, &GroupContextMenu::onRemove));
1090 if (BUDDYLIST->getGroupSortMode() == BuddyList::GROUP_SORT_BY_USER) {
1091 // If the manual sorting is enabled then show a menu item and a submenu for
1092 // group moving.
1093 auto groups = new CppConsUI::MenuWindow(*this, AUTOSIZE, AUTOSIZE);
1095 groups->appendItem(_("-Top-"),
1096 sigc::bind(sigc::mem_fun(this, &GroupContextMenu::onMoveAfter),
1097 static_cast<PurpleGroup *>(nullptr)));
1098 for (PurpleBlistNode *node = purple_blist_get_root(); node != nullptr;
1099 node = purple_blist_node_get_sibling_next(node)) {
1100 if (!PURPLE_BLIST_NODE_IS_GROUP(node))
1101 continue;
1103 PurpleGroup *group = PURPLE_GROUP(node);
1104 groups->appendItem(purple_group_get_name(group),
1105 sigc::bind(sigc::mem_fun(this, &GroupContextMenu::onMoveAfter), group));
1108 appendSubMenu(_("Move after..."), *groups);
1112 void BuddyListGroup::GroupContextMenu::renameResponseHandler(
1113 CppConsUI::InputDialog &activator,
1114 CppConsUI::AbstractDialog::ResponseType response)
1116 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
1117 return;
1119 const char *name = activator.getText();
1120 PurpleGroup *group = parent_group_->getPurpleGroup();
1121 PurpleGroup *other = purple_find_group(name);
1122 if (other != nullptr &&
1123 !purple_utf8_strcasecmp(name, purple_group_get_name(group))) {
1124 LOG->message(_("Specified group is already in the list."));
1125 // TODO Add group merging. Note that purple_blist_rename_group() can do the
1126 // merging.
1128 else
1129 purple_blist_rename_group(group, name);
1131 // Close context menu.
1132 close();
1135 void BuddyListGroup::GroupContextMenu::onRename(Button & /*activator*/)
1137 PurpleGroup *group = parent_group_->getPurpleGroup();
1138 auto dialog =
1139 new CppConsUI::InputDialog(_("Rename"), purple_group_get_name(group));
1140 dialog->signal_response.connect(
1141 sigc::mem_fun(this, &GroupContextMenu::renameResponseHandler));
1142 dialog->show();
1145 void BuddyListGroup::GroupContextMenu::removeResponseHandler(
1146 CppConsUI::MessageDialog & /*activator*/,
1147 CppConsUI::AbstractDialog::ResponseType response)
1149 if (response != CppConsUI::AbstractDialog::RESPONSE_OK)
1150 return;
1152 // Based on gtkdialogs.c:pidgin_dialogs_remove_group_cb().
1153 PurpleGroup *group = parent_group_->getPurpleGroup();
1154 PurpleBlistNode *cnode =
1155 purple_blist_node_get_first_child(PURPLE_BLIST_NODE(group));
1156 while (cnode != nullptr) {
1157 if (PURPLE_BLIST_NODE_IS_CONTACT(cnode)) {
1158 PurpleBlistNode *bnode = purple_blist_node_get_first_child(cnode);
1159 cnode = purple_blist_node_get_sibling_next(cnode);
1160 while (bnode != nullptr)
1161 if (PURPLE_BLIST_NODE_IS_BUDDY(bnode)) {
1162 PurpleBuddy *buddy = PURPLE_BUDDY(bnode);
1163 PurpleAccount *account = purple_buddy_get_account(buddy);
1164 bnode = purple_blist_node_get_sibling_next(bnode);
1165 if (purple_account_is_connected(account)) {
1166 purple_account_remove_buddy(account, buddy, group);
1167 purple_blist_remove_buddy(buddy);
1170 else
1171 bnode = purple_blist_node_get_sibling_next(bnode);
1173 else if (PURPLE_BLIST_NODE_IS_CHAT(cnode)) {
1174 PurpleChat *chat = PURPLE_CHAT(cnode);
1175 cnode = purple_blist_node_get_sibling_next(cnode);
1176 purple_blist_remove_chat(chat);
1178 else
1179 cnode = purple_blist_node_get_sibling_next(cnode);
1182 // Close the context menu before the group is deleted because its deletion can
1183 // lead to destruction of this object.
1184 close();
1186 purple_blist_remove_group(group);
1189 void BuddyListGroup::GroupContextMenu::onRemove(Button & /*activator*/)
1191 PurpleGroup *group = parent_group_->getPurpleGroup();
1192 char *msg = g_strdup_printf(
1193 _("Are you sure you want to delete group %s from the list?"),
1194 purple_group_get_name(group));
1195 auto dialog = new CppConsUI::MessageDialog(_("Group deletion"), msg);
1196 g_free(msg);
1197 dialog->signal_response.connect(
1198 sigc::mem_fun(this, &GroupContextMenu::removeResponseHandler));
1199 dialog->show();
1202 void BuddyListGroup::GroupContextMenu::onMoveAfter(
1203 Button & /*activator*/, PurpleGroup *group)
1205 PurpleGroup *moved_group = parent_group_->getPurpleGroup();
1206 close();
1208 purple_blist_add_group(moved_group, PURPLE_BLIST_NODE(group));
1211 void BuddyListGroup::openContextMenu()
1213 ContextMenu *w = new GroupContextMenu(*this);
1214 w->show();
1217 BuddyListGroup::BuddyListGroup(PurpleBlistNode *node) : BuddyListNode(node)
1219 setColorScheme(CenterIM::SCHEME_BUDDYLISTGROUP);
1221 group_ = PURPLE_GROUP(blist_node_);
1224 // vim: set tabstop=2 shiftwidth=2 textwidth=80 expandtab: