4 class IdentityManagerView : Window {
5 private const int WINDOW_WIDTH = 400;
6 private const int WINDOW_HEIGHT = 500;
7 protected IdentityManagerApp parent_app;
9 public OSXApplication osxApp;
11 private UIManager ui_manager = new UIManager();
12 private Entry search_entry;
13 private VBox vbox_right;
14 private CustomVBox custom_vbox;
15 private VBox services_internal_vbox;
17 private Entry username_entry;
18 private Entry password_entry;
20 private ListStore* listmodel;
21 private TreeModelFilter filter;
23 public IdentityManagerModel identities_manager;
24 private SList<IdCard> candidates;
26 private IdCard default_id_card;
27 public GLib.Queue<IdentityRequest> request_queue;
29 private HashTable<Gtk.Button, string> service_button_map;
41 private const string layout =
42 "<menubar name='MenuBar'>" +
43 " <menu name='FileMenu' action='FileMenuAction'>" +
44 " <menuitem name='AddIdCard' action='AddIdCardAction' />" +
46 " <menuitem name='Quit' action='QuitAction' />" +
49 " <menu name='HelpMenu' action='HelpMenuAction'>" +
50 " <menuitem name='About' action='AboutAction' />" +
54 public IdentityManagerView(IdentityManagerApp app) {
57 osxApp = OSXApplication.get_instance();
59 identities_manager = parent_app.model;
60 request_queue = new GLib.Queue<IdentityRequest>();
61 service_button_map = new HashTable<Gtk.Button, string> (direct_hash, direct_equal);
62 this.title = "Moonshoot";
63 this.set_position (WindowPosition.CENTER);
64 set_default_size (WINDOW_WIDTH, WINDOW_HEIGHT);
71 public void on_card_list_changed () {
75 public void add_candidate (IdCard idcard)
77 candidates.append (idcard);
80 private bool visible_func (TreeModel model, TreeIter iter)
85 Columns.IDCARD_COL, out id_card);
90 if (candidates != null)
92 bool is_candidate = false;
93 foreach (IdCard candidate in candidates)
95 if (candidate == id_card)
102 string entry_text = search_entry.get_text ();
103 if (entry_text == null || entry_text == "")
108 foreach (string search_text in entry_text.split(" "))
110 if (search_text == "")
114 string search_text_casefold = search_text.casefold ();
116 if (id_card.issuer != null)
118 string issuer_casefold = id_card.issuer;
120 if (issuer_casefold.contains (search_text_casefold))
124 if (id_card.display_name != null)
126 string display_name_casefold = id_card.display_name.casefold ();
128 if (display_name_casefold.contains (search_text_casefold))
132 if (id_card.services.length > 0)
134 foreach (string service in id_card.services)
136 string service_casefold = service.casefold ();
138 if (service_casefold.contains (search_text_casefold))
146 private void setup_list_model ()
148 this.listmodel = new ListStore (Columns.N_COLUMNS, typeof (IdCard),
153 this.filter = new TreeModelFilter (listmodel, null);
155 filter.set_visible_func (visible_func);
158 private void search_entry_icon_press_cb (EntryIconPosition pos, Gdk.Event event)
160 if (pos == EntryIconPosition.PRIMARY)
162 print ("Search entry icon pressed\n");
166 this.search_entry.set_text ("");
170 private void search_entry_text_changed_cb ()
172 this.filter.refilter ();
173 redraw_id_card_widgets ();
175 var has_text = this.search_entry.get_text_length () > 0;
176 this.search_entry.set_icon_sensitive (EntryIconPosition.PRIMARY, has_text);
177 this.search_entry.set_icon_sensitive (EntryIconPosition.SECONDARY, has_text);
179 this.vbox_right.set_visible (false);
182 private bool search_entry_key_press_event_cb (Gdk.EventKey e)
184 if(Gdk.keyval_name(e.keyval) == "Escape")
185 this.search_entry.set_text("");
187 // Continue processing this event, since the
188 // text entry functionality needs to see it too.
192 private void load_id_cards () {
193 var children = this.custom_vbox.get_children ();
194 foreach (var id_card_widget in children) {
195 remove_id_card_widget((IdCardWidget)id_card_widget);
198 this.default_id_card = null;
199 LinkedList<IdCard> card_list = identities_manager.get_card_list() ;
200 if (card_list == null) {
204 foreach (IdCard id_card in card_list) {
205 add_id_card_data (id_card);
206 add_id_card_widget (id_card);
209 if (card_list.size > 0){
210 this.default_id_card = card_list.first();
214 private void fill_details (IdCardWidget id_card_widget)
216 var id_card = id_card_widget.id_card;
217 this.username_entry.set_text (id_card.username);
218 this.password_entry.set_text (id_card.password ?? "");
220 var children = this.services_internal_vbox.get_children ();
221 foreach (var hbox in children)
223 fill_services_vbox (id_card_widget.id_card);
224 // identities_manager.store_id_cards();
227 private void show_details (IdCard id_card)
229 this.vbox_right.set_visible (!vbox_right.get_visible ());
231 if (this.vbox_right.get_visible () == false)
233 this.resize (WINDOW_WIDTH, WINDOW_HEIGHT);
237 private void details_identity_cb (IdCardWidget id_card_widget)
239 fill_details (id_card_widget);
240 show_details (id_card_widget.id_card);
243 private IdCard get_id_card_data (AddIdentityDialog dialog)
245 var id_card = new IdCard ();
247 id_card.display_name = dialog.display_name;
248 id_card.issuer = dialog.issuer;
249 if (id_card.issuer == "")
250 id_card.issuer = "Issuer";
251 id_card.username = dialog.username;
252 id_card.password = dialog.password;
253 id_card.services = {};
254 id_card.set_data("pixbuf", find_icon ("avatar-default", 48));
259 private void add_id_card_data (IdCard id_card)
263 this.listmodel->append (out iter);
264 pixbuf = id_card.get_data("pixbuf");
265 listmodel->set (iter,
266 Columns.IDCARD_COL, id_card,
267 Columns.LOGO_COL, pixbuf,
268 Columns.ISSUER_COL, id_card.issuer,
269 Columns.USERNAME_COL, id_card.username,
270 Columns.PASSWORD_COL, id_card.password);
273 private void remove_id_card_data (IdCard id_card)
278 if (listmodel->get_iter_first (out iter))
282 listmodel->get (iter,
283 Columns.ISSUER_COL, out issuer);
285 if (id_card.issuer == issuer)
287 listmodel->remove (iter);
291 while (listmodel->iter_next (ref iter));
295 private void add_id_card_widget (IdCard id_card)
297 var id_card_widget = new IdCardWidget (id_card);
298 this.custom_vbox.add_id_card_widget (id_card_widget);
299 id_card_widget.details_id.connect (details_identity_cb);
300 id_card_widget.remove_id.connect (remove_identity_cb);
301 id_card_widget.send_id.connect ((w) => send_identity_cb (w.id_card));
302 id_card_widget.expanded.connect (this.custom_vbox.receive_expanded_event);
303 id_card_widget.expanded.connect (fill_details);
306 /* This method finds a valid display name */
307 public bool display_name_is_valid (string name,
308 out string? candidate)
310 foreach (IdCard id_card in identities_manager.get_card_list())
312 if (id_card.display_name == name)
314 if (&candidate != null)
316 for (int i=0; i<1000; i++)
318 string tmp = "%s %d".printf (name, i);
319 if (display_name_is_valid (tmp, null))
333 public void insert_id_card (IdCard id_card)
337 if (!display_name_is_valid (id_card.display_name, out candidate))
339 id_card.display_name = candidate;
342 this.identities_manager.add_card(id_card);
345 public bool add_identity (IdCard id_card)
349 * TODO: We should have a confirmation dialog, but currently it will crash on Mac OS
350 * so for now we will install silently
352 var ret = Gtk.ResponseType.YES;
355 var dialog = new Gtk.MessageDialog (this,
356 Gtk.DialogFlags.DESTROY_WITH_PARENT,
357 Gtk.MessageType.QUESTION,
358 Gtk.ButtonsType.YES_NO,
359 _("Would you like to add '%s' ID Card to the ID Card Organizer?"),
360 id_card.display_name);
362 var ret = dialog.run ();
366 if (ret == Gtk.ResponseType.YES) {
367 id_card.set_data ("pixbuf", find_icon ("avatar-default", 48));
368 this.insert_id_card (id_card);
375 private void add_identity_manual_cb ()
377 var dialog = new AddIdentityDialog ();
378 var result = dialog.run ();
381 case ResponseType.OK:
382 insert_id_card (get_id_card_data (dialog));
390 private void remove_id_card_widget (IdCardWidget id_card_widget) {
391 this.custom_vbox.remove_id_card_widget (id_card_widget);
394 private void remove_identity (IdCardWidget id_card_widget)
396 var id_card = id_card_widget.id_card;
397 remove_id_card_widget (id_card_widget);
399 this.identities_manager.remove_card(id_card);
402 private void redraw_id_card_widgets ()
407 var children = this.custom_vbox.get_children ();
408 foreach (var id_card_widget in children)
409 id_card_widget.destroy();
411 if (filter.get_iter_first (out iter))
416 Columns.IDCARD_COL, out id_card);
418 add_id_card_widget (id_card);
420 while (filter.iter_next (ref iter));
424 private void remove_identity_cb (IdCardWidget id_card_widget)
426 var id_card = id_card_widget.id_card;
428 var dialog = new MessageDialog (this,
429 DialogFlags.DESTROY_WITH_PARENT,
430 MessageType.QUESTION,
431 Gtk.ButtonsType.YES_NO,
432 _("Are you sure you want to delete %s ID Card?"), id_card.issuer);
433 var result = dialog.run ();
435 case ResponseType.YES:
436 remove_identity (id_card_widget);
444 public void select_identity (IdentityRequest request)
446 IdCard identity = null;
448 this.request_queue.push_tail (request);
450 if (custom_vbox.current_idcard != null &&
451 custom_vbox.current_idcard.send_button != null)
452 custom_vbox.current_idcard.send_button.set_sensitive (true);
454 if (request.select_default)
456 identity = default_id_card;
459 if (identity == null)
461 bool has_nai = request.nai != null && request.nai != "";
462 bool has_srv = request.service != null && request.service != "";
463 bool confirm = false;
464 IdCard nai_provided = null;
466 foreach (IdCard id in identities_manager.get_card_list())
468 /* If NAI matches we add id card to the candidate list */
469 if (has_nai && request.nai == id.nai)
476 /* If any service matches we add id card to the candidate list */
479 foreach (string srv in id.services)
481 if (request.service == srv)
490 /* If more than one candidate we dissasociate service from all ids */
491 if (has_srv && candidates.length() > 1)
493 foreach (IdCard id in candidates)
496 SList<string> services_list = null;
497 bool has_service = false;
499 foreach (string srv in id.services)
501 if (srv == request.service)
506 services_list.append (srv);
512 if (services_list.length () == 0)
518 string[] services = new string[services_list.length ()];
519 foreach (string srv in services_list)
525 id.services = services;
529 // identities_manager.store_id_cards ();
531 /* If there are no candidates we use the service matching rules */
532 if (candidates.length () == 0)
534 foreach (IdCard id in identities_manager.get_card_list())
536 foreach (Rule rule in id.rules)
538 if (!match_service_pattern (request.service, rule.pattern))
541 candidates.append (id);
543 if (rule.always_confirm == "true")
549 if (candidates.length () > 1)
551 if (has_nai && nai_provided != null)
553 identity = nai_provided;
560 identity = candidates.nth_data (0);
562 /* TODO: If candidate list empty return fail */
567 redraw_id_card_widgets ();
572 // Send back the identity (we can't directly run the
573 // callback because we may be being called from a 'yield')
574 Idle.add (() => { send_identity_cb (identity); return false; });
578 private bool match_service_pattern (string service, string pattern)
580 var pspec = new PatternSpec (pattern);
581 return pspec.match_string (service);
584 public void send_identity_cb (IdCard identity)
586 return_if_fail (request_queue.length > 0);
588 var request = this.request_queue.pop_head ();
589 bool reset_password = false;
591 if (request.service != null && request.service != "")
593 bool duplicate_service = false;
595 foreach (string service in identity.services)
597 if (service == request.service)
598 duplicate_service = true;
600 if (duplicate_service == false)
602 string[] services = new string[identity.services.length + 1];
604 for (int i = 0; i < identity.services.length; i++)
605 services[i] = identity.services[i];
607 services[identity.services.length] = request.service;
608 identity.services = services;
610 identities_manager.update_card (identity);
614 if (identity.password == null)
616 var dialog = new AddPasswordDialog ();
617 var result = dialog.run ();
620 case ResponseType.OK:
621 identity.password = dialog.password;
622 reset_password = ! dialog.remember;
632 if (this.request_queue.is_empty())
635 if (identity != null)
636 this.default_id_card = identity;
638 request.return_identity (identity);
641 identity.password = null;
646 private void label_make_bold (Label label)
648 var font_desc = new Pango.FontDescription ();
650 font_desc.set_weight (Pango.Weight.BOLD);
652 /* This will only affect the weight of the font, the rest is
653 * from the current state of the widget, which comes from the
654 * theme or user prefs, since the font desc only has the
655 * weight flag turned on.
657 label.modify_font (font_desc);
660 private void fill_services_vbox (IdCard id_card)
663 var n_columns = id_card.services.length;
665 var services_table = new Table (n_columns, 2, false);
666 services_table.set_col_spacings (10);
667 services_table.set_row_spacings (10);
668 this.services_internal_vbox.add (services_table);
670 service_button_map.remove_all ();
672 foreach (string service in id_card.services)
674 var label = new Label (service);
675 label.set_alignment (0, (float) 0.5);
677 var remove_button = new Button.from_stock (Stock.REMOVE);
679 var remove_button = new Button.from_stock (STOCK_REMOVE);
683 service_button_map.insert (remove_button, service);
685 remove_button.clicked.connect ((remove_button) =>
687 var dialog = new Gtk.MessageDialog (this,
688 Gtk.DialogFlags.DESTROY_WITH_PARENT,
689 Gtk.MessageType.QUESTION,
690 Gtk.ButtonsType.YES_NO,
691 _("Are you sure you want to stop '%s' ID Card from being used with %s?"),
692 custom_vbox.current_idcard.id_card.display_name,
694 var ret = dialog.run();
697 if (ret == Gtk.ResponseType.YES)
699 IdCard idcard = custom_vbox.current_idcard.id_card;
700 var candidate = service_button_map.lookup (remove_button);
702 SList<string> services = new SList<string>();
704 foreach (string srv in idcard.services)
706 if (srv == candidate)
708 services.append (srv);
711 idcard.services = new string[services.length()];
712 for (int j=0; j<idcard.services.length; j++)
714 idcard.services[j] = services.nth_data(j);
717 var children = services_internal_vbox.get_children ();
718 foreach (var hbox in children)
721 fill_services_vbox (idcard);
722 custom_vbox.current_idcard.update_id_card_label ();
726 services_table.attach_defaults (label, 0, 1, i, i+1);
727 services_table.attach_defaults (remove_button, 1, 2, i, i+1);
730 this.services_internal_vbox.show_all ();
733 private void on_about_action ()
736 "Javier Jardón <jjardon@codethink.co.uk>",
737 "Sam Thursfield <samthursfield@codethink.co.uk>",
738 "Alberto Ruiz <alberto.ruiz@codethink.co.uk>",
742 string copyright = "Copyright 2011 JANET";
746 Copyright (c) 2011, JANET(UK)
749 Redistribution and use in source and binary forms, with or without
750 modification, are permitted provided that the following conditions
753 1. Redistributions of source code must retain the above copyright
754 notice, this list of conditions and the following disclaimer.
756 2. Redistributions in binary form must reproduce the above copyright
757 notice, this list of conditions and the following disclaimer in the
758 documentation and/or other materials provided with the distribution.
760 3. Neither the name of JANET(UK) nor the names of its contributors
761 may be used to endorse or promote products derived from this software
762 without specific prior written permission.
764 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"
765 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
766 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
767 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
768 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
769 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
770 OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
771 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
772 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
773 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
777 Gtk.show_about_dialog (this,
778 "comments", _("Moonshot project UI"),
779 "copyright", copyright,
780 "website", Config.PACKAGE_URL,
781 "version", Config.PACKAGE_VERSION,
783 "website-label", _("Visit the Moonshot project web site"),
785 "translator-credits", _("translator-credits"),
790 private Gtk.ActionEntry[] create_actions() {
791 Gtk.ActionEntry[] actions = new Gtk.ActionEntry[0];
793 Gtk.ActionEntry filemenu = { "FileMenuAction",
798 Gtk.ActionEntry add = { "AddIdCardAction",
806 N_("Add a new ID Card"),
807 add_identity_manual_cb };
809 Gtk.ActionEntry quit = { "QuitAction",
817 N_("Quit the application"),
821 Gtk.ActionEntry helpmenu = { "HelpMenuAction",
826 Gtk.ActionEntry about = { "AboutAction",
834 N_("About this application"),
842 private void create_ui_manager ()
844 Gtk.ActionGroup action_group = new Gtk.ActionGroup ("GeneralActionGroup");
845 action_group.add_actions (create_actions (), this);
846 ui_manager.insert_action_group (action_group, 0);
849 ui_manager.add_ui_from_string (layout, -1);
853 stderr.printf ("%s\n", e.message);
855 ui_manager.ensure_update ();
858 private void build_ui()
860 create_ui_manager ();
862 this.search_entry = new Entry();
864 set_atk_name_description (search_entry, _("Search entry"), _("Search for a specific ID Card"));
865 this.search_entry.set_icon_from_pixbuf (EntryIconPosition.PRIMARY,
866 find_icon_sized ("edit-find", Gtk.IconSize.MENU));
867 // find_icon_sized ("edit-find-symbolic", Gtk.IconSize.MENU));
868 this.search_entry.set_icon_tooltip_text (EntryIconPosition.PRIMARY,
869 _("Search identity or service"));
870 this.search_entry.set_icon_sensitive (EntryIconPosition.PRIMARY, false);
872 this.search_entry.set_icon_from_pixbuf (EntryIconPosition.SECONDARY,
873 find_icon_sized ("process-stop", Gtk.IconSize.MENU));
874 // find_icon_sized ("edit-clear-symbolic", Gtk.IconSize.MENU));
875 this.search_entry.set_icon_tooltip_text (EntryIconPosition.SECONDARY,
876 _("Clear the current search"));
877 this.search_entry.set_icon_sensitive (EntryIconPosition.SECONDARY, false);
880 this.search_entry.icon_press.connect (search_entry_icon_press_cb);
881 this.search_entry.notify["text"].connect (search_entry_text_changed_cb);
882 this.search_entry.key_press_event.connect(search_entry_key_press_event_cb);
884 this.custom_vbox = new CustomVBox (this, false, 6);
886 var viewport = new Viewport (null, null);
887 viewport.set_border_width (6);
888 viewport.set_shadow_type (ShadowType.NONE);
889 viewport.add (custom_vbox);
890 var scroll = new ScrolledWindow (null, null);
891 scroll.set_policy (PolicyType.NEVER, PolicyType.AUTOMATIC);
892 scroll.set_shadow_type (ShadowType.IN);
893 scroll.add_with_viewport (viewport);
895 var vbox_left = new VBox (false, 0);
896 vbox_left.pack_start (search_entry, false, false, 6);
897 vbox_left.pack_start (scroll, true, true, 0);
898 vbox_left.set_size_request (WINDOW_WIDTH, 0);
900 var login_vbox_title = new Label (_("Login: "));
901 label_make_bold (login_vbox_title);
902 login_vbox_title.set_alignment (0, (float) 0.5);
903 var username_label = new Label (_("Username:"));
904 username_label.set_alignment (1, (float) 0.5);
905 this.username_entry = new Entry ();
906 var password_label = new Label (_("Password:"));
907 password_label.set_alignment (1, (float) 0.5);
908 this.password_entry = new Entry ();
909 password_entry.set_invisible_char ('*');
910 password_entry.set_visibility (false);
911 var remember_checkbutton = new CheckButton.with_label (_("Remember password"));
912 var login_table = new Table (3, 3, false);
913 login_table.set_col_spacings (10);
914 login_table.set_row_spacings (10);
915 login_table.attach_defaults (username_label, 0, 1, 0, 1);
916 login_table.attach_defaults (username_entry, 1, 2, 0, 1);
917 login_table.attach_defaults (password_label, 0, 1, 1, 2);
918 login_table.attach_defaults (password_entry, 1, 2, 1, 2);
919 login_table.attach_defaults (remember_checkbutton, 1, 2, 2, 3);
920 var login_vbox_alignment = new Alignment (0, 0, 0, 0);
921 login_vbox_alignment.set_padding (0, 0, 12, 0);
922 login_vbox_alignment.add (login_table);
923 var login_vbox = new VBox (false, 6);
924 login_vbox.pack_start (login_vbox_title, false, true, 0);
925 login_vbox.pack_start (login_vbox_alignment, false, true, 0);
927 var services_vbox_title = new Label (_("Services:"));
928 label_make_bold (services_vbox_title);
929 services_vbox_title.set_alignment (0, (float) 0.5);
930 var services_vbox_alignment = new Alignment (0, 0, 0, 0);
931 services_vbox_alignment.set_padding (0, 0, 12, 0);
932 this.services_internal_vbox = new VBox (true, 6);
933 services_vbox_alignment.add (services_internal_vbox);
934 var services_vbox = new VBox (false, 6);
935 services_vbox.pack_start (services_vbox_title, false, true, 0);
936 services_vbox.pack_start (services_vbox_alignment, false, true, 0);
938 this.vbox_right = new VBox (false, 18);
939 vbox_right.pack_start (login_vbox, false, true, 0);
940 vbox_right.pack_start (services_vbox, false, true, 0);
942 var hbox = new HBox (false, 12);
943 hbox.pack_start (vbox_left, true, true, 0);
944 hbox.pack_start (vbox_right, false, false, 0);
946 var main_vbox = new VBox (false, 0);
947 main_vbox.set_border_width (12);
950 // hide the File | Quit menu item which is now on the Mac Menu
951 Gtk.Widget quit_item = this.ui_manager.get_widget("/MenuBar/FileMenu/Quit");
954 Gtk.MenuShell menushell = this.ui_manager.get_widget("/MenuBar") as Gtk.MenuShell;
955 osxApp.set_menu_bar(menushell);
956 osxApp.set_use_quartz_accelerators(true);
957 osxApp.sync_menu_bar();
960 var menubar = this.ui_manager.get_widget ("/MenuBar");
961 main_vbox.pack_start (menubar, false, false, 0);
963 main_vbox.pack_start (hbox, true, true, 0);
965 main_vbox.show_all();
966 this.vbox_right.hide ();
969 private void set_atk_name_description (Widget widget, string name, string description)
971 var atk_widget = widget.get_accessible ();
973 atk_widget.set_name (name);
974 atk_widget.set_description (description);
977 private void connect_signals()
979 this.destroy.connect (Gtk.main_quit);
980 this.identities_manager.card_list_changed.connect(this.on_card_list_changed);