Updated About dialog; also bumped version number to 1.0.0
[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     public 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         return id_card;
255     }
256
257     private void add_id_card_data(IdCard id_card)
258     {
259         TreeIter   iter;
260         Gdk.Pixbuf pixbuf;
261         this.listmodel->append(out iter);
262         pixbuf = get_pixbuf(id_card);
263         listmodel->set(iter,
264                        Columns.IDCARD_COL, id_card,
265                        Columns.LOGO_COL, pixbuf,
266                        Columns.ISSUER_COL, id_card.issuer,
267                        Columns.USERNAME_COL, id_card.username,
268                        Columns.PASSWORD_COL, id_card.password);
269     }
270
271     private void remove_id_card_data(IdCard id_card)
272     {
273         TreeIter iter;
274         string issuer;
275
276         if (listmodel->get_iter_first(out iter))
277         {
278             do
279             {
280                 listmodel->get(iter,
281                                Columns.ISSUER_COL, out issuer);
282
283                 if (id_card.issuer == issuer)
284                 {
285                     listmodel->remove(iter);
286                     break;
287                 }
288             }
289             while (listmodel->iter_next(ref iter));
290         }
291     }
292
293     private IdCardWidget add_id_card_widget(IdCard id_card)
294     {
295         var id_card_widget = new IdCardWidget(id_card);
296         this.custom_vbox.add_id_card_widget(id_card_widget);
297         id_card_widget.expanded.connect(this.widget_selected_cb);
298         id_card_widget.collapsed.connect(this.widget_unselected_cb);
299         return id_card_widget;
300     }
301
302     private void widget_selected_cb(IdCardWidget id_card_widget)
303     {
304         this.remove_button.set_sensitive(true);
305         this.edit_button.set_sensitive(true);
306         this.custom_vbox.receive_expanded_event(id_card_widget);
307
308         if (this.request_queue.length > 0)
309              this.send_button.set_sensitive(true);
310     }
311
312     private void widget_unselected_cb(IdCardWidget id_card_widget)
313     {
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 ? "non-null" : "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(IdCardWidget id_card_widget)
411     {
412         var id_card = id_card_widget.id_card;
413         this.custom_vbox.remove_id_card_widget(id_card_widget);
414
415         this.identities_manager.remove_card(id_card);
416
417         // Nothing is selected, so disable buttons
418         this.edit_button.set_sensitive(false);
419         this.remove_button.set_sensitive(false);
420         this.send_button.set_sensitive(false);
421     }
422
423     private void redraw_id_card_widgets()
424     {
425         logger.trace("redraw_id_card_widgets");
426
427         TreeIter iter;
428         IdCard id_card;
429
430         this.custom_vbox.clear();
431
432         if (filter.get_iter_first(out iter))
433         {
434             do
435             {
436                 filter.get(iter,
437                            Columns.IDCARD_COL, out id_card);
438
439                 add_id_card_widget(id_card);
440             }
441             while (filter.iter_next(ref iter));
442         }
443     }
444
445     private void remove_identity_cb(IdCardWidget id_card_widget)
446     {
447         var id_card = id_card_widget.id_card;
448
449         bool remove = WarningDialog.confirm(this, 
450                                             Markup.printf_escaped(
451                                                 "<span font-weight='heavy'>You are about to remove the identity '%s'.</span>",
452                                                 id_card.display_name)
453                                             + "\n\nAre you sure you want to do this?",
454                                             "delete_idcard");
455         if (remove) 
456             remove_identity(id_card_widget);
457     }
458
459     private void set_prompting_service(string service)
460     {
461         clear_selection_prompts();
462
463         var prompting_service = new Label(_("Identity requested for service:\n%s").printf(service));
464         prompting_service.set_line_wrap(true);
465
466         // left-align
467         prompting_service.set_alignment(0, (float )0.5);
468
469         var selection_prompt = new Label(_("Select your identity:"));
470         selection_prompt.set_alignment(0, 1);
471
472         this.service_prompt_vbox.pack_start(prompting_service, false, false, 12);
473         this.service_prompt_vbox.pack_start(selection_prompt, false, false, 2);
474         this.service_prompt_vbox.show_all();
475     }
476
477     private void clear_selection_prompts()
478     {
479         var list = service_prompt_vbox.get_children();
480         foreach (Widget w in list)
481         {
482             service_prompt_vbox.remove(w);
483         }
484     }
485
486
487     public void queue_identity_request(IdentityRequest request)
488     {
489         if (this.request_queue.is_empty())
490         { /* setup widgets */
491             candidates = request.candidates;
492             filter.refilter();
493             redraw_id_card_widgets();
494             set_prompting_service(request.service);
495             remember_identity_binding.show();
496             make_visible();
497         }
498         this.request_queue.push_tail(request);
499     }
500
501
502     /** Makes the window visible, or at least, notifies the user that the window
503      * wants to be visible.
504      *
505      * This differs from show() in that show() does not guarantee that the 
506      * window will be moved to the foreground. Actually, neither does this
507      * method, because the user's settings and window manager may affect the
508      * behavior significantly.
509      */
510     public void make_visible()
511     {
512         set_urgency_hint(true);
513         present();
514     }
515
516     public IdCard check_add_password(IdCard identity, IdentityRequest request, IdentityManagerModel model)
517     {
518         logger.trace(@"check_add_password");
519         IdCard retval = identity;
520         bool idcard_has_pw = (identity.password != null) && (identity.password != "");
521         bool request_has_pw = (request.password != null) && (request.password != "");
522         if ((!idcard_has_pw) && (!identity.is_no_identity())) {
523             if (request_has_pw) {
524                 identity.password = request.password;
525                 retval = model.update_card(identity);
526             } else {
527                 var dialog = new AddPasswordDialog(identity, request);
528                 var result = dialog.run();
529
530                 switch (result) {
531                 case ResponseType.OK:
532                     identity.password = dialog.password;
533                     identity.store_password = dialog.remember;
534                     if (dialog.remember)
535                         identity.temporary = false;
536                     retval = model.update_card(identity);
537                     break;
538                 default:
539                     identity = null;
540                     break;
541                 }
542                 dialog.destroy();
543             }
544         }
545         return retval;
546     }
547
548     private void send_identity_cb(IdCard id)
549     {
550         return_if_fail(request_queue.length > 0);
551
552         if (!check_and_confirm_trust_anchor(id)) {
553             // Allow user to pick again
554             return;
555         }
556
557         var request = this.request_queue.pop_head();
558         var identity = check_add_password(id, request, identities_manager);
559         send_button.set_sensitive(false);
560
561         candidates = null;
562       
563         if (this.request_queue.is_empty())
564         {
565             candidates = null;
566             clear_selection_prompts();
567             if (!parent_app.explicitly_launched) {
568 // The following occasionally causes the app to exit without sending the dbus
569 // reply, so for now we just don't exit
570 //                Gtk.main_quit();
571 // just hide instead
572                 this.hide();
573             }
574         } else {
575             IdentityRequest next = this.request_queue.peek_head();
576             candidates = next.candidates;
577             set_prompting_service(next.service);
578         }
579         filter.refilter();
580         redraw_id_card_widgets();
581
582         if ((identity != null) && (!identity.is_no_identity()))
583             parent_app.default_id_card = identity;
584
585         request.return_identity(identity, remember_identity_binding.active);
586
587         remember_identity_binding.active = false;
588         remember_identity_binding.hide();
589     }
590
591     private bool check_and_confirm_trust_anchor(IdCard id)
592     {
593         if (!id.trust_anchor.is_empty() && id.trust_anchor.get_anchor_type() == TrustAnchor.TYPE_ENTERPRISE) {
594             if (get_string_setting("TrustAnchors", id.nai) != id.trust_anchor.server_cert) {
595
596                 bool ret = false;
597                 int result = ResponseType.CANCEL;
598                 var dialog = new TrustAnchorDialog(id, this);
599                 while (!dialog.complete)
600                     result = dialog.run();
601
602                 switch (result) {
603                 case ResponseType.OK:
604                     set_string_setting("TrustAnchors", id.nai, id.trust_anchor.server_cert);
605                     ret = true;
606                     break;
607                 default:
608                     break;
609                 }
610
611                 dialog.destroy();
612                 return ret;
613             }
614         }
615         return true;
616     }
617
618
619     // private void label_make_bold(Label label)
620     // {
621     //     var font_desc = new Pango.FontDescription();
622
623     //     font_desc.set_weight(Pango.Weight.BOLD);
624
625     //     /* This will only affect the weight of the font, the rest is
626     //      * from the current state of the widget, which comes from the
627     //      * theme or user prefs, since the font desc only has the
628     //      * weight flag turned on.
629     //      */
630     //     label.modify_font(font_desc);
631     // }
632
633     private void on_about_action()
634     {
635         string copyright = "Copyright (c) 2011, %d JANET".printf(LATEST_EDIT_YEAR);
636
637         string license =
638         """
639 Copyright (c) 2011, %d JANET(UK)
640 All rights reserved.
641
642 Redistribution and use in source and binary forms, with or without
643 modification, are permitted provided that the following conditions
644 are met:
645
646 1. Redistributions of source code must retain the above copyright
647    notice, this list of conditions and the following disclaimer.
648
649 2. Redistributions in binary form must reproduce the above copyright
650    notice, this list of conditions and the following disclaimer in the
651    documentation and/or other materials provided with the distribution.
652
653 3. Neither the name of JANET(UK) nor the names of its contributors
654    may be used to endorse or promote products derived from this software
655    without specific prior written permission.
656
657 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"
658 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
659 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
660 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
661 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
662 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
663 OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
664 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
665 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
666 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
667 SUCH DAMAGE.
668 """.printf(LATEST_EDIT_YEAR);
669
670         AboutDialog about = new AboutDialog();
671
672         about.set_comments(_("Moonshot project UI"));
673         about.set_copyright(copyright);
674         about.set_website(Config.PACKAGE_URL);
675         about.set_website_label(_("Visit the Moonshot project web site"));
676
677         // Note: The package version is configured at the top of moonshot/ui/configure.ac
678         about.set_version(Config.PACKAGE_VERSION);
679         about.set_license(license);
680         about.set_modal(true);
681         about.set_transient_for(this);
682         about.response.connect((a, b) => {about.destroy();});
683         about.modify_bg(StateType.NORMAL, white);
684         
685         about.run();
686     }
687
688     private Gtk.ActionEntry[] create_actions() {
689         Gtk.ActionEntry[] actions = new Gtk.ActionEntry[0];
690
691         Gtk.ActionEntry helpmenu = { "HelpMenuAction",
692                                      null,
693                                      N_("_Help"),
694                                      null, null, null };
695         actions += helpmenu;
696         Gtk.ActionEntry about = { "AboutAction",
697                                   #if VALA_0_12
698                                   Stock.ABOUT,
699                                   #else
700                                   STOCK_ABOUT,
701                                   #endif
702                                   N_("About"),
703                                   null,
704                                   N_("About this application"),
705                                   on_about_action };
706         actions += about;
707
708         return actions;
709     }
710
711
712     private void create_ui_manager()
713     {
714         Gtk.ActionGroup action_group = new Gtk.ActionGroup("GeneralActionGroup");
715         action_group.add_actions(create_actions(), this);
716         ui_manager.insert_action_group(action_group, 0);
717         try
718         {
719             ui_manager.add_ui_from_string(menu_layout, -1);
720         }
721         catch (Error e)
722         {
723             stderr.printf("%s\n", e.message);
724             logger.error("create_ui_manager: Caught error: " + e.message);
725         }
726         ui_manager.ensure_update();
727     }
728
729     private void build_ui()
730     {
731         // Note: On Debian7/Gtk+2, the menu bar remains gray. This doesn't happen on Debian8/Gtk+3.
732         this.modify_bg(StateType.NORMAL, white);
733
734         create_ui_manager();
735
736         this.search_entry = new Entry();
737
738         set_atk_name_description(search_entry, _("Search entry"), _("Search for a specific ID Card"));
739         this.search_entry.set_icon_from_pixbuf(EntryIconPosition.PRIMARY,
740                                                find_icon_sized("edit-find", Gtk.IconSize.MENU));
741         this.search_entry.set_icon_tooltip_text(EntryIconPosition.PRIMARY,
742                                                 _("Search for an identity or service"));
743         this.search_entry.set_icon_sensitive(EntryIconPosition.PRIMARY, false);
744
745         this.search_entry.set_icon_from_pixbuf(EntryIconPosition.SECONDARY,
746                                                find_icon_sized("process-stop", Gtk.IconSize.MENU));
747         this.search_entry.set_icon_tooltip_text(EntryIconPosition.SECONDARY,
748                                                 _("Clear the current search"));
749         this.search_entry.set_icon_sensitive(EntryIconPosition.SECONDARY, false);
750
751
752         this.search_entry.icon_press.connect(search_entry_icon_press_cb);
753         this.search_entry.notify["text"].connect(search_entry_text_changed_cb);
754         this.search_entry.key_press_event.connect(search_entry_key_press_event_cb);
755         this.search_entry.set_width_chars(30);
756
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
769         service_prompt_vbox = new VBox(false, 0);
770
771         var vbox_left = new VBox(false, 0);
772         vbox_left.pack_start(service_prompt_vbox, false, false, 12);
773
774         var search_hbox = new HBox(false, 6);
775         search_hbox.pack_end(search_entry, false, false, 0);
776         //// var search_label = new Label(_("Search:"));
777         //// search_label.set_alignment(1, (float) 0.5);
778         //// set_atk_relation(search_label, search_entry, Atk.RelationType.LABEL_FOR);
779         //// search_hbox.pack_end(search_label, false, false, 6);
780
781         var full_search_label = new Label(_("Search for an identity or service"));
782         full_search_label.set_alignment(1, 0);
783         var search_vbox = new VBox(false, 4);
784         search_vbox.pack_start(full_search_label, false, false, 0);
785         search_vbox.pack_start(search_hbox, false, false, 0);
786
787         var inner_left_vbox = new VBox(false, 6);
788         inner_left_vbox.pack_start(search_vbox, false, false, 6);
789 //        inner_left_vbox.pack_start(selection_prompt, false, false, 6);
790         inner_left_vbox.pack_start(id_scrollwin, true, true, 0);
791
792         var id_and_button_box = new HBox(false, 6);
793         id_and_button_box.pack_start(inner_left_vbox, true, true, 6);
794         vbox_left.pack_start(id_and_button_box, true, true, 0);
795         // vbox_left.pack_start(prompting_service, false, false, 6);
796         vbox_left.set_size_request(WINDOW_WIDTH, 0);
797
798         this.no_identity_title = new Label(_("No Identity: Send this identity to services which should not use Moonshot"));
799         no_identity_title.set_alignment(0, (float ) 0.5);
800         no_identity_title.set_line_wrap(true);
801         no_identity_title.show();
802
803         this.vbox_right = new VBox(false, 6);
804
805         var add_button = new Button.with_label(_("Add"));
806         add_button.clicked.connect((w) => {add_identity_cb();});
807
808         this.edit_button = new Button.with_label(_("Edit"));
809         edit_button.clicked.connect((w) => {edit_identity_cb(custom_vbox.current_idcard.id_card);});
810         edit_button.set_sensitive(false);
811
812         this.remove_button = new Button.with_label(_("Remove"));
813         remove_button.clicked.connect((w) => {remove_identity_cb(custom_vbox.current_idcard);});
814         remove_button.set_sensitive(false);
815
816         this.send_button = new Button.with_label(_("Send"));
817         send_button.clicked.connect((w) => {send_identity_cb(custom_vbox.current_idcard.id_card);});
818         // send_button.set_visible(false);
819         send_button.set_sensitive(false);
820
821         var empty_box = new VBox(false, 0);
822         empty_box.set_size_request(0, 0);
823         vbox_right.pack_start(empty_box, false, false, 14);
824         vbox_right.pack_start(add_button, false, false, 6);
825         vbox_right.pack_start(edit_button, false, false, 6);
826         vbox_right.pack_start(remove_button, false, false, 6);
827         vbox_right.pack_start(send_button, false, false, 24);
828
829         id_and_button_box.pack_start(vbox_right, false, false, 0);
830
831         var main_vbox = new VBox(false, 0);
832
833         // Note: This places a border above the menubar. Is that what we want?
834         main_vbox.set_border_width(12);
835
836 #if OS_MACOS
837         // hide the  File | Quit menu item which is now on the Mac Menu
838 //        Gtk.Widget quit_item =  this.ui_manager.get_widget("/MenuBar/FileMenu/Quit");
839 //        quit_item.hide();
840         
841         Gtk.MenuShell menushell = this.ui_manager.get_widget("/MenuBar") as Gtk.MenuShell;
842         menushell.modify_bg(StateType.NORMAL, white);
843
844         osxApp.set_menu_bar(menushell);
845         osxApp.set_use_quartz_accelerators(true);
846         osxApp.sync_menu_bar();
847         osxApp.ready();
848 #else
849         var menubar = this.ui_manager.get_widget("/MenuBar");
850         main_vbox.pack_start(menubar, false, false, 0);
851         menubar.modify_bg(StateType.NORMAL, white);
852 #endif
853         main_vbox.pack_start(vbox_left, true, true, 0);
854
855         remember_identity_binding = new CheckButton.with_label(_("Remember my identity choice for this service"));
856         remember_identity_binding.active = false;
857         main_vbox.pack_start(remember_identity_binding, false, false, 6);
858
859         add(main_vbox);
860         main_vbox.show_all();
861
862         if (this.request_queue.length == 0)
863             remember_identity_binding.hide();
864     } 
865
866     private void set_atk_name_description(Widget widget, string name, string description)
867     {
868         var atk_widget = widget.get_accessible();
869
870         atk_widget.set_name(name);
871         atk_widget.set_description(description);
872     }
873
874     private void connect_signals()
875     {
876         this.destroy.connect(Gtk.main_quit);
877         this.identities_manager.card_list_changed.connect(this.on_card_list_changed);
878     }
879 }