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