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