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