]> git.0d.be Git - empathy.git/blob - src/empathy-accounts-dialog.c
connect to signals when account has been created
[empathy.git] / src / empathy-accounts-dialog.c
1 /*
2  * Copyright (C) 2005-2007 Imendio AB
3  * Copyright (C) 2007-2009 Collabora Ltd.
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License as
7  * published by the Free Software Foundation; either version 2 of the
8  * License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public
16  * License along with this program; if not, write to the
17  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
18  * Boston, MA  02110-1301  USA
19  *
20  * Authors: Martyn Russell <martyn@imendio.com>
21  *          Xavier Claessens <xclaesse@gmail.com>
22  *          Cosimo Cecchi <cosimo.cecchi@collabora.co.uk>
23  *          Jonathan Tellier <jonathan.tellier@gmail.com>
24  */
25
26 #include <config.h>
27
28 #include <string.h>
29 #include <stdlib.h>
30
31 #include <gtk/gtk.h>
32 #include <glib/gi18n.h>
33 #include <dbus/dbus-glib.h>
34
35 #include <telepathy-glib/account-manager.h>
36 #include <telepathy-glib/util.h>
37
38 #include <libempathy/empathy-utils.h>
39 #include <libempathy/empathy-connection-managers.h>
40 #include <libempathy/empathy-connectivity.h>
41 #include <libempathy-gtk/empathy-ui-utils.h>
42
43 #include <libempathy-gtk/empathy-protocol-chooser.h>
44 #include <libempathy-gtk/empathy-account-widget.h>
45 #include <libempathy-gtk/empathy-account-widget-irc.h>
46 #include <libempathy-gtk/empathy-account-widget-sip.h>
47 #include <libempathy-gtk/empathy-cell-renderer-activatable.h>
48 #include <libempathy-gtk/empathy-conf.h>
49 #include <libempathy-gtk/empathy-images.h>
50
51 #include "empathy-accounts-dialog.h"
52 #include "empathy-import-dialog.h"
53 #include "empathy-import-utils.h"
54 #include "ephy-spinner.h"
55
56 #define DEBUG_FLAG EMPATHY_DEBUG_ACCOUNT
57 #include <libempathy/empathy-debug.h>
58
59 /* Flashing delay for icons (milliseconds). */
60 #define FLASH_TIMEOUT 500
61
62 /* The primary text of the dialog shown to the user when he is about to lose
63  * unsaved changes */
64 #define PENDING_CHANGES_QUESTION_PRIMARY_TEXT \
65   _("There are unsaved modifications to your %s account.")
66 /* The primary text of the dialog shown to the user when he is about to lose
67  * an unsaved new account */
68 #define UNSAVED_NEW_ACCOUNT_QUESTION_PRIMARY_TEXT \
69   _("Your new account has not been saved yet.")
70
71 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyAccountsDialog)
72 G_DEFINE_TYPE (EmpathyAccountsDialog, empathy_accounts_dialog, GTK_TYPE_DIALOG);
73
74 static EmpathyAccountsDialog *dialog_singleton = NULL;
75
76 typedef struct {
77   GtkWidget *alignment_settings;
78   GtkWidget *alignment_infobar;
79
80   GtkWidget *vbox_details;
81   GtkWidget *infobar;
82   GtkWidget *label_status;
83   GtkWidget *image_status;
84   GtkWidget *throbber;
85   GtkWidget *frame_no_protocol;
86
87   GtkWidget *treeview;
88
89   GtkWidget *button_add;
90   GtkWidget *button_import;
91
92   GtkWidget *combobox_protocol;
93   GtkWidget *hbox_protocol;
94
95   GtkWidget *image_type;
96   GtkWidget *label_name;
97   GtkWidget *label_type;
98   GtkWidget *settings_widget;
99
100   /* We have to keep a reference on the actual EmpathyAccountWidget, not just
101    * his GtkWidget. It is the only reliable source we can query to know if
102    * there are any unsaved changes to the currently selected account. We can't
103    * look at the account settings because it does not contain everything that
104    * can be changed using the EmpathyAccountWidget. For instance, it does not
105    * contain the state of the "Enabled" checkbox. */
106   EmpathyAccountWidget *setting_widget_object;
107
108   gboolean  connecting_show;
109   guint connecting_id;
110
111   gulong  settings_ready_id;
112   EmpathyAccountSettings *settings_ready;
113
114   TpAccountManager *account_manager;
115   EmpathyConnectionManagers *cms;
116
117   GtkWindow *parent_window;
118   TpAccount *initial_selection;
119
120   /* Those are needed when changing the selected row. When a user selects
121    * another account and there are unsaved changes on the currently selected
122    * one, a confirmation message box is presented to him. Since his answer
123    * is retrieved asynchronously, we keep some information as member of the
124    * EmpathyAccountsDialog object. */
125   gboolean force_change_row;
126   GtkTreeRowReference *destination_row;
127
128   gboolean dispose_has_run;
129 } EmpathyAccountsDialogPriv;
130
131 enum {
132   COL_NAME,
133   COL_STATUS,
134   COL_ACCOUNT_POINTER,
135   COL_ACCOUNT_SETTINGS_POINTER,
136   COL_COUNT
137 };
138
139 enum {
140   PROP_PARENT = 1
141 };
142
143 static EmpathyAccountSettings * accounts_dialog_model_get_selected_settings (
144     EmpathyAccountsDialog *dialog);
145
146 static gboolean accounts_dialog_get_settings_iter (
147     EmpathyAccountsDialog *dialog,
148     EmpathyAccountSettings *settings,
149     GtkTreeIter *iter);
150
151 static void accounts_dialog_model_select_first (EmpathyAccountsDialog *dialog);
152
153 static void accounts_dialog_update_settings (EmpathyAccountsDialog *dialog,
154     EmpathyAccountSettings *settings);
155
156 static void accounts_dialog_add (EmpathyAccountsDialog *dialog,
157     EmpathyAccountSettings *settings);
158
159 static void accounts_dialog_model_set_selected (EmpathyAccountsDialog *dialog,
160     EmpathyAccountSettings *settings);
161
162 static void accounts_dialog_connection_changed_cb (TpAccount *account,
163     guint old_status,
164     guint current,
165     guint reason,
166     gchar *dbus_error_name,
167     GHashTable *details,
168     EmpathyAccountsDialog *dialog);
169
170 static void accounts_dialog_presence_changed_cb (TpAccount *account,
171     guint presence,
172     gchar *status,
173     gchar *status_message,
174     EmpathyAccountsDialog *dialog);
175
176 static void
177 accounts_dialog_update_name_label (EmpathyAccountsDialog *dialog,
178     const gchar *display_name)
179 {
180   gchar *text;
181   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
182
183   text = g_markup_printf_escaped ("<big><b>%s</b></big>", display_name);
184   gtk_label_set_markup (GTK_LABEL (priv->label_name), text);
185
186   g_free (text);
187 }
188
189 static void
190 accounts_dialog_update_status_infobar (EmpathyAccountsDialog *dialog,
191     TpAccount *account)
192 {
193   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
194   const gchar               *message;
195   gchar                     *status_message = NULL;
196   guint                     status;
197   guint                     reason;
198   guint                     presence;
199   EmpathyConnectivity       *connectivity;
200   GtkTreeView               *view;
201   GtkTreeModel              *model;
202   GtkTreeSelection          *selection;
203   GtkTreeIter               iter;
204   TpAccount                 *selected_account;
205   gboolean                  account_enabled;
206   gboolean                  creating_account;
207
208   view = GTK_TREE_VIEW (priv->treeview);
209   selection = gtk_tree_view_get_selection (view);
210
211   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
212     return;
213
214   gtk_tree_model_get (model, &iter,
215       COL_ACCOUNT_POINTER, &selected_account, -1);
216
217   /* do not update the infobar when the account is not selected */
218   if (account != selected_account)
219     return;
220
221   if (account != NULL)
222     {
223       status = tp_account_get_connection_status (account, &reason);
224       presence = tp_account_get_current_presence (account, NULL, &status_message);
225       account_enabled = tp_account_is_enabled (account);
226       creating_account = FALSE;
227     }
228   else
229     {
230       status = TP_CONNECTION_STATUS_DISCONNECTED;
231       presence = TP_CONNECTION_PRESENCE_TYPE_OFFLINE;
232       account_enabled = FALSE;
233       creating_account = TRUE;
234     }
235
236   gtk_image_set_from_icon_name (GTK_IMAGE (priv->image_status),
237       empathy_icon_name_for_presence (presence), GTK_ICON_SIZE_SMALL_TOOLBAR);
238
239   if (account_enabled)
240     {
241       switch (status)
242         {
243           case TP_CONNECTION_STATUS_CONNECTING:
244             message = _("Connecting…");
245             gtk_info_bar_set_message_type (GTK_INFO_BAR (priv->infobar),
246                 GTK_MESSAGE_INFO);
247
248             ephy_spinner_start (EPHY_SPINNER (priv->throbber));
249             gtk_widget_show (priv->throbber);
250             gtk_widget_hide (priv->image_status);
251             break;
252           case TP_CONNECTION_STATUS_CONNECTED:
253             if (g_strcmp0 (status_message, "") == 0)
254               {
255                 message = g_strdup_printf ("%s",
256                     empathy_presence_get_default_message (presence));
257               }
258             else
259               {
260                 message = g_strdup_printf ("%s â€” %s",
261                     empathy_presence_get_default_message (presence),
262                     status_message);
263               }
264             gtk_info_bar_set_message_type (GTK_INFO_BAR (priv->infobar),
265                 GTK_MESSAGE_INFO);
266
267             gtk_widget_show (priv->image_status);
268             gtk_widget_hide (priv->throbber);
269             break;
270           case TP_CONNECTION_STATUS_DISCONNECTED:
271             message = g_strdup_printf (_("Disconnected â€” %s"),
272                 empathy_status_reason_get_default_message (reason));
273
274             if (reason == TP_CONNECTION_STATUS_REASON_REQUESTED)
275               {
276                 message = g_strdup_printf (_("Offline â€” %s"),
277                     empathy_status_reason_get_default_message (reason));
278                 gtk_info_bar_set_message_type (GTK_INFO_BAR (priv->infobar),
279                     GTK_MESSAGE_WARNING);
280               }
281             else
282               {
283                 gtk_info_bar_set_message_type (GTK_INFO_BAR (priv->infobar),
284                     GTK_MESSAGE_ERROR);
285               }
286
287             connectivity = empathy_connectivity_dup_singleton ();
288             if (!empathy_connectivity_is_online (connectivity))
289                message = _("Offline â€” No Network Connection");
290
291             g_object_unref (connectivity);
292             ephy_spinner_stop (EPHY_SPINNER (priv->throbber));
293             gtk_widget_show (priv->image_status);
294             gtk_widget_hide (priv->throbber);
295             break;
296           default:
297             message = _("Unknown Status");
298             gtk_info_bar_set_message_type (GTK_INFO_BAR (priv->infobar),
299                 GTK_MESSAGE_WARNING);
300
301             ephy_spinner_stop (EPHY_SPINNER (priv->throbber));
302             gtk_widget_hide (priv->image_status);
303             gtk_widget_hide (priv->throbber);
304         }
305     }
306   else
307     {
308       message = _("Offline â€” Account Disabled");
309
310       gtk_info_bar_set_message_type (GTK_INFO_BAR (priv->infobar),
311           GTK_MESSAGE_WARNING);
312       ephy_spinner_stop (EPHY_SPINNER (priv->throbber));
313       gtk_widget_show (priv->image_status);
314       gtk_widget_hide (priv->throbber);
315     }
316
317   gtk_label_set_text (GTK_LABEL (priv->label_status), message);
318   gtk_widget_show (priv->label_status);
319
320   if (!creating_account)
321     gtk_widget_show (priv->infobar);
322   else
323     gtk_widget_hide (priv->infobar);
324
325   g_free (status_message);
326 }
327
328 static void
329 empathy_account_dialog_widget_cancelled_cb (
330     EmpathyAccountWidget *widget_object,
331     EmpathyAccountsDialog *dialog)
332 {
333   GtkTreeView *view;
334   GtkTreeModel *model;
335   GtkTreeSelection *selection;
336   GtkTreeIter iter;
337   EmpathyAccountSettings *settings;
338   TpAccount *account;
339   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
340
341   view = GTK_TREE_VIEW (priv->treeview);
342   selection = gtk_tree_view_get_selection (view);
343
344   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
345     return;
346
347   gtk_tree_model_get (model, &iter,
348       COL_ACCOUNT_SETTINGS_POINTER, &settings,
349       COL_ACCOUNT_POINTER, &account, -1);
350
351   empathy_account_widget_discard_pending_changes (priv->setting_widget_object);
352
353   if (account == NULL)
354     {
355       /* We were creating an account. We remove the selected row */
356       gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
357     }
358   else
359     {
360       /* We were modifying an account. We discard the changes by reloading the
361        * settings and the UI. */
362       accounts_dialog_update_settings (dialog, settings);
363       g_object_unref (account);
364     }
365
366   gtk_widget_set_sensitive (priv->treeview, TRUE);
367   gtk_widget_set_sensitive (priv->button_add, TRUE);
368   gtk_widget_set_sensitive (priv->button_import, TRUE);
369
370   if (settings != NULL)
371     g_object_unref (settings);
372 }
373
374 static void
375 empathy_account_dialog_account_created_cb (EmpathyAccountWidget *widget_object,
376     TpAccount *account,
377     EmpathyAccountsDialog *dialog)
378 {
379   gchar *display_name;
380   EmpathyAccountSettings *settings =
381       accounts_dialog_model_get_selected_settings (dialog);
382   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
383
384   display_name = empathy_account_widget_get_default_display_name (
385       widget_object);
386
387   empathy_account_settings_set_display_name_async (settings,
388       display_name, NULL, NULL);
389
390   g_free (display_name);
391
392   accounts_dialog_update_settings (dialog, settings);
393   accounts_dialog_update_status_infobar (dialog,
394       empathy_account_settings_get_account (settings));
395
396   gtk_widget_set_sensitive (priv->treeview, TRUE);
397   gtk_widget_set_sensitive (priv->button_add, TRUE);
398   gtk_widget_set_sensitive (priv->button_import, TRUE);
399
400   empathy_signal_connect_weak (account, "status-changed",
401       G_CALLBACK (accounts_dialog_connection_changed_cb), G_OBJECT (dialog));
402   empathy_signal_connect_weak (account, "presence-changed",
403       G_CALLBACK (accounts_dialog_presence_changed_cb), G_OBJECT (dialog));
404
405   if (settings)
406     g_object_unref (settings);
407 }
408
409 static void
410 account_dialog_create_settings_widget (EmpathyAccountsDialog *dialog,
411     EmpathyAccountSettings *settings)
412 {
413   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
414   gchar                     *icon_name;
415   TpAccount                 *account;
416
417   priv->setting_widget_object =
418       empathy_account_widget_new_for_protocol (settings, FALSE);
419
420   priv->settings_widget =
421       empathy_account_widget_get_widget (priv->setting_widget_object);
422
423   g_signal_connect (priv->setting_widget_object, "account-created",
424         G_CALLBACK (empathy_account_dialog_account_created_cb), dialog);
425   g_signal_connect (priv->setting_widget_object, "cancelled",
426           G_CALLBACK (empathy_account_dialog_widget_cancelled_cb), dialog);
427
428   gtk_container_add (GTK_CONTAINER (priv->alignment_settings),
429       priv->settings_widget);
430   gtk_widget_show (priv->settings_widget);
431
432   icon_name = empathy_account_settings_get_icon_name (settings);
433
434   if (!gtk_icon_theme_has_icon (gtk_icon_theme_get_default (),
435           icon_name))
436     /* show the default icon; keep this in sync with the default
437      * one in empathy-accounts-dialog.ui.
438      */
439     icon_name = GTK_STOCK_CUT;
440
441   gtk_image_set_from_icon_name (GTK_IMAGE (priv->image_type),
442       icon_name, GTK_ICON_SIZE_DIALOG);
443   gtk_widget_set_tooltip_text (priv->image_type,
444       empathy_protocol_name_to_display_name
445       (empathy_account_settings_get_protocol (settings)));
446   gtk_widget_show (priv->image_type);
447
448   accounts_dialog_update_name_label (dialog,
449       empathy_account_settings_get_display_name (settings));
450
451   account = empathy_account_settings_get_account (settings);
452   accounts_dialog_update_status_infobar (dialog, account);
453 }
454
455 static void
456 account_dialog_settings_ready_cb (EmpathyAccountSettings *settings,
457     GParamSpec *spec,
458     EmpathyAccountsDialog *dialog)
459 {
460   if (empathy_account_settings_is_ready (settings))
461     account_dialog_create_settings_widget (dialog, settings);
462 }
463
464 static void
465 accounts_dialog_model_select_first (EmpathyAccountsDialog *dialog)
466 {
467   GtkTreeView      *view;
468   GtkTreeModel     *model;
469   GtkTreeSelection *selection;
470   GtkTreeIter       iter;
471   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
472
473   /* select first */
474   view = GTK_TREE_VIEW (priv->treeview);
475   model = gtk_tree_view_get_model (view);
476
477   if (gtk_tree_model_get_iter_first (model, &iter))
478     {
479       selection = gtk_tree_view_get_selection (view);
480       gtk_tree_selection_select_iter (selection, &iter);
481     }
482   else
483     {
484       accounts_dialog_update_settings (dialog, NULL);
485     }
486 }
487
488 static gboolean
489 accounts_dialog_has_pending_change (EmpathyAccountsDialog *dialog,
490     TpAccount **account)
491 {
492   GtkTreeIter iter;
493   GtkTreeModel *model;
494   GtkTreeSelection *selection;
495   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
496
497   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (priv->treeview));
498
499   if (gtk_tree_selection_get_selected (selection, &model, &iter))
500     gtk_tree_model_get (model, &iter, COL_ACCOUNT_POINTER, account, -1);
501
502   return priv->setting_widget_object != NULL
503       && empathy_account_widget_contains_pending_changes (
504           priv->setting_widget_object);
505 }
506
507 static void
508 accounts_dialog_setup_ui_to_add_account (EmpathyAccountsDialog *dialog)
509 {
510   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
511   EmpathyAccountSettings *settings;
512   gchar *str;
513   const gchar *display_name;
514   TpConnectionManager *cm;
515   TpConnectionManagerProtocol *proto;
516   gboolean is_gtalk;
517
518   cm = empathy_protocol_chooser_dup_selected (
519       EMPATHY_PROTOCOL_CHOOSER (priv->combobox_protocol), &proto, &is_gtalk);
520   if (cm == NULL)
521     return;
522
523   display_name = empathy_protocol_name_to_display_name (
524       is_gtalk ? "gtalk" : proto->name);
525
526   if (display_name == NULL)
527     display_name = proto->name;
528
529   /* Create account */
530   /* To translator: %s is the name of the protocol, such as "Google Talk" or
531    * "Yahoo!"
532    */
533   str = g_strdup_printf (_("New %s account"), display_name);
534   settings = empathy_account_settings_new (cm->name, proto->name, str);
535
536   g_free (str);
537
538   if (is_gtalk)
539     empathy_account_settings_set_icon_name_async (settings, "im-google-talk",
540         NULL, NULL);
541
542   accounts_dialog_add (dialog, settings);
543   accounts_dialog_model_set_selected (dialog, settings);
544
545   gtk_widget_show_all (priv->hbox_protocol);
546
547   g_object_unref (settings);
548   g_object_unref (cm);
549 }
550
551 static void
552 accounts_dialog_protocol_changed_cb (GtkWidget *widget,
553     EmpathyAccountsDialog *dialog)
554 {
555   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
556   GtkTreeSelection *selection;
557   GtkTreeModel *model;
558   GtkTreeIter iter;
559   gboolean creating;
560
561   /* The "changed" signal is fired during the initiation of the
562    * EmpathyProtocolChooser while populating the widget. Such signals should
563    * be ignored so we check if we are actually creating a new account. */
564   if (priv->setting_widget_object == NULL)
565     return;
566
567   g_object_get (priv->setting_widget_object,
568       "creating-account", &creating, NULL);
569   if (!creating)
570     return;
571
572   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (priv->treeview));
573
574   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
575     return;
576
577   /* We are creating a new widget to replace the current one, don't ask
578    * confirmation to the user. */
579   priv->force_change_row = TRUE;
580   gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
581
582   accounts_dialog_setup_ui_to_add_account (dialog);
583 }
584
585 static void
586 accounts_dialog_show_question_dialog (EmpathyAccountsDialog *dialog,
587     const gchar *primary_text,
588     const gchar *secondary_text,
589     GCallback response_callback,
590     gpointer user_data,
591     const gchar *first_button_text,
592     ...)
593 {
594   va_list button_args;
595   GtkWidget *message_dialog;
596   const gchar *button_text;
597
598   message_dialog = gtk_message_dialog_new (GTK_WINDOW (dialog),
599       GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
600       GTK_MESSAGE_QUESTION,
601       GTK_BUTTONS_NONE,
602       "%s", primary_text);
603
604   gtk_message_dialog_format_secondary_text (
605       GTK_MESSAGE_DIALOG (message_dialog), "%s", secondary_text);
606
607   va_start (button_args, first_button_text);
608   for (button_text = first_button_text;
609        button_text;
610        button_text = va_arg (button_args, const gchar *))
611     {
612       gint response_id;
613       response_id = va_arg (button_args, gint);
614
615       gtk_dialog_add_button (GTK_DIALOG (message_dialog), button_text,
616           response_id);
617     }
618   va_end (button_args);
619
620   g_signal_connect (message_dialog, "response", response_callback, user_data);
621
622   gtk_widget_show (message_dialog);
623 }
624
625 static void
626 accounts_dialog_add_pending_changes_response_cb (GtkDialog *message_dialog,
627   gint response_id,
628   gpointer *user_data)
629 {
630   EmpathyAccountsDialog *dialog = EMPATHY_ACCOUNTS_DIALOG (user_data);
631   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
632
633   gtk_widget_destroy (GTK_WIDGET (message_dialog));
634
635   if (response_id == GTK_RESPONSE_YES)
636     {
637       empathy_account_widget_discard_pending_changes (
638           priv->setting_widget_object);
639       accounts_dialog_setup_ui_to_add_account (dialog);
640     }
641 }
642
643 static gchar *
644 get_dialog_primary_text (TpAccount *account)
645 {
646   if (account != NULL)
647     {
648       /* Existing account */
649       return g_strdup_printf (PENDING_CHANGES_QUESTION_PRIMARY_TEXT,
650           tp_account_get_display_name (account));
651     }
652   else
653     {
654       /* Newly created account */
655       return g_strdup (UNSAVED_NEW_ACCOUNT_QUESTION_PRIMARY_TEXT);
656     }
657 }
658
659 static void
660 accounts_dialog_button_add_clicked_cb (GtkWidget *button,
661     EmpathyAccountsDialog *dialog)
662 {
663   TpAccount *account = NULL;
664   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
665
666   if (accounts_dialog_has_pending_change (dialog, &account))
667     {
668       gchar *question_dialog_primary_text = get_dialog_primary_text (account);
669
670       accounts_dialog_show_question_dialog (dialog,
671           question_dialog_primary_text,
672           _("You are about to create a new account, which will discard\n"
673               "your changes. Are you sure you want to proceed?"),
674           G_CALLBACK (accounts_dialog_add_pending_changes_response_cb),
675           dialog,
676           GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
677           GTK_STOCK_DISCARD, GTK_RESPONSE_YES, NULL);
678
679       g_free (question_dialog_primary_text);
680     }
681   else
682     {
683       accounts_dialog_setup_ui_to_add_account (dialog);
684       gtk_widget_set_sensitive (priv->treeview, FALSE);
685       gtk_widget_set_sensitive (priv->button_add, FALSE);
686       gtk_widget_set_sensitive (priv->button_import, FALSE);
687     }
688 }
689
690 static void
691 accounts_dialog_update_settings (EmpathyAccountsDialog *dialog,
692     EmpathyAccountSettings *settings)
693 {
694   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
695
696   if (priv->settings_ready != NULL)
697     {
698       g_signal_handler_disconnect (priv->settings_ready,
699           priv->settings_ready_id);
700       priv->settings_ready = NULL;
701       priv->settings_ready_id = 0;
702     }
703
704   if (!settings)
705     {
706       GtkTreeView  *view;
707       GtkTreeModel *model;
708       GtkTreeSelection *selection;
709
710       view = GTK_TREE_VIEW (priv->treeview);
711       model = gtk_tree_view_get_model (view);
712       selection = gtk_tree_view_get_selection (view);
713
714       if (gtk_tree_model_iter_n_children (model, NULL) > 0)
715         {
716           /* We have configured accounts, select the first one if there
717            * is no other account selected already. */
718           if (!gtk_tree_selection_get_selected (selection, NULL, NULL))
719             accounts_dialog_model_select_first (dialog);
720
721           return;
722         }
723       if (empathy_connection_managers_get_cms_num (priv->cms) > 0)
724         {
725           /* We have no account configured but we have some
726            * profiles installed. The user obviously wants to add
727            * an account. Click on the Add button for him. */
728           accounts_dialog_button_add_clicked_cb (priv->button_add,
729               dialog);
730           return;
731         }
732
733       /* No account and no profile, warn the user */
734       gtk_widget_hide (priv->vbox_details);
735       gtk_widget_show (priv->frame_no_protocol);
736       gtk_widget_set_sensitive (priv->button_add, FALSE);
737       return;
738     }
739
740   /* We have an account selected, destroy old settings and create a new
741    * one for the account selected */
742   gtk_widget_hide (priv->frame_no_protocol);
743   gtk_widget_show (priv->vbox_details);
744   gtk_widget_hide (priv->hbox_protocol);
745
746   if (priv->settings_widget)
747     {
748       gtk_widget_destroy (priv->settings_widget);
749       priv->settings_widget = NULL;
750     }
751
752   if (empathy_account_settings_is_ready (settings))
753     {
754       account_dialog_create_settings_widget (dialog, settings);
755     }
756   else
757     {
758       priv->settings_ready = settings;
759       priv->settings_ready_id =
760         g_signal_connect (settings, "notify::ready",
761             G_CALLBACK (account_dialog_settings_ready_cb), dialog);
762     }
763
764 }
765
766 static void
767 accounts_dialog_name_editing_started_cb (GtkCellRenderer *renderer,
768     GtkCellEditable *editable,
769     gchar *path,
770     EmpathyAccountsDialog *dialog)
771 {
772   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
773
774   if (priv->connecting_id)
775     g_source_remove (priv->connecting_id);
776
777   DEBUG ("Editing account name started; stopping flashing");
778 }
779
780 static void
781 accounts_dialog_model_pixbuf_data_func (GtkTreeViewColumn *tree_column,
782     GtkCellRenderer *cell,
783     GtkTreeModel *model,
784     GtkTreeIter *iter,
785     EmpathyAccountsDialog *dialog)
786 {
787   EmpathyAccountSettings  *settings;
788   gchar              *icon_name;
789   GdkPixbuf          *pixbuf;
790   TpConnectionStatus  status;
791   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
792
793   gtk_tree_model_get (model, iter,
794       COL_STATUS, &status,
795       COL_ACCOUNT_SETTINGS_POINTER, &settings,
796       -1);
797
798   icon_name = empathy_account_settings_get_icon_name (settings);
799   pixbuf = empathy_pixbuf_from_icon_name (icon_name, GTK_ICON_SIZE_BUTTON);
800
801   if (pixbuf)
802     {
803       if (status == TP_CONNECTION_STATUS_DISCONNECTED ||
804           (status == TP_CONNECTION_STATUS_CONNECTING &&
805               !priv->connecting_show))
806         {
807           GdkPixbuf *modded_pixbuf;
808
809           modded_pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
810               TRUE,
811               8,
812               gdk_pixbuf_get_width (pixbuf),
813               gdk_pixbuf_get_height (pixbuf));
814
815           gdk_pixbuf_saturate_and_pixelate (pixbuf,
816               modded_pixbuf,
817               1.0,
818               TRUE);
819           g_object_unref (pixbuf);
820           pixbuf = modded_pixbuf;
821         }
822     }
823
824   g_object_set (cell,
825       "visible", TRUE,
826       "pixbuf", pixbuf,
827       NULL);
828
829   g_object_unref (settings);
830
831   if (pixbuf)
832     g_object_unref (pixbuf);
833 }
834
835 static gboolean
836 accounts_dialog_row_changed_foreach (GtkTreeModel *model,
837     GtkTreePath *path,
838     GtkTreeIter *iter,
839     gpointer user_data)
840 {
841   gtk_tree_model_row_changed (model, path, iter);
842
843   return FALSE;
844 }
845
846 static gboolean
847 accounts_dialog_flash_connecting_cb (EmpathyAccountsDialog *dialog)
848 {
849   GtkTreeView  *view;
850   GtkTreeModel *model;
851   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
852
853   priv->connecting_show = !priv->connecting_show;
854
855   view = GTK_TREE_VIEW (priv->treeview);
856   model = gtk_tree_view_get_model (view);
857
858   gtk_tree_model_foreach (model, accounts_dialog_row_changed_foreach, NULL);
859
860   return TRUE;
861 }
862
863 static void
864 accounts_dialog_name_edited_cb (GtkCellRendererText *renderer,
865     gchar *path,
866     gchar *new_text,
867     EmpathyAccountsDialog *dialog)
868 {
869   EmpathyAccountSettings    *settings;
870   GtkTreeModel *model;
871   GtkTreePath  *treepath;
872   GtkTreeIter   iter;
873   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
874   gboolean connecting;
875
876   empathy_account_manager_get_accounts_connected (&connecting);
877
878   if (connecting)
879     {
880       priv->connecting_id = g_timeout_add (FLASH_TIMEOUT,
881           (GSourceFunc) accounts_dialog_flash_connecting_cb,
882           dialog);
883     }
884
885   model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->treeview));
886   treepath = gtk_tree_path_new_from_string (path);
887   gtk_tree_model_get_iter (model, &iter, treepath);
888   gtk_tree_model_get (model, &iter,
889       COL_ACCOUNT_SETTINGS_POINTER, &settings,
890       -1);
891   gtk_list_store_set (GTK_LIST_STORE (model), &iter,
892       COL_NAME, new_text,
893       -1);
894   gtk_tree_path_free (treepath);
895
896   empathy_account_settings_set_display_name_async (settings, new_text,
897       NULL, NULL);
898   g_object_set (settings, "display-name-overridden", TRUE, NULL);
899   g_object_unref (settings);
900 }
901
902 static void
903 accounts_dialog_delete_account_response_cb (GtkDialog *message_dialog,
904   gint response_id,
905   gpointer user_data)
906 {
907   TpAccount *account;
908   GtkTreeModel *model;
909   GtkTreeIter iter;
910   GtkTreeSelection *selection;
911   EmpathyAccountsDialog *account_dialog = EMPATHY_ACCOUNTS_DIALOG (user_data);
912   EmpathyAccountsDialogPriv *priv = GET_PRIV (account_dialog);
913
914   if (response_id == GTK_RESPONSE_YES)
915     {
916       selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (priv->treeview));
917
918       if (!gtk_tree_selection_get_selected (selection, &model, &iter))
919         return;
920
921       gtk_tree_model_get (model, &iter, COL_ACCOUNT_POINTER, &account, -1);
922
923       if (account != NULL)
924         {
925           tp_account_remove_async (account, NULL, NULL);
926           g_object_unref (account);
927           account = NULL;
928         }
929
930       gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
931       accounts_dialog_model_select_first (account_dialog);
932     }
933
934   gtk_widget_destroy (GTK_WIDGET (message_dialog));
935 }
936
937 static void
938 accounts_dialog_view_delete_activated_cb (EmpathyCellRendererActivatable *cell,
939     const gchar *path_string,
940     EmpathyAccountsDialog *dialog)
941 {
942   TpAccount *account;
943   GtkTreeModel *model;
944   GtkTreeIter iter;
945   gchar *question_dialog_primary_text;
946   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
947
948   model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->treeview));
949
950   if (!gtk_tree_model_get_iter_from_string (model, &iter, path_string))
951     return;
952
953   gtk_tree_model_get (model, &iter, COL_ACCOUNT_POINTER, &account, -1);
954
955   if (account == NULL || !tp_account_is_valid (account))
956     {
957       gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
958       accounts_dialog_model_select_first (dialog);
959       return;
960     }
961
962   question_dialog_primary_text = g_strdup_printf (
963       _("Do you want to remove %s from your computer?"),
964       tp_account_get_display_name (account));
965
966   accounts_dialog_show_question_dialog (dialog, question_dialog_primary_text,
967       _("This will not remove your account on the server."),
968       G_CALLBACK (accounts_dialog_delete_account_response_cb),
969       dialog,
970       GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
971       GTK_STOCK_REMOVE, GTK_RESPONSE_YES, NULL);
972
973   g_free (question_dialog_primary_text);
974
975   if (account != NULL)
976     {
977       g_object_unref (account);
978       account = NULL;
979     }
980 }
981
982 static void
983 accounts_dialog_model_add_columns (EmpathyAccountsDialog *dialog)
984 {
985   GtkTreeView       *view;
986   GtkTreeViewColumn *column;
987   GtkCellRenderer   *cell;
988   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
989
990   view = GTK_TREE_VIEW (priv->treeview);
991   gtk_tree_view_set_headers_visible (view, FALSE);
992
993   /* Account column */
994   column = gtk_tree_view_column_new ();
995   gtk_tree_view_column_set_expand (column, TRUE);
996   gtk_tree_view_append_column (view, column);
997
998   /* Icon renderer */
999   cell = gtk_cell_renderer_pixbuf_new ();
1000   gtk_tree_view_column_pack_start (column, cell, FALSE);
1001   gtk_tree_view_column_set_cell_data_func (column, cell,
1002       (GtkTreeCellDataFunc)
1003       accounts_dialog_model_pixbuf_data_func,
1004       dialog,
1005       NULL);
1006
1007   /* Name renderer */
1008   cell = gtk_cell_renderer_text_new ();
1009   g_object_set (cell,
1010       "ellipsize", PANGO_ELLIPSIZE_END,
1011       "width-chars", 25,
1012       "editable", TRUE,
1013       NULL);
1014   gtk_tree_view_column_pack_start (column, cell, TRUE);
1015   gtk_tree_view_column_add_attribute (column, cell, "text", COL_NAME);
1016   g_signal_connect (cell, "edited",
1017       G_CALLBACK (accounts_dialog_name_edited_cb),
1018       dialog);
1019   g_signal_connect (cell, "editing-started",
1020       G_CALLBACK (accounts_dialog_name_editing_started_cb),
1021       dialog);
1022   g_object_set (cell, "ypad", 4, NULL);
1023
1024   /* Delete column */
1025   cell = empathy_cell_renderer_activatable_new ();
1026   gtk_tree_view_column_pack_start (column, cell, FALSE);
1027   g_object_set (cell,
1028         "icon-name", GTK_STOCK_DELETE,
1029         "show-on-select", TRUE,
1030         NULL);
1031
1032   g_signal_connect (cell, "path-activated",
1033       G_CALLBACK (accounts_dialog_view_delete_activated_cb),
1034       dialog);
1035 }
1036
1037 static EmpathyAccountSettings *
1038 accounts_dialog_model_get_selected_settings (EmpathyAccountsDialog *dialog)
1039 {
1040   GtkTreeView      *view;
1041   GtkTreeModel     *model;
1042   GtkTreeSelection *selection;
1043   GtkTreeIter       iter;
1044   EmpathyAccountSettings   *settings;
1045   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1046
1047   view = GTK_TREE_VIEW (priv->treeview);
1048   selection = gtk_tree_view_get_selection (view);
1049
1050   if (!gtk_tree_selection_get_selected (selection, &model, &iter))
1051     return NULL;
1052
1053   gtk_tree_model_get (model, &iter,
1054       COL_ACCOUNT_SETTINGS_POINTER, &settings, -1);
1055
1056   return settings;
1057 }
1058
1059 static void
1060 accounts_dialog_model_selection_changed (GtkTreeSelection *selection,
1061     EmpathyAccountsDialog *dialog)
1062 {
1063   EmpathyAccountSettings *settings;
1064   GtkTreeModel *model;
1065   GtkTreeIter   iter;
1066   gboolean      is_selection;
1067
1068   is_selection = gtk_tree_selection_get_selected (selection, &model, &iter);
1069
1070   settings = accounts_dialog_model_get_selected_settings (dialog);
1071   accounts_dialog_update_settings (dialog, settings);
1072
1073   if (settings != NULL)
1074     g_object_unref (settings);
1075 }
1076
1077 static void
1078 accounts_dialog_selection_change_response_cb (GtkDialog *message_dialog,
1079   gint response_id,
1080   gpointer *user_data)
1081 {
1082   EmpathyAccountsDialog *dialog = EMPATHY_ACCOUNTS_DIALOG (user_data);
1083   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1084
1085   gtk_widget_destroy (GTK_WIDGET (message_dialog));
1086
1087     if (response_id == GTK_RESPONSE_YES && priv->destination_row != NULL)
1088       {
1089         /* The user wants to lose unsaved changes to the currently selected
1090          * account and select another account. We discard the changes and
1091          * select the other account. */
1092         GtkTreePath *path;
1093         GtkTreeSelection *selection;
1094
1095         priv->force_change_row = TRUE;
1096         empathy_account_widget_discard_pending_changes (
1097             priv->setting_widget_object);
1098
1099         path = gtk_tree_row_reference_get_path (priv->destination_row);
1100         selection = gtk_tree_view_get_selection (
1101             GTK_TREE_VIEW (priv->treeview));
1102
1103         if (path != NULL)
1104           {
1105             /* This will trigger a call to
1106              * accounts_dialog_account_selection_change() */
1107             gtk_tree_selection_select_path (selection, path);
1108             gtk_tree_path_free (path);
1109           }
1110
1111         gtk_tree_row_reference_free (priv->destination_row);
1112       }
1113     else
1114       {
1115         priv->force_change_row = FALSE;
1116       }
1117 }
1118
1119 static gboolean
1120 accounts_dialog_account_selection_change (GtkTreeSelection *selection,
1121     GtkTreeModel *model,
1122     GtkTreePath *path,
1123     gboolean path_currently_selected,
1124     gpointer data)
1125 {
1126   TpAccount *account = NULL;
1127   EmpathyAccountsDialog *dialog = EMPATHY_ACCOUNTS_DIALOG (data);
1128   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1129
1130   if (priv->force_change_row)
1131     {
1132       /* We came back here because the user wants to discard changes to his
1133        * modified account. The changes have already been discarded so we
1134        * just change the selected row. */
1135       priv->force_change_row = FALSE;
1136       return TRUE;
1137     }
1138
1139   if (accounts_dialog_has_pending_change (dialog, &account))
1140     {
1141       /* The currently selected account has some unsaved changes. We ask
1142        * the user if he really wants to lose his changes and select another
1143        * account */
1144       gchar *question_dialog_primary_text = get_dialog_primary_text (account);
1145       priv->destination_row = gtk_tree_row_reference_new (model, path);
1146
1147       accounts_dialog_show_question_dialog (dialog,
1148           question_dialog_primary_text,
1149           _("You are about to select another account, which will discard\n"
1150               "your changes. Are you sure you want to proceed?"),
1151           G_CALLBACK (accounts_dialog_selection_change_response_cb),
1152           dialog,
1153           GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
1154           GTK_STOCK_DISCARD, GTK_RESPONSE_YES, NULL);
1155
1156       g_free (question_dialog_primary_text);
1157     }
1158   else
1159     {
1160       return TRUE;
1161     }
1162
1163   return FALSE;
1164 }
1165
1166 static void
1167 accounts_dialog_model_setup (EmpathyAccountsDialog *dialog)
1168 {
1169   GtkListStore     *store;
1170   GtkTreeSelection *selection;
1171   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1172
1173   store = gtk_list_store_new (COL_COUNT,
1174       G_TYPE_STRING,         /* name */
1175       G_TYPE_UINT,           /* status */
1176       TP_TYPE_ACCOUNT,   /* account */
1177       EMPATHY_TYPE_ACCOUNT_SETTINGS); /* settings */
1178
1179   gtk_tree_view_set_model (GTK_TREE_VIEW (priv->treeview),
1180       GTK_TREE_MODEL (store));
1181
1182   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (priv->treeview));
1183   gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE);
1184   gtk_tree_selection_set_select_function (selection,
1185       accounts_dialog_account_selection_change, dialog, NULL);
1186
1187   g_signal_connect (selection, "changed",
1188       G_CALLBACK (accounts_dialog_model_selection_changed),
1189       dialog);
1190
1191   gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (store),
1192       COL_NAME, GTK_SORT_ASCENDING);
1193
1194   accounts_dialog_model_add_columns (dialog);
1195
1196   g_object_unref (store);
1197 }
1198
1199 static gboolean
1200 accounts_dialog_get_settings_iter (EmpathyAccountsDialog *dialog,
1201     EmpathyAccountSettings *settings,
1202     GtkTreeIter *iter)
1203 {
1204   GtkTreeView      *view;
1205   GtkTreeSelection *selection;
1206   GtkTreeModel     *model;
1207   gboolean          ok;
1208   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1209
1210   /* Update the status in the model */
1211   view = GTK_TREE_VIEW (priv->treeview);
1212   selection = gtk_tree_view_get_selection (view);
1213   model = gtk_tree_view_get_model (view);
1214
1215   for (ok = gtk_tree_model_get_iter_first (model, iter);
1216        ok;
1217        ok = gtk_tree_model_iter_next (model, iter))
1218     {
1219       EmpathyAccountSettings *this_settings;
1220       gboolean   equal;
1221
1222       gtk_tree_model_get (model, iter,
1223           COL_ACCOUNT_SETTINGS_POINTER, &this_settings,
1224           -1);
1225
1226       equal = (this_settings == settings);
1227       g_object_unref (this_settings);
1228
1229       if (equal)
1230         return TRUE;
1231     }
1232
1233   return FALSE;
1234 }
1235
1236 static gboolean
1237 accounts_dialog_get_account_iter (EmpathyAccountsDialog *dialog,
1238     TpAccount *account,
1239     GtkTreeIter *iter)
1240 {
1241   GtkTreeView      *view;
1242   GtkTreeSelection *selection;
1243   GtkTreeModel     *model;
1244   gboolean          ok;
1245   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1246
1247   /* Update the status in the model */
1248   view = GTK_TREE_VIEW (priv->treeview);
1249   selection = gtk_tree_view_get_selection (view);
1250   model = gtk_tree_view_get_model (view);
1251
1252   for (ok = gtk_tree_model_get_iter_first (model, iter);
1253        ok;
1254        ok = gtk_tree_model_iter_next (model, iter))
1255     {
1256       EmpathyAccountSettings *settings;
1257       gboolean   equal;
1258
1259       gtk_tree_model_get (model, iter,
1260           COL_ACCOUNT_SETTINGS_POINTER, &settings,
1261           -1);
1262
1263       equal = empathy_account_settings_has_account (settings, account);
1264       g_object_unref (settings);
1265
1266       if (equal)
1267         return TRUE;
1268     }
1269
1270   return FALSE;
1271 }
1272
1273 static void
1274 accounts_dialog_model_set_selected (EmpathyAccountsDialog *dialog,
1275     EmpathyAccountSettings *settings)
1276 {
1277   GtkTreeSelection *selection;
1278   GtkTreeIter       iter;
1279   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1280
1281   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (priv->treeview));
1282   if (accounts_dialog_get_settings_iter (dialog, settings, &iter))
1283     gtk_tree_selection_select_iter (selection, &iter);
1284 }
1285 static void
1286 accounts_dialog_add (EmpathyAccountsDialog *dialog,
1287     EmpathyAccountSettings *settings)
1288 {
1289   GtkTreeModel       *model;
1290   GtkTreeIter         iter;
1291   const gchar        *name;
1292   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1293
1294   model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->treeview));
1295   name = empathy_account_settings_get_display_name (settings);
1296
1297   gtk_list_store_append (GTK_LIST_STORE (model), &iter);
1298
1299   gtk_list_store_set (GTK_LIST_STORE (model), &iter,
1300       COL_NAME, name,
1301       COL_STATUS, TP_CONNECTION_STATUS_DISCONNECTED,
1302       COL_ACCOUNT_SETTINGS_POINTER, settings,
1303       -1);
1304 }
1305
1306 static void
1307 accounts_dialog_connection_changed_cb (TpAccount *account,
1308     guint old_status,
1309     guint current,
1310     guint reason,
1311     gchar *dbus_error_name,
1312     GHashTable *details,
1313     EmpathyAccountsDialog *dialog)
1314 {
1315   GtkTreeModel *model;
1316   GtkTreeIter   iter;
1317   gboolean      found;
1318   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1319
1320   /* Update the status-infobar in the details view */
1321   accounts_dialog_update_status_infobar (dialog, account);
1322
1323   /* Update the status in the model */
1324   model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->treeview));
1325
1326   if (accounts_dialog_get_account_iter (dialog, account, &iter))
1327     {
1328       GtkTreePath *path;
1329
1330       gtk_list_store_set (GTK_LIST_STORE (model), &iter,
1331           COL_STATUS, current,
1332           -1);
1333
1334       path = gtk_tree_model_get_path (model, &iter);
1335       gtk_tree_model_row_changed (model, path, &iter);
1336       gtk_tree_path_free (path);
1337     }
1338
1339   empathy_account_manager_get_accounts_connected (&found);
1340
1341   if (!found && priv->connecting_id)
1342     {
1343       g_source_remove (priv->connecting_id);
1344       priv->connecting_id = 0;
1345     }
1346
1347   if (found && !priv->connecting_id)
1348     priv->connecting_id = g_timeout_add (FLASH_TIMEOUT,
1349         (GSourceFunc) accounts_dialog_flash_connecting_cb,
1350         dialog);
1351 }
1352
1353 static void
1354 accounts_dialog_presence_changed_cb (TpAccount *account,
1355     guint presence,
1356     gchar *status,
1357     gchar *status_message,
1358     EmpathyAccountsDialog *dialog)
1359 {
1360   /* Update the status-infobar in the details view */
1361   accounts_dialog_update_status_infobar (dialog, account);
1362 }
1363
1364 static void
1365 accounts_dialog_account_display_name_changed_cb (TpAccount *account,
1366   GParamSpec *pspec,
1367   gpointer user_data)
1368 {
1369   const gchar *display_name;
1370   GtkTreeIter iter;
1371   GtkTreeModel *model;
1372   EmpathyAccountSettings *settings;
1373   TpAccount *selected_account;
1374   EmpathyAccountsDialog *dialog = EMPATHY_ACCOUNTS_DIALOG (user_data);
1375   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1376
1377   display_name = tp_account_get_display_name (account);
1378   model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->treeview));
1379   settings = accounts_dialog_model_get_selected_settings (dialog);
1380   if (settings == NULL)
1381     return;
1382
1383   selected_account = empathy_account_settings_get_account (settings);
1384
1385   if (accounts_dialog_get_account_iter (dialog, account, &iter))
1386     {
1387       gtk_list_store_set (GTK_LIST_STORE (model), &iter,
1388           COL_NAME, display_name,
1389           -1);
1390     }
1391
1392   if (selected_account == account)
1393     accounts_dialog_update_name_label (dialog, display_name);
1394
1395   g_object_unref (settings);
1396 }
1397
1398 static void
1399 accounts_dialog_add_account (EmpathyAccountsDialog *dialog,
1400     TpAccount *account)
1401 {
1402   EmpathyAccountSettings *settings;
1403   GtkTreeModel       *model;
1404   GtkTreeIter         iter;
1405   TpConnectionStatus  status;
1406   const gchar        *name;
1407   gboolean            enabled;
1408   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1409
1410   model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->treeview));
1411   status = tp_account_get_connection_status (account, NULL);
1412   name = tp_account_get_display_name (account);
1413   enabled = tp_account_is_enabled (account);
1414
1415   settings = empathy_account_settings_new_for_account (account);
1416
1417   if (!accounts_dialog_get_account_iter (dialog, account, &iter))
1418     gtk_list_store_append (GTK_LIST_STORE (model), &iter);
1419
1420   gtk_list_store_set (GTK_LIST_STORE (model), &iter,
1421       COL_NAME, name,
1422       COL_STATUS, status,
1423       COL_ACCOUNT_POINTER, account,
1424       COL_ACCOUNT_SETTINGS_POINTER, settings,
1425       -1);
1426
1427   accounts_dialog_connection_changed_cb (account,
1428       0,
1429       status,
1430       TP_CONNECTION_STATUS_REASON_NONE_SPECIFIED,
1431       NULL,
1432       NULL,
1433       dialog);
1434
1435   empathy_signal_connect_weak (account, "notify::display-name",
1436       G_CALLBACK (accounts_dialog_account_display_name_changed_cb),
1437       G_OBJECT (dialog));
1438
1439   g_object_unref (settings);
1440 }
1441
1442 static void
1443 account_prepare_cb (GObject *source_object,
1444     GAsyncResult *result,
1445     gpointer user_data)
1446 {
1447   EmpathyAccountsDialog *dialog = EMPATHY_ACCOUNTS_DIALOG (user_data);
1448   TpAccount *account = TP_ACCOUNT (source_object);
1449   GError *error = NULL;
1450
1451   if (!tp_account_prepare_finish (account, result, &error))
1452     {
1453       DEBUG ("Failed to prepare account: %s", error->message);
1454       g_error_free (error);
1455       return;
1456     }
1457
1458   accounts_dialog_add_account (dialog, account);
1459 }
1460
1461 static void
1462 accounts_dialog_account_validity_changed_cb (TpAccountManager *manager,
1463     TpAccount *account,
1464     gboolean valid,
1465     EmpathyAccountsDialog *dialog)
1466 {
1467   tp_account_prepare_async (account, NULL, account_prepare_cb, dialog);
1468 }
1469
1470 static void
1471 accounts_dialog_account_removed_cb (TpAccountManager *manager,
1472     TpAccount *account,
1473     EmpathyAccountsDialog *dialog)
1474 {
1475   GtkTreeIter iter;
1476   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1477
1478   if (accounts_dialog_get_account_iter (dialog, account, &iter))
1479     {
1480       gtk_list_store_remove (GTK_LIST_STORE (
1481             gtk_tree_view_get_model (GTK_TREE_VIEW (priv->treeview))), &iter);
1482     }
1483 }
1484
1485 static void
1486 enable_or_disable_account (EmpathyAccountsDialog *dialog,
1487     TpAccount *account,
1488     gboolean enabled)
1489 {
1490   GtkTreeModel *model;
1491   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1492
1493   /* Update the status in the model */
1494   model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->treeview));
1495
1496   /* Update the status-infobar in the details view when disabling */
1497   if (!enabled)
1498     accounts_dialog_update_status_infobar (dialog, account);
1499
1500   DEBUG ("Account %s is now %s",
1501       tp_account_get_display_name (account),
1502       enabled ? "enabled" : "disabled");
1503 }
1504
1505 static void
1506 accounts_dialog_account_disabled_cb (TpAccountManager *manager,
1507     TpAccount *account,
1508     EmpathyAccountsDialog *dialog)
1509 {
1510   enable_or_disable_account (dialog, account, FALSE);
1511 }
1512
1513 static void
1514 accounts_dialog_account_enabled_cb (TpAccountManager *manager,
1515     TpAccount *account,
1516     EmpathyAccountsDialog *dialog)
1517 {
1518   enable_or_disable_account (dialog, account, TRUE);
1519 }
1520
1521 static void
1522 accounts_dialog_button_import_clicked_cb (GtkWidget *button,
1523     EmpathyAccountsDialog *dialog)
1524 {
1525   GtkWidget *import_dialog;
1526
1527   import_dialog = empathy_import_dialog_new (GTK_WINDOW (dialog),
1528       FALSE);
1529   gtk_widget_show (import_dialog);
1530 }
1531
1532 static void
1533 accounts_dialog_close_response_cb (GtkDialog *message_dialog,
1534   gint response_id,
1535   gpointer user_data)
1536 {
1537   GtkWidget *account_dialog = GTK_WIDGET (user_data);
1538
1539   gtk_widget_destroy (GTK_WIDGET (message_dialog));
1540
1541   if (response_id == GTK_RESPONSE_YES)
1542     gtk_widget_destroy (account_dialog);
1543 }
1544
1545 static gboolean
1546 accounts_dialog_delete_event_cb (GtkWidget *widget,
1547     GdkEvent *event,
1548     EmpathyAccountsDialog *dialog)
1549 {
1550   /* we maunally handle responses to delete events */
1551   return TRUE;
1552 }
1553
1554 static void
1555 accounts_dialog_set_selected_account (EmpathyAccountsDialog *dialog,
1556     TpAccount *account)
1557 {
1558   GtkTreeSelection *selection;
1559   GtkTreeIter       iter;
1560   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1561
1562   selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (priv->treeview));
1563   if (accounts_dialog_get_account_iter (dialog, account, &iter))
1564     gtk_tree_selection_select_iter (selection, &iter);
1565 }
1566
1567 static void
1568 accounts_dialog_cms_ready_cb (EmpathyConnectionManagers *cms,
1569     GParamSpec *pspec,
1570     EmpathyAccountsDialog *dialog)
1571 {
1572   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1573
1574   if (empathy_connection_managers_is_ready (cms))
1575     {
1576       accounts_dialog_update_settings (dialog, NULL);
1577
1578       if (priv->initial_selection != NULL)
1579         {
1580           accounts_dialog_set_selected_account
1581               (dialog, priv->initial_selection);
1582           g_object_unref (priv->initial_selection);
1583           priv->initial_selection = NULL;
1584         }
1585     }
1586 }
1587
1588 static void
1589 accounts_dialog_accounts_setup (EmpathyAccountsDialog *dialog)
1590 {
1591   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1592   GList *accounts, *l;
1593
1594   g_signal_connect (priv->account_manager, "account-validity-changed",
1595       G_CALLBACK (accounts_dialog_account_validity_changed_cb),
1596       dialog);
1597   g_signal_connect (priv->account_manager, "account-removed",
1598       G_CALLBACK (accounts_dialog_account_removed_cb),
1599       dialog);
1600   g_signal_connect (priv->account_manager, "account-enabled",
1601       G_CALLBACK (accounts_dialog_account_enabled_cb),
1602       dialog);
1603   g_signal_connect (priv->account_manager, "account-disabled",
1604       G_CALLBACK (accounts_dialog_account_disabled_cb),
1605       dialog);
1606
1607   /* Add existing accounts */
1608   accounts = tp_account_manager_get_valid_accounts (priv->account_manager);
1609   for (l = accounts; l; l = l->next)
1610     {
1611       accounts_dialog_add_account (dialog, l->data);
1612
1613       empathy_signal_connect_weak (l->data, "status-changed",
1614           G_CALLBACK (accounts_dialog_connection_changed_cb), G_OBJECT (dialog));
1615       empathy_signal_connect_weak (l->data, "presence-changed",
1616           G_CALLBACK (accounts_dialog_presence_changed_cb), G_OBJECT (dialog));
1617     }
1618   g_list_free (accounts);
1619
1620   priv->cms = empathy_connection_managers_dup_singleton ();
1621   if (!empathy_connection_managers_is_ready (priv->cms))
1622     g_signal_connect (priv->cms, "notify::ready",
1623         G_CALLBACK (accounts_dialog_cms_ready_cb), dialog);
1624
1625   accounts_dialog_model_select_first (dialog);
1626 }
1627
1628 static void
1629 accounts_dialog_manager_ready_cb (GObject *source_object,
1630     GAsyncResult *result,
1631     gpointer user_data)
1632 {
1633   TpAccountManager *manager = TP_ACCOUNT_MANAGER (source_object);
1634   GError *error = NULL;
1635
1636   if (!tp_account_manager_prepare_finish (manager, result, &error))
1637     {
1638       DEBUG ("Failed to prepare account manager: %s", error->message);
1639       g_error_free (error);
1640       return;
1641     }
1642
1643   accounts_dialog_accounts_setup (user_data);
1644 }
1645
1646 static void
1647 dialog_response_cb (GtkWidget *widget,
1648     gint response_id,
1649     gpointer user_data)
1650 {
1651   EmpathyAccountsDialog *dialog = EMPATHY_ACCOUNTS_DIALOG (widget);
1652
1653   if (response_id == GTK_RESPONSE_HELP)
1654     {
1655       empathy_url_show (widget, "ghelp:empathy?accounts-window");
1656     }
1657   else if (response_id == GTK_RESPONSE_CLOSE ||
1658       response_id == GTK_RESPONSE_DELETE_EVENT)
1659     {
1660       TpAccount *account = NULL;
1661
1662       if (accounts_dialog_has_pending_change (dialog, &account))
1663         {
1664           gchar *question_dialog_primary_text = get_dialog_primary_text (
1665               account);
1666
1667           accounts_dialog_show_question_dialog (dialog,
1668               question_dialog_primary_text,
1669               _("You are about to close the window, which will discard\n"
1670                   "your changes. Are you sure you want to proceed?"),
1671               G_CALLBACK (accounts_dialog_close_response_cb),
1672               widget,
1673               GTK_STOCK_CANCEL, GTK_RESPONSE_NO,
1674               GTK_STOCK_DISCARD, GTK_RESPONSE_YES, NULL);
1675
1676           g_free (question_dialog_primary_text);
1677         }
1678       else
1679         {
1680           gtk_widget_destroy (widget);
1681         }
1682     }
1683 }
1684
1685 static void
1686 accounts_dialog_build_ui (EmpathyAccountsDialog *dialog)
1687 {
1688   GtkWidget *top_hbox;
1689   GtkBuilder                   *gui;
1690   gchar                        *filename;
1691   EmpathyAccountsDialogPriv    *priv = GET_PRIV (dialog);
1692   GtkWidget                    *content_area;
1693   GtkWidget *action_area;
1694
1695   filename = empathy_file_lookup ("empathy-accounts-dialog.ui", "src");
1696
1697   gui = empathy_builder_get_file (filename,
1698       "accounts_dialog_hbox", &top_hbox,
1699       "vbox_details", &priv->vbox_details,
1700       "frame_no_protocol", &priv->frame_no_protocol,
1701       "alignment_settings", &priv->alignment_settings,
1702       "alignment_infobar", &priv->alignment_infobar,
1703       "treeview", &priv->treeview,
1704       "image_type", &priv->image_type,
1705       "label_name", &priv->label_name,
1706       "button_add", &priv->button_add,
1707       "button_import", &priv->button_import,
1708       "hbox_protocol", &priv->hbox_protocol,
1709       NULL);
1710   g_free (filename);
1711
1712   empathy_builder_connect (gui, dialog,
1713       "button_add", "clicked", accounts_dialog_button_add_clicked_cb,
1714       "button_import", "clicked", accounts_dialog_button_import_clicked_cb,
1715       NULL);
1716
1717   content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
1718
1719   gtk_container_add (GTK_CONTAINER (content_area), top_hbox);
1720
1721   g_object_unref (gui);
1722
1723   action_area = gtk_dialog_get_action_area (GTK_DIALOG (dialog));
1724
1725 #ifdef HAVE_MOBLIN
1726   gtk_widget_hide (action_area);
1727 #endif
1728
1729   priv->combobox_protocol = empathy_protocol_chooser_new ();
1730   gtk_box_pack_start (GTK_BOX (priv->hbox_protocol), priv->combobox_protocol,
1731       TRUE, TRUE, 0);
1732   g_signal_connect (priv->combobox_protocol, "changed",
1733       G_CALLBACK (accounts_dialog_protocol_changed_cb),
1734       dialog);
1735
1736   if (priv->parent_window)
1737     gtk_window_set_transient_for (GTK_WINDOW (dialog),
1738         priv->parent_window);
1739
1740   /* set up spinner */
1741   priv->throbber = ephy_spinner_new ();
1742   ephy_spinner_set_size (EPHY_SPINNER (priv->throbber), GTK_ICON_SIZE_SMALL_TOOLBAR);
1743
1744   priv->infobar = gtk_info_bar_new ();
1745   gtk_container_add (GTK_CONTAINER (priv->alignment_infobar),
1746       priv->infobar);
1747   gtk_widget_show (priv->infobar);
1748
1749   priv->image_status = gtk_image_new_from_icon_name (
1750             empathy_icon_name_for_presence (
1751             TP_CONNECTION_PRESENCE_TYPE_OFFLINE), GTK_ICON_SIZE_SMALL_TOOLBAR);
1752
1753   priv->label_status = gtk_label_new (NULL);
1754
1755   content_area = gtk_info_bar_get_content_area (GTK_INFO_BAR (priv->infobar));
1756   gtk_box_pack_start (GTK_BOX (content_area), priv->throbber,
1757       FALSE, FALSE, 0);
1758   gtk_box_pack_start (GTK_BOX (content_area), priv->image_status,
1759       FALSE, FALSE, 0);
1760   gtk_container_add (GTK_CONTAINER (content_area), priv->label_status);
1761
1762   /* Tweak the dialog */
1763   gtk_window_set_title (GTK_WINDOW (dialog), _("Accounts"));
1764   gtk_window_set_role (GTK_WINDOW (dialog), "accounts");
1765
1766   gtk_window_set_default_size (GTK_WINDOW (dialog), 640, -1);
1767
1768   gtk_window_set_type_hint (GTK_WINDOW (dialog), GDK_WINDOW_TYPE_HINT_DIALOG);
1769
1770   gtk_dialog_set_has_separator (GTK_DIALOG (dialog), FALSE);
1771
1772   /* add dialog buttons */
1773   gtk_button_box_set_layout (GTK_BUTTON_BOX (action_area), GTK_BUTTONBOX_END);
1774
1775   gtk_dialog_add_buttons (GTK_DIALOG (dialog),
1776       GTK_STOCK_HELP, GTK_RESPONSE_HELP,
1777       GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
1778       NULL);
1779
1780   g_signal_connect (dialog, "response",
1781       G_CALLBACK (dialog_response_cb), dialog);
1782
1783   g_signal_connect (dialog, "delete-event",
1784       G_CALLBACK (accounts_dialog_delete_event_cb), dialog);
1785 }
1786
1787 static void
1788 do_dispose (GObject *obj)
1789 {
1790   EmpathyAccountsDialog *dialog = EMPATHY_ACCOUNTS_DIALOG (obj);
1791   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1792
1793   if (priv->dispose_has_run)
1794     return;
1795
1796   priv->dispose_has_run = TRUE;
1797
1798   /* Disconnect signals */
1799   g_signal_handlers_disconnect_by_func (priv->account_manager,
1800       accounts_dialog_account_validity_changed_cb,
1801       dialog);
1802   g_signal_handlers_disconnect_by_func (priv->account_manager,
1803       accounts_dialog_account_removed_cb,
1804       dialog);
1805   g_signal_handlers_disconnect_by_func (priv->account_manager,
1806       accounts_dialog_account_enabled_cb,
1807       dialog);
1808   g_signal_handlers_disconnect_by_func (priv->account_manager,
1809       accounts_dialog_account_disabled_cb,
1810       dialog);
1811   g_signal_handlers_disconnect_by_func (priv->account_manager,
1812       accounts_dialog_manager_ready_cb,
1813       dialog);
1814
1815   if (priv->connecting_id)
1816     g_source_remove (priv->connecting_id);
1817
1818   if (priv->account_manager != NULL)
1819     {
1820       g_object_unref (priv->account_manager);
1821       priv->account_manager = NULL;
1822     }
1823
1824   if (priv->cms != NULL)
1825     {
1826       g_object_unref (priv->cms);
1827       priv->cms = NULL;
1828     }
1829
1830   if (priv->initial_selection != NULL)
1831     g_object_unref (priv->initial_selection);
1832   priv->initial_selection = NULL;
1833
1834   G_OBJECT_CLASS (empathy_accounts_dialog_parent_class)->dispose (obj);
1835 }
1836
1837 static GObject *
1838 do_constructor (GType type,
1839     guint n_props,
1840     GObjectConstructParam *props)
1841 {
1842   GObject *retval;
1843
1844   if (dialog_singleton)
1845     {
1846       retval = G_OBJECT (dialog_singleton);
1847     }
1848   else
1849     {
1850       retval =
1851         G_OBJECT_CLASS (empathy_accounts_dialog_parent_class)->constructor
1852             (type, n_props, props);
1853
1854       dialog_singleton = EMPATHY_ACCOUNTS_DIALOG (retval);
1855       g_object_add_weak_pointer (retval, (gpointer) &dialog_singleton);
1856     }
1857
1858   return retval;
1859 }
1860
1861 static void
1862 do_get_property (GObject *object,
1863     guint property_id,
1864     GValue *value,
1865     GParamSpec *pspec)
1866 {
1867   EmpathyAccountsDialogPriv *priv = GET_PRIV (object);
1868
1869   switch (property_id)
1870     {
1871     case PROP_PARENT:
1872       g_value_set_object (value, priv->parent_window);
1873       break;
1874     default:
1875       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
1876     }
1877 }
1878
1879 static void
1880 do_set_property (GObject *object,
1881     guint property_id,
1882     const GValue *value,
1883     GParamSpec *pspec)
1884 {
1885   EmpathyAccountsDialogPriv *priv = GET_PRIV (object);
1886
1887   switch (property_id)
1888     {
1889     case PROP_PARENT:
1890       priv->parent_window = g_value_get_object (value);
1891       break;
1892     default:
1893       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
1894     }
1895 }
1896
1897 static void
1898 do_constructed (GObject *object)
1899 {
1900   EmpathyAccountsDialog *dialog = EMPATHY_ACCOUNTS_DIALOG (object);
1901   EmpathyAccountsDialogPriv *priv = GET_PRIV (dialog);
1902   gboolean import_asked;
1903
1904   accounts_dialog_build_ui (dialog);
1905   accounts_dialog_model_setup (dialog);
1906
1907   /* Set up signalling */
1908   priv->account_manager = tp_account_manager_dup ();
1909
1910   tp_account_manager_prepare_async (priv->account_manager, NULL,
1911       accounts_dialog_manager_ready_cb, dialog);
1912
1913   empathy_conf_get_bool (empathy_conf_get (),
1914       EMPATHY_PREFS_IMPORT_ASKED, &import_asked);
1915
1916   if (empathy_import_accounts_to_import ())
1917     {
1918       gtk_widget_show (priv->button_import);
1919
1920       if (!import_asked)
1921         {
1922           GtkWidget *import_dialog;
1923
1924           empathy_conf_set_bool (empathy_conf_get (),
1925               EMPATHY_PREFS_IMPORT_ASKED, TRUE);
1926           import_dialog = empathy_import_dialog_new (GTK_WINDOW (dialog),
1927               FALSE);
1928           gtk_widget_show (import_dialog);
1929         }
1930     }
1931 }
1932
1933 static void
1934 empathy_accounts_dialog_class_init (EmpathyAccountsDialogClass *klass)
1935 {
1936   GObjectClass *oclass = G_OBJECT_CLASS (klass);
1937   GParamSpec *param_spec;
1938
1939   oclass->constructor = do_constructor;
1940   oclass->dispose = do_dispose;
1941   oclass->constructed = do_constructed;
1942   oclass->set_property = do_set_property;
1943   oclass->get_property = do_get_property;
1944
1945   param_spec = g_param_spec_object ("parent",
1946       "parent", "The parent window",
1947       GTK_TYPE_WINDOW,
1948       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY);
1949   g_object_class_install_property (oclass, PROP_PARENT, param_spec);
1950
1951   g_type_class_add_private (klass, sizeof (EmpathyAccountsDialogPriv));
1952 }
1953
1954 static void
1955 empathy_accounts_dialog_init (EmpathyAccountsDialog *dialog)
1956 {
1957   EmpathyAccountsDialogPriv *priv;
1958
1959   priv = G_TYPE_INSTANCE_GET_PRIVATE ((dialog),
1960       EMPATHY_TYPE_ACCOUNTS_DIALOG,
1961       EmpathyAccountsDialogPriv);
1962   dialog->priv = priv;
1963 }
1964
1965 /* public methods */
1966
1967 GtkWidget *
1968 empathy_accounts_dialog_show (GtkWindow *parent,
1969     TpAccount *selected_account)
1970 {
1971   EmpathyAccountsDialog *dialog;
1972   EmpathyAccountsDialogPriv *priv;
1973
1974   dialog = g_object_new (EMPATHY_TYPE_ACCOUNTS_DIALOG,
1975       "parent", parent, NULL);
1976
1977   priv = GET_PRIV (dialog);
1978
1979   if (selected_account)
1980     {
1981       if (priv->cms != NULL && empathy_connection_managers_is_ready (priv->cms))
1982         accounts_dialog_set_selected_account (dialog, selected_account);
1983       else
1984         /* save the selection to set it later when the cms
1985          * becomes ready.
1986          */
1987         priv->initial_selection = g_object_ref (selected_account);
1988     }
1989
1990   gtk_window_present (GTK_WINDOW (dialog));
1991
1992   return GTK_WIDGET (dialog);
1993 }