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