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