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