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