Various stability fixes and logging improvements
[moonshot-ui.git] / src / moonshot-identity-management-view.vala
1 /*
2  * Copyright (c) 2011-2016, JANET(UK)
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  *
12  * 2. Redistributions in binary form must reproduce the above copyright
13  *    notice, this list of conditions and the following disclaimer in the
14  *    documentation and/or other materials provided with the distribution.
15  *
16  * 3. Neither the name of JANET(UK) nor the names of its contributors
17  *    may be used to endorse or promote products derived from this software
18  *    without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
26  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
27  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
29  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30  * SUCH DAMAGE.
31 */
32 using Gee;
33 using Gtk;
34 using WebProvisioning;
35
36 public class IdentityManagerView : Window {
37     static MoonshotLogger logger = get_logger("IdentityManagerView");
38
39     bool use_flat_file_store = false;
40
41     // The latest year in which Moonshot sources were modified.
42     private static int LATEST_EDIT_YEAR = 2016;
43
44     public static Gdk.Color white = make_color(65535, 65535, 65535);
45
46     private const int WINDOW_WIDTH = 700;
47     private const int WINDOW_HEIGHT = 500;
48     protected IdentityManagerApp parent_app;
49     #if OS_MACOS
50         public OSXApplication osxApp;
51     #endif
52     private UIManager ui_manager = new UIManager();
53     private Entry search_entry;
54     private CustomVBox custom_vbox;
55     private VBox service_prompt_vbox;
56     private Button edit_button;
57     private Button remove_button;
58
59     private Button send_button;
60     
61     private Gtk.ListStore* listmodel;
62     private TreeModelFilter filter;
63
64     internal IdentityManagerModel identities_manager;
65     private unowned SList<IdCard>    candidates;
66
67     private GLib.Queue<IdentityRequest> request_queue;
68
69     internal CheckButton remember_identity_binding = null;
70
71     private IdCard selected_card = null;
72
73     private string import_directory = null;
74
75     private enum Columns
76     {
77         IDCARD_COL,
78         LOGO_COL,
79         ISSUER_COL,
80         USERNAME_COL,
81         PASSWORD_COL,
82         N_COLUMNS
83     }
84
85     private const string menu_layout =
86     "<menubar name='MenuBar'>" +
87     "        <menu name='HelpMenu' action='HelpMenuAction'>" +
88     "             <menuitem name='About' action='AboutAction' />" +
89     "        </menu>" +
90     "</menubar>";
91
92     public IdentityManagerView(IdentityManagerApp app, bool use_flat_file_store) {
93         parent_app = app;
94         this.use_flat_file_store = use_flat_file_store;
95
96         #if OS_MACOS
97             osxApp = OSXApplication.get_instance();
98         #endif
99         identities_manager = parent_app.model;
100         request_queue = new GLib.Queue<IdentityRequest>();
101         this.title = _("Moonshot Identity Selector");
102         this.set_position(WindowPosition.CENTER);
103         set_default_size(WINDOW_WIDTH, WINDOW_HEIGHT);
104         build_ui();
105         setup_list_model(); 
106         load_id_cards();
107         connect_signals();
108         report_duplicate_nais(); 
109     }
110     
111     private void report_duplicate_nais() {
112         ArrayList<ArrayList<IdCard>> duplicates;
113         identities_manager.find_duplicate_nai_sets(out duplicates);
114         foreach (ArrayList<IdCard> list in duplicates) {
115             string message = _("The following identities use the same Network Access Identifier (NAI),\n'%s'.").printf(list.get(0).nai)
116                 + _("\n\nDuplicate NAIs are not allowed. Please remove identities you don't need, or modify") 
117                 + _(" user ID or issuer fields so that they are no longer the same NAI.");
118
119             foreach (var card in list) {
120                 message += _("\n\nDisplay Name: '%s'\nServices:\n     %s").printf(card.display_name, card.get_services_string(",\n     "));
121             }
122             var msg_dialog = new Gtk.MessageDialog(this,
123                                                    Gtk.DialogFlags.DESTROY_WITH_PARENT,
124                                                    Gtk.MessageType.INFO,
125                                                    Gtk.ButtonsType.OK,
126                                                    "%s",
127                                                    message);
128             msg_dialog.run();
129             msg_dialog.destroy();
130         }
131     }
132
133     private void on_card_list_changed() {
134         logger.trace("on_card_list_changed");
135         load_id_cards();
136     }
137     
138     private bool visible_func(TreeModel model, TreeIter iter)
139     {
140         IdCard id_card;
141
142         model.get(iter,
143                   Columns.IDCARD_COL, out id_card);
144
145         if (id_card == null)
146             return false;
147         
148         if (candidates != null)
149         {
150             bool is_candidate = false;
151             foreach (IdCard candidate in candidates)
152             {
153                 if (candidate == id_card)
154                     is_candidate = true;
155             }
156             if (!is_candidate)
157                 return false;
158         }
159         
160         string entry_text = search_entry.get_text();
161         if (entry_text == null || entry_text == "")
162         {
163             return true;
164         }
165
166         foreach (string search_text in entry_text.split(" "))
167         {
168             if (search_text == "")
169                 continue;
170          
171
172             string search_text_casefold = search_text.casefold();
173
174             if (id_card.issuer != null)
175             {
176                 string issuer_casefold = id_card.issuer;
177
178                 if (issuer_casefold.contains(search_text_casefold))
179                     return true;
180             }
181
182             if (id_card.display_name != null)
183             {
184                 string display_name_casefold = id_card.display_name.casefold();
185               
186                 if (display_name_casefold.contains(search_text_casefold))
187                     return true;
188             }
189             
190             if (id_card.services.size > 0)
191             {
192                 foreach (string service in id_card.services)
193                 {
194                     string service_casefold = service.casefold();
195
196                     if (service_casefold.contains(search_text_casefold))
197                         return true;
198                 }
199             }
200         }
201         return false;
202     }
203
204     private void setup_list_model()
205     {
206         this.listmodel = new Gtk.ListStore(Columns.N_COLUMNS, typeof(IdCard),
207                                            typeof(Gdk.Pixbuf),
208                                            typeof(string),
209                                            typeof(string),
210                                            typeof(string));
211         this.filter = new TreeModelFilter(listmodel, null);
212
213         filter.set_visible_func(visible_func);
214     }
215
216     private void search_entry_text_changed_cb()
217     {
218         this.filter.refilter();
219         redraw_id_card_widgets();
220     }
221
222     private bool search_entry_key_press_event_cb(Gdk.EventKey e)
223     {
224         if(Gdk.keyval_name(e.keyval) == "Escape")
225             this.search_entry.set_text("");
226
227         // Continue processing this event, since the
228         // text entry functionality needs to see it too.
229         return false;
230     }
231
232     private void load_id_cards() {
233         logger.trace("load_id_cards");
234
235         custom_vbox.clear();
236         this.listmodel->clear();
237         LinkedList<IdCard> card_list = identities_manager.get_card_list() ;
238         if (card_list == null) {
239             return;
240         }
241
242         foreach (IdCard id_card in card_list) {
243             logger.trace(@"load_id_cards: Loading card with display name '$(id_card.display_name)'");
244             add_id_card_data(id_card);
245             add_id_card_widget(id_card);
246         }
247     }
248     
249     private IdCard update_id_card_data(IdentityDialog dialog, IdCard id_card)
250     {
251         id_card.display_name = dialog.display_name;
252         id_card.issuer = dialog.issuer;
253         id_card.username = dialog.username;
254         id_card.password = dialog.password;
255         id_card.store_password = dialog.store_password;
256
257         id_card.update_services_from_list(dialog.get_services());
258
259         if (dialog.clear_trust_anchor) {
260             id_card.clear_trust_anchor();
261         }
262
263         return id_card;
264     }
265
266     private void add_id_card_data(IdCard id_card)
267     {
268         TreeIter   iter;
269         Gdk.Pixbuf pixbuf;
270         this.listmodel->append(out iter);
271         pixbuf = get_pixbuf(id_card);
272         listmodel->set(iter,
273                        Columns.IDCARD_COL, id_card,
274                        Columns.LOGO_COL, pixbuf,
275                        Columns.ISSUER_COL, id_card.issuer,
276                        Columns.USERNAME_COL, id_card.username,
277                        Columns.PASSWORD_COL, id_card.password);
278     }
279
280     private IdCardWidget add_id_card_widget(IdCard id_card)
281     {
282         if (id_card == null) {
283             logger.trace("add_id_card_widget: id_card == null; returning.");
284             return null;
285         }
286
287         logger.trace("add_id_card_widget: id_card.nai='%s'; selected nai='%s'"
288                      .printf(id_card.nai, 
289                              this.selected_card == null ? "[null selection]" : this.selected_card.nai));
290
291
292         var id_card_widget = new IdCardWidget(id_card, this);
293         this.custom_vbox.add_id_card_widget(id_card_widget);
294         id_card_widget.expanded.connect(this.widget_selected_cb);
295         id_card_widget.collapsed.connect(this.widget_unselected_cb);
296
297         if (this.selected_card != null && this.selected_card.nai == id_card.nai) {
298             logger.trace(@"add_id_card_widget: Expanding selected idcard widget");
299             id_card_widget.expand();
300
301             // After a card is added, modified, or deleted, we reload all the cards.
302             // (I'm not sure why, or if it's necessary to do this.) This means that the
303             // selected_card may now point to a card instance that's not in the current list.
304             // Hence the only way to carry the selection across reloads is to identify
305             // the selected card by its NAI. And hence we need to reset what our idea of the
306             // "selected card" is.
307             // There should be a better way to do this, especially since we're not great
308             // at preventing duplicate NAIs.
309             this.selected_card = id_card;
310         }
311         return id_card_widget;
312     }
313
314     private void widget_selected_cb(IdCardWidget id_card_widget)
315     {
316         logger.trace(@"widget_selected_cb: id_card_widget.id_card.display_name='$(id_card_widget.id_card.display_name)'");
317
318         this.selected_card = id_card_widget.id_card;
319         bool allow_removes = !id_card_widget.id_card.is_no_identity();
320         this.remove_button.set_sensitive(allow_removes);
321         this.edit_button.set_sensitive(true);
322         this.custom_vbox.receive_expanded_event(id_card_widget);
323
324         if (this.selection_in_progress())
325              this.send_button.set_sensitive(true);
326     }
327
328     private void widget_unselected_cb(IdCardWidget id_card_widget)
329     {
330         logger.trace(@"widget_unselected_cb: id_card_widget.id_card.display_name='$(id_card_widget.id_card.display_name)'");
331
332         this.selected_card = null;
333         this.remove_button.set_sensitive(false);
334         this.edit_button.set_sensitive(false);
335         this.custom_vbox.receive_collapsed_event(id_card_widget);
336
337         this.send_button.set_sensitive(false);
338     }
339
340     public bool add_identity(IdCard id_card, bool force_flat_file_store, out ArrayList<IdCard>? old_duplicates=null)
341     {
342         #if OS_MACOS
343         /* 
344          * TODO: We should have a confirmation dialog, but currently it will crash on Mac OS
345          * so for now we will install silently
346          */
347         var ret = Gtk.ResponseType.YES;
348         #else
349         Gtk.MessageDialog dialog;
350         IdCard? prev_id = identities_manager.find_id_card(id_card.nai, force_flat_file_store);
351         logger.trace("add_identity(flat=%s, card='%s'): find_id_card returned %s"
352                      .printf(force_flat_file_store.to_string(), id_card.display_name, (prev_id != null ? prev_id.display_name : "null")));
353         if (prev_id!=null) {
354             int flags = prev_id.Compare(id_card);
355             logger.trace("add_identity: compare returned " + flags.to_string());
356             if (flags == 0) {
357                 if (&old_duplicates != null) {
358                     old_duplicates = new ArrayList<IdCard>();
359                 }
360
361                 return false; // no changes, no need to update
362             } else if ((flags & (1 << IdCard.DiffFlags.DISPLAY_NAME)) != 0) {
363                 dialog = new Gtk.MessageDialog(this,
364                                                Gtk.DialogFlags.DESTROY_WITH_PARENT,
365                                                Gtk.MessageType.QUESTION,
366                                                Gtk.ButtonsType.YES_NO,
367                                                _("Would you like to replace ID Card '%s' using nai '%s' with the new ID Card '%s'?"),
368                                                prev_id.display_name,
369                                                prev_id.nai,
370                                                id_card.display_name);
371             } else {
372                 dialog = new Gtk.MessageDialog(this,
373                                                Gtk.DialogFlags.DESTROY_WITH_PARENT,
374                                                Gtk.MessageType.QUESTION,
375                                                Gtk.ButtonsType.YES_NO,
376                                                _("Would you like to update ID Card '%s' using nai '%s'?"),
377                                                id_card.display_name,
378                                                id_card.nai);
379             }
380         } else {
381             dialog = new Gtk.MessageDialog(this,
382                                            Gtk.DialogFlags.DESTROY_WITH_PARENT,
383                                            Gtk.MessageType.QUESTION,
384                                            Gtk.ButtonsType.YES_NO,
385                                            _("Would you like to add '%s' ID Card to the ID Card Organizer?"),
386                                            id_card.display_name);
387         }
388         var ret = dialog.run();
389         dialog.destroy();
390         #endif
391
392         if (ret == Gtk.ResponseType.YES) {
393             this.identities_manager.add_card(id_card, force_flat_file_store, out old_duplicates);
394             return true;
395         }
396         else {
397             if (&old_duplicates != null) {
398                 old_duplicates = new ArrayList<IdCard>();
399             }
400             return false;
401         }
402     }
403
404     private void add_identity_cb()
405     {
406         var dialog = new IdentityDialog(this);
407         int result = ResponseType.CANCEL;
408         while (!dialog.complete)
409             result = dialog.run();
410
411         switch (result) {
412         case ResponseType.OK:
413             this.identities_manager.add_card(update_id_card_data(dialog, new IdCard()), false);
414             break;
415         default:
416             break;
417         }
418         dialog.destroy();
419     }
420
421     private void edit_identity_cb(IdCard card)
422     {
423         var dialog = new IdentityDialog.with_idcard(card, _("Edit Identity"), this);
424         int result = ResponseType.CANCEL;
425         while (!dialog.complete)
426             result = dialog.run();
427
428         switch (result) {
429         case ResponseType.OK:
430             this.identities_manager.update_card(update_id_card_data(dialog, card));
431
432             // Make sure we haven't created a duplicate NAI via this update.
433             report_duplicate_nais();
434             break;
435         default:
436             break;
437         }
438         dialog.destroy();
439     }
440
441     private void remove_identity(IdCard id_card)
442     {
443         logger.trace(@"remove_identity: id_card.display_name='$(id_card.display_name)'");
444
445         this.selected_card = null;
446         this.identities_manager.remove_card(id_card);
447
448         // Nothing is selected, so disable buttons
449         this.edit_button.set_sensitive(false);
450         this.remove_button.set_sensitive(false);
451         this.send_button.set_sensitive(false);
452     }
453
454     private void redraw_id_card_widgets()
455     {
456         TreeIter iter;
457         IdCard id_card;
458
459         this.custom_vbox.clear();
460
461         if (filter.get_iter_first(out iter))
462         {
463             do
464             {
465                 filter.get(iter,
466                            Columns.IDCARD_COL, out id_card);
467
468                 add_id_card_widget(id_card);
469             }
470             while (filter.iter_next(ref iter));
471         }
472     }
473
474     private void remove_identity_cb(IdCard id_card)
475     {
476         bool remove = WarningDialog.confirm(this, 
477                                             Markup.printf_escaped(
478                                                 "<span font-weight='heavy'>" + _("You are about to remove the identity '%s'.") + "</span>",
479                                                 id_card.display_name)
480                                             + "\n\n" + _("Are you sure you want to do this?"),
481                                             "delete_idcard");
482         if (remove) 
483             remove_identity(id_card);
484     }
485
486     private void set_prompting_service(string service)
487     {
488         clear_selection_prompts();
489
490         var prompting_service = new Label(_("Identity requested for service:\n%s").printf(service));
491         prompting_service.set_line_wrap(true);
492
493         // left-align
494         prompting_service.set_alignment(0, (float )0.5);
495
496         var selection_prompt = new Label(_("Select your identity:"));
497         selection_prompt.set_alignment(0, 1);
498
499         this.service_prompt_vbox.pack_start(prompting_service, false, false, 12);
500         this.service_prompt_vbox.pack_start(selection_prompt, false, false, 2);
501         this.service_prompt_vbox.show_all();
502     }
503
504     private void clear_selection_prompts()
505     {
506         var list = service_prompt_vbox.get_children();
507         foreach (Widget w in list)
508         {
509             service_prompt_vbox.remove(w);
510         }
511     }
512
513
514     public void queue_identity_request(IdentityRequest request)
515     {
516         bool queue_was_empty = !this.selection_in_progress();
517         this.request_queue.push_tail(request);
518
519         if (queue_was_empty)
520         { /* setup widgets */
521             candidates = request.candidates;
522             filter.refilter();
523             redraw_id_card_widgets();
524             set_prompting_service(request.service);
525             remember_identity_binding.show();
526
527             if (this.custom_vbox.find_idcard_widget(this.selected_card) != null) {
528                 // A widget is already selected, and has not been filtered out of the display via search
529                 send_button.set_sensitive(true);
530             }
531
532             make_visible();
533         }
534     }
535
536
537     /** Makes the window visible, or at least, notifies the user that the window
538      * wants to be visible.
539      *
540      * This differs from show() in that show() does not guarantee that the 
541      * window will be moved to the foreground. Actually, neither does this
542      * method, because the user's settings and window manager may affect the
543      * behavior significantly.
544      */
545     public void make_visible()
546     {
547         set_urgency_hint(true);
548         present();
549     }
550
551     public IdCard check_add_password(IdCard identity, IdentityRequest request, IdentityManagerModel model)
552     {
553         logger.trace(@"check_add_password");
554         IdCard retval = identity;
555         bool idcard_has_pw = (identity.password != null) && (identity.password != "");
556         bool request_has_pw = (request.password != null) && (request.password != "");
557         if ((!idcard_has_pw) && (!identity.is_no_identity())) {
558             if (request_has_pw) {
559                 identity.password = request.password;
560                 retval = model.update_card(identity);
561             } else {
562                 var dialog = new AddPasswordDialog(identity, request);
563                 var result = dialog.run();
564
565                 switch (result) {
566                 case ResponseType.OK:
567                     identity.password = dialog.password;
568                     // Don't leave passwords in memory longer than necessary.
569                     // (This may not actually clear the data, but it's the best we can do.)
570                     dialog.clear_password();
571                     identity.store_password = dialog.remember;
572                     if (dialog.remember)
573                         identity.temporary = false;
574                     retval = model.update_card(identity);
575                     break;
576                 default:
577                     identity = null;
578                     break;
579                 }
580                 // Do this again, in case OK button wasn't selected.
581                 dialog.clear_password();
582                 dialog.destroy();
583             }
584         }
585         return retval;
586     }
587
588     private void send_identity_cb(IdCard id)
589     {
590         return_if_fail(this.selection_in_progress());
591
592         var request = this.request_queue.pop_head();
593         var identity = check_add_password(id, request, identities_manager);
594         send_button.set_sensitive(false);
595
596         candidates = null;
597       
598         if (!this.selection_in_progress())
599         {
600             candidates = null;
601             clear_selection_prompts();
602             if (!parent_app.explicitly_launched) {
603 // The following occasionally causes the app to exit without sending the dbus
604 // reply, so for now we just don't exit
605 //                Gtk.main_quit();
606 // just hide instead
607                 this.hide();
608             }
609         } else {
610             IdentityRequest next = this.request_queue.peek_head();
611             candidates = next.candidates;
612             set_prompting_service(next.service);
613         }
614         filter.refilter();
615         redraw_id_card_widgets();
616
617         if ((identity != null) && (!identity.is_no_identity()))
618             parent_app.default_id_card = identity;
619
620         request.return_identity(identity, remember_identity_binding.active);
621
622         remember_identity_binding.active = true;
623         remember_identity_binding.hide();
624     }
625
626     private void on_about_action()
627     {
628         string copyright = "Copyright (c) 2011, %d JANET".printf(LATEST_EDIT_YEAR);
629
630         string license =
631         """
632 Copyright (c) 2011, %d JANET(UK)
633 All rights reserved.
634
635 Redistribution and use in source and binary forms, with or without
636 modification, are permitted provided that the following conditions
637 are met:
638
639 1. Redistributions of source code must retain the above copyright
640    notice, this list of conditions and the following disclaimer.
641
642 2. Redistributions in binary form must reproduce the above copyright
643    notice, this list of conditions and the following disclaimer in the
644    documentation and/or other materials provided with the distribution.
645
646 3. Neither the name of JANET(UK) nor the names of its contributors
647    may be used to endorse or promote products derived from this software
648    without specific prior written permission.
649
650 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"
651 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
652 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
653 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
654 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
655 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
656 OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
657 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
658 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
659 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
660 SUCH DAMAGE.
661 """.printf(LATEST_EDIT_YEAR);
662
663         AboutDialog about = new AboutDialog();
664
665         about.set_comments(_("Moonshot project UI"));
666         about.set_copyright(copyright);
667         about.set_website(Config.PACKAGE_URL);
668         about.set_website_label(_("Visit the Moonshot project web site"));
669
670         // Note: The package version is configured at the top of moonshot/ui/configure.ac
671         about.set_version(Config.PACKAGE_VERSION);
672         about.set_license(license);
673         about.set_modal(true);
674         about.set_transient_for(this);
675         about.response.connect((a, b) => {about.destroy();});
676         set_bg_color(about);
677         
678         about.run();
679     }
680
681     private Gtk.ActionEntry[] create_actions() {
682         Gtk.ActionEntry[] actions = new Gtk.ActionEntry[0];
683
684         Gtk.ActionEntry helpmenu = { "HelpMenuAction",
685                                      null,
686                                      N_("_Help"),
687                                      null, null, null };
688
689         // Pick up the translated version of the name, if any
690         helpmenu.label = dgettext(null, helpmenu.label);
691         actions += helpmenu;
692
693         Gtk.ActionEntry about = { "AboutAction",
694                                   #if VALA_0_12
695                                   Stock.ABOUT,
696                                   #else
697                                   STOCK_ABOUT,
698                                   #endif
699                                   N_("About"),
700                                   null,
701                                   N_("About this application"),
702                                   on_about_action };
703
704         about.label = dgettext(null, about.label);
705         actions += about;
706
707         return actions;
708     }
709
710
711     private void create_ui_manager()
712     {
713         Gtk.ActionGroup action_group = new Gtk.ActionGroup("GeneralActionGroup");
714         action_group.add_actions(create_actions(), this);
715         ui_manager.insert_action_group(action_group, 0);
716         try
717         {
718             ui_manager.add_ui_from_string(menu_layout, -1);
719         }
720         catch (Error e)
721         {
722             stderr.printf("%s\n", e.message);
723             logger.error("create_ui_manager: Caught error: " + e.message);
724         }
725         ui_manager.ensure_update();
726     }
727
728     private void build_ui()
729     {
730         set_bg_color(this);
731
732         create_ui_manager();
733
734         int num_rows = 18;
735         int num_cols = 8;
736         int button_width = 1;
737
738         Table top_table = new Table(num_rows, 10, false);
739         top_table.set_border_width(12);
740
741         AttachOptions fill_and_expand = AttachOptions.EXPAND | AttachOptions.FILL;
742         AttachOptions fill = AttachOptions.FILL;
743         int row = 0;
744
745         service_prompt_vbox = new VBox(false, 0);
746         top_table.attach(service_prompt_vbox, 0, 1, row, row + 1, fill_and_expand, fill_and_expand, 12, 0);
747         row++;
748
749         string search_tooltip_text = _("Search for an identity or service");
750         this.search_entry = new Entry();
751
752         set_atk_name_description(search_entry, _("Search entry"), _("Search for a specific ID Card"));
753         this.search_entry.set_icon_from_pixbuf(EntryIconPosition.SECONDARY,
754                                                find_icon_sized("edit-find", Gtk.IconSize.MENU));
755         this.search_entry.set_icon_tooltip_text(EntryIconPosition.SECONDARY,
756                                                 search_tooltip_text);
757
758         this.search_entry.set_tooltip_text(search_tooltip_text);
759
760         this.search_entry.set_icon_sensitive(EntryIconPosition.SECONDARY, false);
761
762         this.search_entry.notify["text"].connect(search_entry_text_changed_cb);
763         this.search_entry.key_press_event.connect(search_entry_key_press_event_cb);
764         this.search_entry.set_width_chars(24);
765
766         var search_label_markup ="<small>" + search_tooltip_text + "</small>";
767         var full_search_label = new Label(null);
768         full_search_label.set_markup(search_label_markup);
769         full_search_label.set_alignment(1, 0);
770
771         var search_vbox = new VBox(false, 0);
772         search_vbox.pack_start(search_entry, false, false, 0);
773         var search_spacer = new Alignment(0, 0, 0, 0);
774         search_spacer.set_size_request(0, 2);
775         search_vbox.pack_start(search_spacer, false, false, 0);
776         search_vbox.pack_start(full_search_label, false, false, 0);
777
778         // Overlap with the service_prompt_box
779         top_table.attach(search_vbox, 5, num_cols - button_width, row - 1, row + 1, fill_and_expand, fill, 0, 12);
780         row++;
781
782         this.custom_vbox = new CustomVBox(this, false, 2);
783
784         var viewport = new Viewport(null, null);
785         viewport.set_border_width(2);
786         viewport.set_shadow_type(ShadowType.NONE);
787         viewport.add(custom_vbox);
788         var id_scrollwin = new ScrolledWindow(null, null);
789         id_scrollwin.set_policy(PolicyType.NEVER, PolicyType.AUTOMATIC);
790         id_scrollwin.set_shadow_type(ShadowType.IN);
791         id_scrollwin.add_with_viewport(viewport);
792         top_table.attach(id_scrollwin, 0, num_cols - 1, row, num_rows - 1, fill_and_expand, fill_and_expand, 6, 0);
793
794         // Right below id_scrollwin:
795         remember_identity_binding = new CheckButton.with_label(_("Remember my identity choice for this service"));
796         remember_identity_binding.active = true;
797         top_table.attach(remember_identity_binding, 0, num_cols / 2, num_rows - 1, num_rows, fill_and_expand, fill_and_expand, 3, 0);
798
799         var add_button = new Button.with_label(_("Add"));
800         add_button.clicked.connect((w) => {add_identity_cb();});
801         top_table.attach(make_rigid(add_button), num_cols - button_width, num_cols, row, row + 1, fill, fill, 0, 0);
802         row++;
803
804         var import_button = new Button.with_label(_("Import"));
805         import_button.clicked.connect((w) => {import_identities_cb();});
806         top_table.attach(make_rigid(import_button), num_cols - button_width, num_cols, row, row + 1, fill, fill, 0, 0);
807         row++;
808
809         this.edit_button = new Button.with_label(_("Edit"));
810         edit_button.clicked.connect((w) => {edit_identity_cb(this.selected_card);});
811         edit_button.set_sensitive(false);
812         top_table.attach(make_rigid(edit_button), num_cols - button_width, num_cols, row, row + 1, fill, fill, 0, 0);
813         row++;
814
815         this.remove_button = new Button.with_label(_("Remove"));
816         remove_button.clicked.connect((w) => {remove_identity_cb(this.selected_card);});
817         remove_button.set_sensitive(false);
818         top_table.attach(make_rigid(remove_button), num_cols - button_width, num_cols, row, row + 1, fill, fill, 0, 0);
819         row++;
820
821         // push the send button down another row.
822         row++;
823         this.send_button = new Button.with_label(_("Send"));
824         send_button.clicked.connect((w) => {send_identity_cb(this.selected_card);});
825         // send_button.set_visible(false);
826         send_button.set_sensitive(false);
827         top_table.attach(make_rigid(send_button), num_cols - button_width, num_cols, row, row + 1, fill, fill, 0, 0);
828         row++;
829
830         var main_vbox = new VBox(false, 0);
831
832 #if OS_MACOS
833         // hide the  File | Quit menu item which is now on the Mac Menu
834 //        Gtk.Widget quit_item =  this.ui_manager.get_widget("/MenuBar/FileMenu/Quit");
835 //        quit_item.hide();
836         
837         Gtk.MenuShell menushell = this.ui_manager.get_widget("/MenuBar") as Gtk.MenuShell;
838
839         osxApp.set_menu_bar(menushell);
840         osxApp.set_use_quartz_accelerators(true);
841         osxApp.sync_menu_bar();
842         osxApp.ready();
843 #else
844         var menubar = this.ui_manager.get_widget("/MenuBar");
845         main_vbox.pack_start(menubar, false, false, 0);
846         set_bg_color(menubar);
847 #endif
848         main_vbox.pack_start(top_table, true, true, 6);
849
850         add(main_vbox);
851         main_vbox.show_all();
852
853         if (!this.selection_in_progress())
854             remember_identity_binding.hide();
855     } 
856
857     internal bool selection_in_progress() {
858         return !this.request_queue.is_empty();
859     }
860
861     private void set_atk_name_description(Widget widget, string name, string description)
862     {
863         var atk_widget = widget.get_accessible();
864
865         atk_widget.set_name(name);
866         atk_widget.set_description(description);
867     }
868
869     private void connect_signals()
870     {
871         this.destroy.connect(() => {
872                 logger.trace("Destroy event; calling Gtk.main_quit()");
873                 Gtk.main_quit();
874             });
875         this.identities_manager.card_list_changed.connect(this.on_card_list_changed);
876         this.delete_event.connect(() => {return confirm_quit();});
877     }
878
879     private bool confirm_quit() {
880         logger.trace("delete_event intercepted; selection_in_progress()=" + selection_in_progress().to_string());
881
882         if (selection_in_progress()) {
883             var result = WarningDialog.confirm(this,
884                                                Markup.printf_escaped(
885                                                    "<span font-weight='heavy'>" + _("Do you wish to use the %s service?") + "</span>",
886                                                    this.request_queue.peek_head().service)
887                                                + "\n\n" + _("Select Yes to select an ID for this service, or No to cancel"),
888                                                "close_moonshot_window");
889             if (result) {
890                 // Prevent other handlers from handling this event; this keeps the window open.
891                 return true; 
892             }
893         }
894
895         // Allow the window deletion to proceed.
896         return false;
897     }
898
899     private static Widget make_rigid(Button button) 
900     {
901         // Hack to prevent the button from growing vertically
902         VBox fixed_height = new VBox(false, 0);
903         fixed_height.pack_start(button, false, false, 0);
904
905         return fixed_height;
906     }
907
908     private void import_identities_cb() {
909         var dialog = new FileChooserDialog(_("Import File"),
910                                            this,
911                                            FileChooserAction.OPEN,
912                                            _("Cancel"),ResponseType.CANCEL,
913                                            _("Open"), ResponseType.ACCEPT,
914                                            null);
915
916         if (import_directory != null) {
917             dialog.set_current_folder(import_directory);
918         }
919
920         if (dialog.run() == ResponseType.ACCEPT)
921         {
922             // Save the parent directory to use as default for next save
923             string filename = dialog.get_filename();
924             var file  = File.new_for_path(filename);
925             import_directory = file.get_parent().get_path();
926
927             int import_count = 0;
928
929             var webp = new Parser(filename);
930             dialog.destroy();
931             webp.parse();
932             logger.trace(@"import_identities_cb: Have $(webp.cards.length) IdCards");
933             foreach (IdCard card in webp.cards)
934             {
935
936                 if (card == null) {
937                     logger.trace(@"import_identities_cb: Skipping null IdCard");
938                     continue;
939                 }
940
941                 if (!card.trust_anchor.is_empty()) {
942                     string ta_datetime_added = TrustAnchor.format_datetime_now();
943                     card.trust_anchor.set_datetime_added(ta_datetime_added);
944                     logger.trace("import_identities_cb : Set ta_datetime_added for '%s' to '%s'; ca_cert='%s'; server_cert='%s'"
945                                  .printf(card.display_name, ta_datetime_added, card.trust_anchor.ca_cert, card.trust_anchor.server_cert));
946                 }
947
948
949                 bool result = add_identity(card, use_flat_file_store);
950                 if (result) {
951                     logger.trace(@"import_identities_cb: Added or updated '$(card.display_name)'");
952                     import_count++;
953                 }
954                 else {
955                     logger.trace(@"import_identities_cb: Did not add or update '$(card.display_name)'");
956                 }
957             }
958             if (import_count == 0) {
959                 var msg_dialog = new Gtk.MessageDialog(this,
960                                                        Gtk.DialogFlags.DESTROY_WITH_PARENT,
961                                                        Gtk.MessageType.INFO,
962                                                        Gtk.ButtonsType.OK,
963                                                        "%s",
964                                                        _("Import completed. No identities were added or updated."));
965                 msg_dialog.run();
966                 msg_dialog.destroy();
967             }
968         }
969         dialog.destroy();
970     }
971
972 }