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