1/*
2 * Copyright (C) 2011 Alexander Larsson <alexl@redhat.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18using Folks;
19
20public class Contacts.App : Gtk.Application {
21  private Settings settings;
22
23  private Store contacts_store;
24
25  private MainWindow window;
26
27  private const GLib.ActionEntry[] action_entries = {
28    { "quit",             quit                },
29    { "help",             show_help           },
30    { "about",            show_about          },
31    { "change-book",      change_address_book },
32    { "online-accounts",  online_accounts     },
33    { "new-contact",      new_contact         },
34    { "show-contact",     on_show_contact, "s"}
35  };
36
37  private const OptionEntry[] options = {
38    { "email",       'e', 0, OptionArg.STRING, null, N_("Show contact with this email address") },
39    { "individual",  'i', 0, OptionArg.STRING, null, N_("Show contact with this individual id") },
40    { "search",      's', 0, OptionArg.STRING, null, N_("Show contacts with the given filter") },
41    { "version",     'v', 0, OptionArg.NONE,   null, N_("Show the current version of Contacts") },
42    {}
43  };
44
45  public App () {
46    Object (
47      application_id: Config.APP_ID,
48      flags: ApplicationFlags.HANDLES_COMMAND_LINE
49    );
50
51    this.settings = new Settings (this);
52    add_main_option_entries (options);
53  }
54
55  public override int command_line (ApplicationCommandLine command_line) {
56    var options = command_line.get_options_dict ();
57
58    activate ();
59
60    if ("individual" in options) {
61      var individual = options.lookup_value ("individual", VariantType.STRING);
62      if (individual != null)
63        show_individual.begin (individual.get_string ());
64    } else if ("email" in options) {
65      var email = options.lookup_value ("email", VariantType.STRING);
66      if (email != null)
67        show_by_email.begin (email.get_string ());
68    } else if ("search" in options) {
69      var search_term = options.lookup_value ("search", VariantType.STRING);
70      if (search_term != null)
71        show_search (search_term.get_string ());
72    }
73
74    return 0;
75  }
76
77  public override int handle_local_options (VariantDict options) {
78    if ("version" in options) {
79      stdout.printf ("%s %s\n", Config.PACKAGE_NAME, Config.PACKAGE_VERSION);
80      return 0;
81    }
82
83    return -1;
84  }
85
86  public void show_contact (Individual? individual) {
87    window.set_shown_contact (individual);
88  }
89
90  public async void show_individual (string id) {
91    if (contacts_store.is_quiescent) {
92      show_individual_ready.begin (id);
93    } else {
94      contacts_store.quiescent.connect( () => {
95        show_individual_ready.begin (id);
96      });
97    }
98  }
99
100  private async void show_individual_ready (string id) {
101    Individual? contact = null;
102    try {
103      contact = yield contacts_store.aggregator.look_up_individual (id);
104    } catch (Error e) {
105      debug ("Couldn't look up individual");
106    }
107    if (contact != null) {
108      show_contact (contact);
109    } else {
110      var dialog = new Gtk.MessageDialog (this.window, Gtk.DialogFlags.DESTROY_WITH_PARENT,
111                                          Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE,
112                                          _("No contact with id %s found"), id);
113      dialog.set_title (_("Contact not found"));
114      dialog.run ();
115      dialog.destroy ();
116    }
117  }
118
119  public void change_address_book () {
120    var dialog = new AddressbookDialog (this.contacts_store, this.window);
121    dialog.run ();
122    dialog.destroy ();
123  }
124
125  public void online_accounts () {
126    try {
127      var proxy = new DBusProxy.for_bus_sync (BusType.SESSION,
128                                              DBusProxyFlags.NONE,
129                                              null,
130                                              "org.gnome.ControlCenter",
131                                              "/org/gnome/ControlCenter",
132                                              "org.gtk.Actions");
133
134      var builder = new VariantBuilder (new VariantType ("av") );
135      builder.add ("v", new Variant.string (""));
136      var param = new Variant.tuple ({
137        new Variant.string ("launch-panel"),
138        new Variant.array (new VariantType ("v"), {
139          new Variant ("v", new Variant ("(sav)", "online-accounts", builder))
140        }),
141        new Variant.array (new VariantType ("{sv}"), {})
142      });
143
144      proxy.call_sync ("Activate", param, DBusCallFlags.NONE, -1);
145    } catch (Error e) {
146      // TODO: Show error dialog
147      warning ("Couldn't open online-accounts: %s", e.message);
148    }
149  }
150
151  public void show_help () {
152    try {
153      Gtk.show_uri_on_window (window, "help:gnome-help/contacts", Gtk.get_current_event_time ());
154    } catch (GLib.Error e1) {
155      warning ("Error showing help: %s", e1.message);
156    }
157  }
158
159  public void show_about () {
160    string[] authors = {
161      "Alexander Larsson <alexl@redhat.com>",
162      "Erick Pérez Castellanos <erick.red@gmail.com>",
163      "Niels De Graef <nielsdegraef@gmail.com>",
164      "Julian Sparber <jsparber@gnome.org>"
165    };
166    string[] artists = {
167      "Allan Day <allanpday@gmail.com>"
168    };
169    Gtk.show_about_dialog (window,
170                           "artists", artists,
171                           "authors", authors,
172                           "translator-credits", _("translator-credits"),
173                           "program-name", _("GNOME Contacts"),
174                           "title", _("About GNOME Contacts"),
175                           "comments", _("Contact Management Application"),
176                           "copyright", _("© 2011 Red Hat, Inc.\n© 2011-2020 The Contacts Developers"),
177                           "license-type", Gtk.License.GPL_2_0,
178                           "logo-icon-name", Config.APP_ID,
179                           "version", Config.PACKAGE_VERSION,
180                           "website", "https://wiki.gnome.org/Apps/Contacts",
181                           "wrap-license", true);
182  }
183
184  public async void show_by_email (string email_address) {
185    var query = new SimpleQuery(email_address, { "email-addresses" });
186    Individual individual = yield contacts_store.find_contact (query);
187    if (individual != null) {
188      show_contact (individual);
189    } else {
190      var dialog = new Gtk.MessageDialog (this.window, Gtk.DialogFlags.DESTROY_WITH_PARENT,
191                                          Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE,
192                                          _("No contact with email address %s found"), email_address);
193      dialog.set_title (_("Contact not found"));
194      dialog.run ();
195      dialog.destroy ();
196    }
197  }
198
199  public void show_search (string query) {
200    if (contacts_store.is_quiescent) {
201      window.show_search (query);
202    } else {
203      contacts_store.quiescent.connect_after (() => {
204        window.show_search (query);
205      });
206    }
207  }
208
209  private void create_window () {
210    this.window = new MainWindow (this.settings, this, this.contacts_store);
211
212    show_contact_list ();
213  }
214
215  // We have to wait until our Store is quiescent before showing contacts.
216  // However, some backends can take quite a while to load (or even timeout),
217  // so make sure we also show something within a reasonable time frame.
218  private const int LOADING_TIMEOUT = 1; // in seconds
219
220  private void show_contact_list () {
221    uint timeout_id = 0;
222
223    // Happy flow callback
224    ulong quiescence_id = contacts_store.quiescent.connect (() => {
225      Source.remove (timeout_id);
226      debug ("Got quiescent in time. Showing contact list");
227      window.show_contact_list ();
228    });
229
230    // Timeout callback
231    timeout_id = Timeout.add_seconds (LOADING_TIMEOUT, () => {
232      contacts_store.disconnect (quiescence_id);
233
234      debug ("Didn't achieve quiescence in time! Showing contact list anyway");
235      window.show_contact_list ();
236      return false;
237    });
238  }
239
240  public override void startup () {
241    if (!ensure_eds_accounts (true))
242      quit ();
243
244    this.contacts_store = new Store ();
245    base.startup ();
246
247    Hdy.init ();
248
249    load_styling ();
250    create_actions ();
251  }
252
253  private void create_actions () {
254    this.add_action_entries (action_entries, this);
255
256    this.set_accels_for_action ("app.help", {"F1"});
257    this.set_accels_for_action ("app.new-contact", {"<Primary>n"});
258    this.set_accels_for_action ("win.show-help-overlay", {"<Primary>question"});
259  }
260
261  public void load_styling () {
262    var provider = new Gtk.CssProvider ();
263    provider.load_from_resource ("/org/gnome/Contacts/ui/style.css");
264    Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default(),
265                                              provider,
266                                              Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
267  }
268
269  public override void activate () {
270    // Check if we've already done the setup process
271    if (this.settings.did_initial_setup)
272      create_window ();
273    else
274      run_setup ();
275  }
276
277  private void run_setup () {
278    // Disable the change-book action (don't want the user to do that during setup)
279    var change_book_action = lookup_action ("change-book") as SimpleAction;
280    change_book_action.set_enabled (false);
281
282    // Create and show the setup window
283    var setup_window = new SetupWindow (this, this.contacts_store);
284    setup_window.setup_done.connect ( (selected_store) => {
285        setup_window.destroy ();
286
287        eds_source_registry.set_default_address_book (selected_store.source);
288        this.settings.did_initial_setup = true;
289
290        change_book_action.set_enabled (true); // re-enable change-book action
291        create_window ();
292      });
293    setup_window.show ();
294  }
295
296  public void new_contact () {
297    this.window.new_contact ();
298  }
299
300  private void on_show_contact(SimpleAction action, Variant? param) {
301    activate();
302
303    var individual = param as string;
304    if (individual != null)
305      show_individual.begin (individual);
306  }
307
308}
309