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