/* * Pidgin - Internet Messenger * Copyright (C) Pidgin Developers * * Pidgin is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * source distribution. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include #include #include "pidgindisplaywindow.h" #include "gtkconv.h" #include "gtkdialogs.h" #include "gtkutils.h" #include "pidgindisplayitem.h" #include "pidgininvitedialog.h" enum { SIG_CONVERSATION_SWITCHED, N_SIGNALS, }; static guint signals[N_SIGNALS] = {0, }; struct _PidginDisplayWindow { GtkApplicationWindow parent; GtkWidget *view; GtkWidget *bin; GListModel *base_model; GListModel *selection_model; GListStore *conversation_model; }; G_DEFINE_TYPE(PidginDisplayWindow, pidgin_display_window, GTK_TYPE_APPLICATION_WINDOW) static GtkWidget *default_window = NULL; /****************************************************************************** * Helpers *****************************************************************************/ static void pidgin_display_window_actions_set_enabled(GActionMap *map, const gchar **actions, gboolean enabled) { for(int i = 0; actions[i] != NULL; i++) { GAction *action = NULL; const gchar *name = actions[i]; action = g_action_map_lookup_action(map, name); if(action != NULL) { g_simple_action_set_enabled(G_SIMPLE_ACTION(action), enabled); } } } static GListModel * pidgin_display_window_create_model(GObject *item, G_GNUC_UNUSED gpointer data) { GListModel *model = NULL; model = pidgin_display_item_get_children(PIDGIN_DISPLAY_ITEM(item)); if(model != NULL) { return g_object_ref(model); } return NULL; } static gboolean pidgin_display_window_find_conversation(gconstpointer a, gconstpointer b) { PidginDisplayItem *item_a = PIDGIN_DISPLAY_ITEM((gpointer)a); PidginDisplayItem *item_b = PIDGIN_DISPLAY_ITEM((gpointer)b); PurpleConversation *conversation_a = NULL; PurpleConversation *conversation_b = NULL; conversation_a = g_object_get_data(G_OBJECT(item_a), "conversation"); conversation_b = g_object_get_data(G_OBJECT(item_b), "conversation"); return (conversation_a == conversation_b); } /****************************************************************************** * Callbacks *****************************************************************************/ static void pidgin_display_window_invite_cb(GtkDialog *dialog, gint response_id, G_GNUC_UNUSED gpointer data) { PidginInviteDialog *invite_dialog = PIDGIN_INVITE_DIALOG(dialog); PurpleChatConversation *chat = NULL; chat = pidgin_invite_dialog_get_conversation(invite_dialog); g_object_set_data(G_OBJECT(chat), "pidgin-invite-dialog", NULL); if(response_id == GTK_RESPONSE_ACCEPT) { const gchar *contact = NULL, *message = NULL; contact = pidgin_invite_dialog_get_contact(invite_dialog); message = pidgin_invite_dialog_get_message(invite_dialog); if(!purple_strequal(contact, "")) { PurpleConnection *connection = NULL; connection = purple_conversation_get_connection(PURPLE_CONVERSATION(chat)); purple_serv_chat_invite(connection, purple_chat_conversation_get_id(chat), message, contact); } } gtk_window_destroy(GTK_WINDOW(invite_dialog)); } /****************************************************************************** * Actions *****************************************************************************/ static void pidgin_display_window_alias(G_GNUC_UNUSED GSimpleAction *simple, G_GNUC_UNUSED GVariant *parameter, gpointer data) { PidginDisplayWindow *window = data; PurpleConversation *selected = NULL; selected = pidgin_display_window_get_selected(window); if(PURPLE_IS_CONVERSATION(selected)) { PurpleAccount *account; const gchar *name; account = purple_conversation_get_account(selected); name = purple_conversation_get_name(selected); if(PURPLE_IS_IM_CONVERSATION(selected)) { PurpleBuddy *buddy = purple_blist_find_buddy(account, name); if(PURPLE_IS_BUDDY(buddy)) { pidgin_dialogs_alias_buddy(buddy); } } else if(PURPLE_IS_CHAT_CONVERSATION(selected)) { PurpleChat *chat = purple_blist_find_chat(account, name); if(PURPLE_IS_CHAT(chat)) { pidgin_dialogs_alias_chat(chat); } } } } static void pidgin_display_window_close_conversation(G_GNUC_UNUSED GSimpleAction *simple, G_GNUC_UNUSED GVariant *parameter, gpointer data) { PidginDisplayWindow *window = data; PurpleConversation *selected = NULL; selected = pidgin_display_window_get_selected(window); if(PURPLE_IS_CONVERSATION(selected)) { pidgin_display_window_remove(window, selected); pidgin_conversation_detach(selected); } } static void pidgin_display_window_get_info(G_GNUC_UNUSED GSimpleAction *simple, G_GNUC_UNUSED GVariant *parameter, gpointer data) { PidginDisplayWindow *window = data; PurpleConversation *selected = NULL; selected = pidgin_display_window_get_selected(window); if(PURPLE_IS_CONVERSATION(selected)) { if(PURPLE_IS_IM_CONVERSATION(selected)) { PurpleConnection *connection = NULL; connection = purple_conversation_get_connection(selected); pidgin_retrieve_user_info(connection, purple_conversation_get_name(selected)); } } } static void pidgin_display_window_invite(G_GNUC_UNUSED GSimpleAction *simple, G_GNUC_UNUSED GVariant *parameter, gpointer data) { PidginDisplayWindow *window = data; PurpleConversation *selected = NULL; selected = pidgin_display_window_get_selected(window); if(PURPLE_IS_CHAT_CONVERSATION(selected)) { GtkWidget *invite_dialog = NULL; invite_dialog = g_object_get_data(G_OBJECT(selected), "pidgin-invite-dialog"); if(!GTK_IS_WIDGET(invite_dialog)) { invite_dialog = pidgin_invite_dialog_new(PURPLE_CHAT_CONVERSATION(selected)); g_object_set_data(G_OBJECT(selected), "pidgin-invite-dialog", invite_dialog); gtk_window_set_transient_for(GTK_WINDOW(invite_dialog), GTK_WINDOW(window)); gtk_window_set_destroy_with_parent(GTK_WINDOW(invite_dialog), TRUE); g_signal_connect(invite_dialog, "response", G_CALLBACK(pidgin_display_window_invite_cb), NULL); } gtk_widget_set_visible(invite_dialog, TRUE); } } static void pidgin_display_window_send_file(G_GNUC_UNUSED GSimpleAction *simple, G_GNUC_UNUSED GVariant *parameter, gpointer data) { PidginDisplayWindow *window = data; PurpleConversation *selected = NULL; selected = pidgin_display_window_get_selected(window); if(PURPLE_IS_IM_CONVERSATION(selected)) { PurpleConnection *connection = NULL; connection = purple_conversation_get_connection(selected); purple_serv_send_file(connection, purple_conversation_get_name(selected), NULL); } } static GActionEntry win_entries[] = { { .name = "alias", .activate = pidgin_display_window_alias }, { .name = "close", .activate = pidgin_display_window_close_conversation }, { .name = "get-info", .activate = pidgin_display_window_get_info }, { .name = "invite", .activate = pidgin_display_window_invite }, { .name = "send-file", .activate = pidgin_display_window_send_file } }; /* * pidgin_display_window_conversation_actions: * * A list of action names that are only valid if a conversation is selected. */ static const gchar *pidgin_display_window_conversation_actions[] = { "alias", "close", "get-info", NULL }; static const gchar *pidgin_display_window_im_conversation_actions[] = { "send-file", NULL }; static const gchar *pidgin_display_window_chat_conversation_actions[] = { "invite", NULL }; /****************************************************************************** * Callbacks *****************************************************************************/ static gboolean pidgin_display_window_key_pressed_cb(G_GNUC_UNUSED GtkEventControllerKey *controller, guint keyval, G_GNUC_UNUSED guint keycode, GdkModifierType state, gpointer data) { PidginDisplayWindow *window = data; if (state & GDK_CONTROL_MASK) { switch (keyval) { case GDK_KEY_Page_Down: case GDK_KEY_KP_Page_Down: case ']': pidgin_display_window_select_next(window); return TRUE; break; case GDK_KEY_Home: pidgin_display_window_select_first(window); return TRUE; break; case GDK_KEY_End: pidgin_display_window_select_last(window); return TRUE; break; case GDK_KEY_Page_Up: case GDK_KEY_KP_Page_Up: case '[': pidgin_display_window_select_previous(window); return TRUE; break; } } else if (state & GDK_ALT_MASK) { if ('1' <= keyval && keyval <= '9') { guint switchto = keyval - '1'; pidgin_display_window_select_nth(window, switchto); return TRUE; } } return FALSE; } static void pidgin_display_window_selected_item_changed_cb(GObject *self, G_GNUC_UNUSED GParamSpec *pspec, gpointer data) { PidginDisplayItem *item = NULL; PidginDisplayWindow *window = data; PurpleConversation *conversation = NULL; GtkSingleSelection *selection = GTK_SINGLE_SELECTION(self); GtkTreeListRow *row = NULL; GtkWidget *widget = NULL; gboolean is_conversation = FALSE; gboolean is_im_conversation = FALSE; gboolean is_chat_conversation = FALSE; row = gtk_single_selection_get_selected_item(selection); item = gtk_tree_list_row_get_item(row); /* Toggle whether actions should be enabled or disabled. */ conversation = g_object_get_data(G_OBJECT(item), "conversation"); if(PURPLE_IS_CONVERSATION(conversation)) { is_conversation = PURPLE_IS_CONVERSATION(conversation); is_im_conversation = PURPLE_IS_IM_CONVERSATION(conversation); is_chat_conversation = PURPLE_IS_CHAT_CONVERSATION(conversation); } pidgin_display_window_actions_set_enabled(G_ACTION_MAP(window), pidgin_display_window_conversation_actions, is_conversation); pidgin_display_window_actions_set_enabled(G_ACTION_MAP(window), pidgin_display_window_im_conversation_actions, is_im_conversation); pidgin_display_window_actions_set_enabled(G_ACTION_MAP(window), pidgin_display_window_chat_conversation_actions, is_chat_conversation); widget = pidgin_display_item_get_widget(item); if(GTK_IS_WIDGET(widget)) { adw_bin_set_child(ADW_BIN(window->bin), widget); } } /****************************************************************************** * GObject Implementation *****************************************************************************/ static void pidgin_display_window_dispose(GObject *obj) { PidginDisplayWindow *window = PIDGIN_DISPLAY_WINDOW(obj); g_clear_object(&window->conversation_model); G_OBJECT_CLASS(pidgin_display_window_parent_class)->dispose(obj); } static void pidgin_display_window_init(PidginDisplayWindow *window) { GtkEventController *key = NULL; GtkTreeListModel *tree_model = NULL; gtk_widget_init_template(GTK_WIDGET(window)); /* Setup the tree list model. */ tree_model = gtk_tree_list_model_new(window->base_model, FALSE, TRUE, (GtkTreeListModelCreateModelFunc)pidgin_display_window_create_model, window, NULL); /* Set the model of the selection to the tree model. */ gtk_single_selection_set_model(GTK_SINGLE_SELECTION(window->selection_model), G_LIST_MODEL(tree_model)); g_clear_object(&tree_model); /* Set the application and add all of our actions. */ gtk_window_set_application(GTK_WINDOW(window), GTK_APPLICATION(g_application_get_default())); g_action_map_add_action_entries(G_ACTION_MAP(window), win_entries, G_N_ELEMENTS(win_entries), window); /* Add a key controller. */ key = gtk_event_controller_key_new(); gtk_event_controller_set_propagation_phase(key, GTK_PHASE_CAPTURE); g_signal_connect(G_OBJECT(key), "key-pressed", G_CALLBACK(pidgin_display_window_key_pressed_cb), window); gtk_widget_add_controller(GTK_WIDGET(window), key); } static void pidgin_display_window_class_init(PidginDisplayWindowClass *klass) { GObjectClass *obj_class = G_OBJECT_CLASS(klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); obj_class->dispose = pidgin_display_window_dispose; /** * PidginDisplayWindow::conversation-switched: * @window: The conversation window. * @new_conv: The now active conversation. * * Emitted when a window switched from one conversation to another. */ signals[SIG_CONVERSATION_SWITCHED] = g_signal_new_class_handler( "conversation-switched", G_OBJECT_CLASS_TYPE(obj_class), G_SIGNAL_RUN_LAST, NULL, NULL, NULL, NULL, G_TYPE_NONE, 1, PURPLE_TYPE_CONVERSATION ); gtk_widget_class_set_template_from_resource( widget_class, "/im/pidgin/Pidgin3/Display/window.ui" ); gtk_widget_class_bind_template_child(widget_class, PidginDisplayWindow, view); gtk_widget_class_bind_template_child(widget_class, PidginDisplayWindow, bin); gtk_widget_class_bind_template_child(widget_class, PidginDisplayWindow, base_model); gtk_widget_class_bind_template_child(widget_class, PidginDisplayWindow, selection_model); gtk_widget_class_bind_template_child(widget_class, PidginDisplayWindow, conversation_model); gtk_widget_class_bind_template_callback(widget_class, pidgin_display_window_key_pressed_cb); gtk_widget_class_bind_template_callback(widget_class, pidgin_display_window_selected_item_changed_cb); } /****************************************************************************** * API *****************************************************************************/ GtkWidget * pidgin_display_window_get_default(void) { if(!GTK_IS_WIDGET(default_window)) { default_window = pidgin_display_window_new(); g_object_add_weak_pointer(G_OBJECT(default_window), (gpointer)&default_window); } return default_window; } GtkWidget * pidgin_display_window_new(void) { return g_object_new( PIDGIN_TYPE_DISPLAY_WINDOW, "show-menubar", TRUE, NULL); } void pidgin_display_window_add(PidginDisplayWindow *window, PurpleConversation *conversation) { PidginConversation *gtkconv = NULL; g_return_if_fail(PIDGIN_IS_DISPLAY_WINDOW(window)); g_return_if_fail(PURPLE_IS_CONVERSATION(conversation)); gtkconv = PIDGIN_CONVERSATION(conversation); if(gtkconv != NULL) { PidginDisplayItem *item = NULL; const char *value = NULL; GtkWidget *parent = gtk_widget_get_parent(gtkconv->tab_cont); if(GTK_IS_WIDGET(parent)) { g_object_ref(gtkconv->tab_cont); gtk_widget_unparent(gtkconv->tab_cont); } value = purple_conversation_get_name(conversation); item = pidgin_display_item_new(gtkconv->tab_cont, value); g_object_set_data(G_OBJECT(item), "conversation", conversation); g_object_bind_property(conversation, "title", item, "title", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); g_list_store_append(window->conversation_model, item); g_clear_object(&item); if(GTK_IS_WIDGET(parent)) { g_object_unref(gtkconv->tab_cont); } } } void pidgin_display_window_remove(PidginDisplayWindow *window, PurpleConversation *conversation) { PidginDisplayItem *item = NULL; guint position = 0; gboolean found = FALSE; gchar *id = NULL; g_return_if_fail(PIDGIN_IS_DISPLAY_WINDOW(window)); g_return_if_fail(PURPLE_IS_CONVERSATION(conversation)); /* Create a wrapper item for our find function. */ id = g_uuid_string_random(); item = g_object_new(PIDGIN_TYPE_DISPLAY_ITEM, "id", id, NULL); g_free(id); g_object_set_data(G_OBJECT(item), "conversation", conversation); found = g_list_store_find_with_equal_func(window->conversation_model, item, pidgin_display_window_find_conversation, &position); g_clear_object(&item); if(found) { g_list_store_remove(window->conversation_model, position); } } guint pidgin_display_window_get_count(G_GNUC_UNUSED PidginDisplayWindow *window) { /* TODO: This is only used by the gestures plugin and that will probably * need some rewriting and different api for a mixed content window list * this is now. */ return 0; } PurpleConversation * pidgin_display_window_get_selected(PidginDisplayWindow *window) { GtkSingleSelection *selection = NULL; GtkTreeListRow *tree_row = NULL; GObject *selected = NULL; g_return_val_if_fail(PIDGIN_IS_DISPLAY_WINDOW(window), NULL); selection = GTK_SINGLE_SELECTION(window->selection_model); tree_row = gtk_single_selection_get_selected_item(selection); selected = gtk_tree_list_row_get_item(tree_row); if(PIDGIN_IS_DISPLAY_ITEM(selected)) { return g_object_get_data(selected, "conversation"); } return NULL; } void pidgin_display_window_select(PidginDisplayWindow *window, PurpleConversation *conversation) { /* TODO: This is used by the unity and gestures plugins, but I'm really not * sure how to make this work yet without some hard-coding or something, so * I'm opting to stub it out for now. */ g_return_if_fail(PIDGIN_IS_DISPLAY_WINDOW(window)); g_return_if_fail(PURPLE_IS_CONVERSATION(conversation)); } void pidgin_display_window_select_previous(PidginDisplayWindow *window) { GtkSingleSelection *selection = NULL; guint position = 0; g_return_if_fail(PIDGIN_IS_DISPLAY_WINDOW(window)); selection = GTK_SINGLE_SELECTION(window->selection_model); position = gtk_single_selection_get_selected(selection); if(position == 0) { position = g_list_model_get_n_items(G_LIST_MODEL(selection)) - 1; } else { position = position - 1; } gtk_single_selection_set_selected(selection, position); } void pidgin_display_window_select_next(PidginDisplayWindow *window) { GtkSingleSelection *selection = NULL; guint position = 0; g_return_if_fail(PIDGIN_IS_DISPLAY_WINDOW(window)); selection = GTK_SINGLE_SELECTION(window->selection_model); position = gtk_single_selection_get_selected(selection); if(position + 1 >= g_list_model_get_n_items(G_LIST_MODEL(selection))) { position = 0; } else { position = position + 1; } gtk_single_selection_set_selected(selection, position); } void pidgin_display_window_select_first(PidginDisplayWindow *window) { GtkSingleSelection *selection = NULL; g_return_if_fail(PIDGIN_IS_DISPLAY_WINDOW(window)); selection = GTK_SINGLE_SELECTION(window->selection_model); /* The selection has autoselect set to true, which won't do anything if * this is an invalid value. */ gtk_single_selection_set_selected(selection, 0); } void pidgin_display_window_select_last(PidginDisplayWindow *window) { GtkSingleSelection *selection = NULL; guint n_items = 0; g_return_if_fail(PIDGIN_IS_DISPLAY_WINDOW(window)); selection = GTK_SINGLE_SELECTION(window->selection_model); n_items = g_list_model_get_n_items(G_LIST_MODEL(selection)); /* The selection has autoselect set to true, which won't do anything if * this is an invalid value. */ gtk_single_selection_set_selected(selection, n_items - 1); } void pidgin_display_window_select_nth(PidginDisplayWindow *window, guint nth) { GtkSingleSelection *selection = NULL; guint n_items = 0; g_return_if_fail(PIDGIN_IS_DISPLAY_WINDOW(window)); selection = GTK_SINGLE_SELECTION(window->selection_model); /* The selection has autoselect set to true, but this isn't bound checking * or something on the children models, so we verify before setting. */ n_items = g_list_model_get_n_items(G_LIST_MODEL(selection)); if(nth < n_items) { gtk_single_selection_set_selected(selection, nth); } } gboolean pidgin_display_window_conversation_is_selected(PidginDisplayWindow *window, PurpleConversation *conversation) { GtkSingleSelection *selection = NULL; GObject *selected = NULL; g_return_val_if_fail(PIDGIN_IS_DISPLAY_WINDOW(window), FALSE); g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE); selection = GTK_SINGLE_SELECTION(window->selection_model); selected = gtk_single_selection_get_selected_item(selection); if(PIDGIN_IS_DISPLAY_ITEM(selected)) { PurpleConversation *selected_conversation = NULL; selected_conversation = g_object_get_data(G_OBJECT(selected), "conversation"); if(selected_conversation != NULL) { return (selected_conversation == conversation); } } return FALSE; }