3 class MainWindow : Window
5 private const int WINDOW_WIDTH = 400;
6 private const int WINDOW_HEIGHT = 500;
8 private UIManager ui_manager = new UIManager();
9 private Entry search_entry;
10 private VBox vbox_right;
11 private CustomVBox custom_vbox;
12 private VBox services_internal_vbox;
14 private Entry username_entry;
15 private Entry password_entry;
17 private ListStore listmodel;
18 private TreeModelFilter filter;
20 public IdentitiesManager identities_manager;
21 private SList<IdCard> candidates;
23 private MoonshotServer ipc_server;
25 private IdCard default_id_card;
26 public Queue<IdentityRequest> request_queue;
28 private HashTable<Gtk.Button, string> service_button_map;
40 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' />
57 request_queue = new Queue<IdentityRequest>();
59 service_button_map = new HashTable<Gtk.Button, string> (direct_hash, direct_equal);
61 this.title = "Moonshoot";
62 this.set_position (WindowPosition.CENTER);
63 set_default_size (WINDOW_WIDTH, WINDOW_HEIGHT);
66 setup_identities_list();
72 public void add_candidate (IdCard idcard)
74 candidates.append (idcard);
77 private bool visible_func (TreeModel model, TreeIter iter)
82 Columns.IDCARD_COL, out id_card);
86 foreach (IdCard candidate in candidates)
88 if (&candidate == &id_card)
92 string entry_text = search_entry.get_text ();
93 if (entry_text == null || entry_text == "")
98 foreach (string search_text in entry_text.split(" "))
100 if (search_text == "")
104 string search_text_casefold = search_text.casefold ();
106 if (id_card.issuer != null)
108 string issuer_casefold = id_card.issuer;
110 if (issuer_casefold.contains (search_text_casefold))
114 if (id_card.display_name != null)
116 string display_name_casefold = id_card.display_name.casefold ();
118 if (display_name_casefold.contains (search_text_casefold))
122 if (id_card.services.length > 0)
124 foreach (string service in id_card.services)
126 string service_casefold = service.casefold ();
128 if (service_casefold.contains (search_text_casefold))
136 private void setup_identities_list ()
138 this.listmodel = new ListStore (Columns.N_COLUMNS, typeof (IdCard),
143 this.filter = new TreeModelFilter (listmodel, null);
145 filter.set_visible_func (visible_func);
148 private void search_entry_icon_press_cb (EntryIconPosition pos, Gdk.Event event)
150 if (pos == EntryIconPosition.PRIMARY)
152 print ("Search entry icon pressed\n");
156 this.search_entry.set_text ("");
160 private void search_entry_text_changed_cb ()
162 this.filter.refilter ();
163 redraw_id_card_widgets ();
165 var has_text = this.search_entry.get_text_length () > 0;
166 this.search_entry.set_icon_sensitive (EntryIconPosition.PRIMARY, has_text);
167 this.search_entry.set_icon_sensitive (EntryIconPosition.SECONDARY, has_text);
169 this.vbox_right.set_visible (false);
172 private bool search_entry_key_press_event_cb (Gdk.EventKey e)
174 if(Gdk.keyval_name(e.keyval) == "Escape")
175 this.search_entry.set_text("");
177 // Continue processing this event, since the
178 // text entry functionality needs to see it too.
182 private void load_id_cards ()
184 identities_manager = new IdentitiesManager ();
186 if (identities_manager.id_card_list == null)
189 foreach (IdCard id_card in identities_manager.id_card_list)
191 add_id_card_data (id_card);
192 add_id_card_widget (id_card);
195 this.default_id_card = identities_manager.id_card_list.data;
198 private void fill_details (IdCardWidget id_card_widget)
200 var id_card = id_card_widget.id_card;
201 this.username_entry.set_text (id_card.username);
202 this.password_entry.set_text (id_card.password ?? "");
204 var children = this.services_internal_vbox.get_children ();
205 foreach (var hbox in children)
207 fill_services_vbox (id_card_widget.id_card);
208 identities_manager.store_id_cards();
211 private void show_details (IdCard id_card)
213 this.vbox_right.set_visible (!vbox_right.get_visible ());
215 if (this.vbox_right.get_visible () == false)
217 this.resize (WINDOW_WIDTH, WINDOW_HEIGHT);
221 private void details_identity_cb (IdCardWidget id_card_widget)
223 fill_details (id_card_widget);
224 show_details (id_card_widget.id_card);
227 private IdCard get_id_card_data (AddIdentityDialog dialog)
229 var id_card = new IdCard ();
231 id_card.display_name = dialog.display_name;
232 id_card.issuer = dialog.issuer;
233 if (id_card.issuer == "")
234 id_card.issuer = "Issuer";
235 id_card.username = dialog.username;
236 id_card.password = dialog.password;
237 id_card.services = {};
238 id_card.set_data("pixbuf", find_icon ("avatar-default", 48));
243 private void add_id_card_data (IdCard id_card)
248 this.listmodel.append (out iter);
249 pixbuf = id_card.get_data("pixbuf");
251 Columns.IDCARD_COL, id_card,
252 Columns.LOGO_COL, pixbuf,
253 Columns.ISSUER_COL, id_card.issuer,
254 Columns.USERNAME_COL, id_card.username,
255 Columns.PASSWORD_COL, id_card.password);
258 private void remove_id_card_data (IdCard id_card)
263 if (listmodel.get_iter_first (out iter))
268 Columns.ISSUER_COL, out issuer);
270 if (id_card.issuer == issuer)
272 listmodel.remove (iter);
276 while (listmodel.iter_next (ref iter));
280 private void add_id_card_widget (IdCard id_card)
282 var id_card_widget = new IdCardWidget (id_card);
284 this.custom_vbox.add_id_card_widget (id_card_widget);
286 id_card_widget.details_id.connect (details_identity_cb);
287 id_card_widget.remove_id.connect (remove_identity_cb);
288 id_card_widget.send_id.connect ((w) => send_identity_cb (w.id_card));
289 id_card_widget.expanded.connect (this.custom_vbox.receive_expanded_event);
290 id_card_widget.expanded.connect (fill_details);
293 /* This method finds a valid display name */
294 public bool display_name_is_valid (string name,
295 out string? candidate)
297 foreach (IdCard id_card in identities_manager.id_card_list)
299 if (id_card.display_name == name)
301 if (&candidate != null)
303 for (int i=0; i<1000; i++)
305 string tmp = "%s %d".printf (name, i);
306 if (display_name_is_valid (tmp, null))
320 public void insert_id_card (IdCard id_card)
324 if (!display_name_is_valid (id_card.display_name, out candidate))
326 id_card.display_name = candidate;
329 this.identities_manager.id_card_list.prepend (id_card);
330 this.identities_manager.store_id_cards ();
332 add_id_card_data (id_card);
333 add_id_card_widget (id_card);
336 public bool add_identity (IdCard id_card)
338 /* TODO: Check if display name already exists */
340 var dialog = new Gtk.MessageDialog (this,
341 Gtk.DialogFlags.DESTROY_WITH_PARENT,
342 Gtk.MessageType.QUESTION,
343 Gtk.ButtonsType.YES_NO,
344 _("Would you like to add '%s' ID Card to the ID Card Organizer?"),
345 id_card.display_name);
348 var ret = dialog.run ();
351 if (ret == Gtk.ResponseType.YES) {
352 id_card.set_data ("pixbuf", find_icon ("avatar-default", 48));
353 this.insert_id_card (id_card);
360 private void add_identity_manual_cb ()
362 var dialog = new AddIdentityDialog ();
363 var result = dialog.run ();
366 case ResponseType.OK:
367 insert_id_card (get_id_card_data (dialog));
375 private void remove_id_card_widget (IdCardWidget id_card_widget)
377 remove_id_card_data (id_card_widget.id_card);
379 this.custom_vbox.remove_id_card_widget (id_card_widget);
382 private void remove_identity (IdCardWidget id_card_widget)
384 var id_card = id_card_widget.id_card;
386 this.identities_manager.id_card_list.remove (id_card);
387 this.identities_manager.store_id_cards ();
389 remove_id_card_widget (id_card_widget);
392 private void redraw_id_card_widgets ()
397 var children = this.custom_vbox.get_children ();
398 foreach (var id_card_widget in children)
399 id_card_widget.destroy();
401 if (filter.get_iter_first (out iter))
406 Columns.IDCARD_COL, out id_card);
408 add_id_card_widget (id_card);
410 while (filter.iter_next (ref iter));
414 private void remove_identity_cb (IdCardWidget id_card_widget)
416 var id_card = id_card_widget.id_card;
418 var dialog = new MessageDialog (null,
419 DialogFlags.DESTROY_WITH_PARENT,
421 Gtk.ButtonsType.YES_NO,
422 _("Are you sure you want to delete %s ID Card?"), id_card.issuer);
423 var result = dialog.run ();
425 case ResponseType.YES:
426 remove_identity (id_card_widget);
434 public void select_identity (IdentityRequest request)
436 IdCard identity = null;
438 this.request_queue.push_tail (request);
440 if (custom_vbox.current_idcard != null &&
441 custom_vbox.current_idcard.send_button != null)
442 custom_vbox.current_idcard.send_button.set_sensitive (true);
444 if (request.select_default)
446 identity = default_id_card;
449 if (identity == null)
451 bool has_nai = request.nai != null && request.nai != "";
452 bool has_srv = request.service != null && request.service != "";
453 bool confirm = false;
454 IdCard nai_provided = null;
456 foreach (IdCard id in identities_manager.id_card_list)
458 /* If NAI matches we add id card to the candidate list */
459 if (has_nai && request.nai == id.nai)
466 /* If any service matches we add id card to the candidate list */
469 foreach (string srv in id.services)
471 if (request.service == srv)
480 /* If more than one candidate we dissasociate service from all ids */
481 if (has_srv && candidates.length() > 1)
483 foreach (IdCard id in candidates)
486 SList<string> services_list = null;
487 foreach (string srv in id.services)
489 if (srv == request.service)
491 services_list.append (srv);
494 if (services_list.length () == 0)
500 string[] services = new string[services_list.length ()];
501 foreach (string srv in services_list)
507 id.services = services;
511 /* If there are no candidates we use the service matching rules */
512 if (candidates.length () == 0)
514 foreach (IdCard id in identities_manager.id_card_list)
516 foreach (Rule rule in id.rules)
518 if (!match_service_pattern (request.service, rule.pattern))
521 candidates.append (id);
523 if (rule.always_confirm == "true")
529 if (candidates.length () > 1)
532 identity = candidates.nth_data (0);
538 redraw_id_card_widgets ();
543 // Send back the identity (we can't directly run the
544 // callback because we may be being called from a 'yield')
545 Idle.add (() => { send_identity_cb (identity); return false; });
549 private bool match_service_pattern (string service, string pattern)
551 var pspec = new PatternSpec (pattern);
552 return pspec.match_string (service);
555 public void send_identity_cb (IdCard identity)
557 return_if_fail (request_queue.length > 0);
559 var request = this.request_queue.pop_head ();
560 bool reset_password = false;
562 if (identity.password == null)
564 var dialog = new AddPasswordDialog ();
565 var result = dialog.run ();
568 case ResponseType.OK:
569 identity.password = dialog.password;
570 reset_password = ! dialog.remember;
580 if (this.request_queue.is_empty())
583 if (identity != null)
584 this.default_id_card = identity;
586 request.return_identity (identity);
589 identity.password = null;
591 candidates = new SList<IdCard>();
594 private void label_make_bold (Label label)
596 var font_desc = new Pango.FontDescription ();
598 font_desc.set_weight (Pango.Weight.BOLD);
600 /* This will only affect the weight of the font, the rest is
601 * from the current state of the widget, which comes from the
602 * theme or user prefs, since the font desc only has the
603 * weight flag turned on.
605 label.modify_font (font_desc);
608 private void fill_services_vbox (IdCard id_card)
611 var n_columns = id_card.services.length;
613 var services_table = new Table (n_columns, 2, false);
614 services_table.set_col_spacings (10);
615 services_table.set_row_spacings (10);
616 this.services_internal_vbox.add (services_table);
618 service_button_map.remove_all ();
620 foreach (string service in id_card.services)
622 var label = new Label (service);
623 label.set_alignment (0, (float) 0.5);
625 var remove_button = new Button.from_stock (Stock.REMOVE);
627 var remove_button = new Button.from_stock (STOCK_REMOVE);
631 service_button_map.insert (remove_button, service);
633 remove_button.clicked.connect ((remove_button) =>
635 var dialog = new Gtk.MessageDialog (this,
636 Gtk.DialogFlags.DESTROY_WITH_PARENT,
637 Gtk.MessageType.QUESTION,
638 Gtk.ButtonsType.YES_NO,
639 _("Are you sure you want to stop '%s' ID Card to use %s?"),
640 custom_vbox.current_idcard.id_card.display_name);
641 var ret = dialog.run();
644 if (ret == Gtk.ResponseType.YES)
646 IdCard idcard = custom_vbox.current_idcard.id_card;
647 var candidate = service_button_map.lookup (remove_button);
649 SList<string> services = new SList<string>();
651 foreach (string srv in idcard.services)
653 if (srv == candidate)
655 services.append (srv);
658 idcard.services = new string[services.length()];
659 for (int j=0; j<idcard.services.length; j++)
661 idcard.services[j] = services.nth_data(j);
664 var children = services_internal_vbox.get_children ();
665 foreach (var hbox in children)
668 fill_services_vbox (idcard);
669 custom_vbox.current_idcard.update_id_card_label ();
673 services_table.attach_defaults (label, 0, 1, i, i+1);
674 services_table.attach_defaults (remove_button, 1, 2, i, i+1);
677 this.services_internal_vbox.show_all ();
680 private void on_about_action ()
683 "Javier Jardón <jjardon@codethink.co.uk>",
684 "Sam Thursfield <samthursfield@codethink.co.uk>",
685 "Alberto Ruiz <alberto.ruiz@codethink.co.uk>",
689 string copyright = "Copyright 2011 JANET";
693 Copyright (c) 2011, JANET(UK)
696 Redistribution and use in source and binary forms, with or without
697 modification, are permitted provided that the following conditions
700 1. Redistributions of source code must retain the above copyright
701 notice, this list of conditions and the following disclaimer.
703 2. Redistributions in binary form must reproduce the above copyright
704 notice, this list of conditions and the following disclaimer in the
705 documentation and/or other materials provided with the distribution.
707 3. Neither the name of JANET(UK) nor the names of its contributors
708 may be used to endorse or promote products derived from this software
709 without specific prior written permission.
711 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"
712 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
713 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
714 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
715 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
716 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
717 OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
718 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
719 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
720 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
724 Gtk.show_about_dialog (this,
725 "comments", _("Moonshot project UI"),
726 "copyright", copyright,
727 "website", "http://www.project-moonshot.org/",
729 "website-label", _("Visit the Moonshot project web site"),
731 "translator-credits", _("translator-credits"),
736 private Gtk.ActionEntry[] create_actions() {
737 Gtk.ActionEntry[] actions = new Gtk.ActionEntry[0];
739 Gtk.ActionEntry filemenu = { "FileMenuAction",
744 Gtk.ActionEntry add = { "AddIdCardAction",
752 N_("Add a new ID Card"),
753 add_identity_manual_cb };
755 Gtk.ActionEntry quit = { "QuitAction",
763 N_("Quit the application"),
767 Gtk.ActionEntry helpmenu = { "HelpMenuAction",
772 Gtk.ActionEntry about = { "AboutAction",
780 N_("About this application"),
788 private void create_ui_manager ()
790 Gtk.ActionGroup action_group = new Gtk.ActionGroup ("GeneralActionGroup");
791 action_group.add_actions (create_actions (), this);
792 ui_manager.insert_action_group (action_group, 0);
795 ui_manager.add_ui_from_string (layout, -1);
799 stderr.printf ("%s\n", e.message);
801 ui_manager.ensure_update ();
804 private void build_ui()
806 create_ui_manager ();
808 this.search_entry = new Entry();
810 set_atk_name_description (search_entry, _("Search entry"), _("Search for a specific ID Card"));
811 this.search_entry.set_icon_from_pixbuf (EntryIconPosition.PRIMARY,
812 find_icon_sized ("edit-find-symbolic", Gtk.IconSize.MENU));
813 this.search_entry.set_icon_tooltip_text (EntryIconPosition.PRIMARY,
814 _("Search identity or service"));
815 this.search_entry.set_icon_sensitive (EntryIconPosition.PRIMARY, false);
817 this.search_entry.set_icon_from_pixbuf (EntryIconPosition.SECONDARY,
818 find_icon_sized ("edit-clear-symbolic", Gtk.IconSize.MENU));
819 this.search_entry.set_icon_tooltip_text (EntryIconPosition.SECONDARY,
820 _("Clear the current search"));
821 this.search_entry.set_icon_sensitive (EntryIconPosition.SECONDARY, false);
824 this.search_entry.icon_press.connect (search_entry_icon_press_cb);
825 this.search_entry.notify["text"].connect (search_entry_text_changed_cb);
826 this.search_entry.key_press_event.connect(search_entry_key_press_event_cb);
828 this.custom_vbox = new CustomVBox (this, false, 6);
830 var viewport = new Viewport (null, null);
831 viewport.set_border_width (6);
832 viewport.set_shadow_type (ShadowType.NONE);
833 viewport.add (custom_vbox);
834 var scroll = new ScrolledWindow (null, null);
835 scroll.set_policy (PolicyType.NEVER, PolicyType.AUTOMATIC);
836 scroll.set_shadow_type (ShadowType.IN);
837 scroll.add_with_viewport (viewport);
839 var vbox_left = new VBox (false, 0);
840 vbox_left.pack_start (search_entry, false, false, 6);
841 vbox_left.pack_start (scroll, true, true, 0);
842 vbox_left.set_size_request (WINDOW_WIDTH, 0);
844 var login_vbox_title = new Label (_("Login: "));
845 label_make_bold (login_vbox_title);
846 login_vbox_title.set_alignment (0, (float) 0.5);
847 var username_label = new Label (_("Username:"));
848 username_label.set_alignment (1, (float) 0.5);
849 this.username_entry = new Entry ();
850 var password_label = new Label (_("Password:"));
851 password_label.set_alignment (1, (float) 0.5);
852 this.password_entry = new Entry ();
853 password_entry.set_invisible_char ('*');
854 password_entry.set_visibility (false);
855 var remember_checkbutton = new CheckButton.with_label (_("Remember password"));
856 var login_table = new Table (3, 3, false);
857 login_table.set_col_spacings (10);
858 login_table.set_row_spacings (10);
859 login_table.attach_defaults (username_label, 0, 1, 0, 1);
860 login_table.attach_defaults (username_entry, 1, 2, 0, 1);
861 login_table.attach_defaults (password_label, 0, 1, 1, 2);
862 login_table.attach_defaults (password_entry, 1, 2, 1, 2);
863 login_table.attach_defaults (remember_checkbutton, 1, 2, 2, 3);
864 var login_vbox_alignment = new Alignment (0, 0, 0, 0);
865 login_vbox_alignment.set_padding (0, 0, 12, 0);
866 login_vbox_alignment.add (login_table);
867 var login_vbox = new VBox (false, 6);
868 login_vbox.pack_start (login_vbox_title, false, true, 0);
869 login_vbox.pack_start (login_vbox_alignment, false, true, 0);
871 var services_vbox_title = new Label (_("Services:"));
872 label_make_bold (services_vbox_title);
873 services_vbox_title.set_alignment (0, (float) 0.5);
874 var services_vbox_alignment = new Alignment (0, 0, 0, 0);
875 services_vbox_alignment.set_padding (0, 0, 12, 0);
876 this.services_internal_vbox = new VBox (true, 6);
877 services_vbox_alignment.add (services_internal_vbox);
878 var services_vbox = new VBox (false, 6);
879 services_vbox.pack_start (services_vbox_title, false, true, 0);
880 services_vbox.pack_start (services_vbox_alignment, false, true, 0);
882 this.vbox_right = new VBox (false, 18);
883 vbox_right.pack_start (login_vbox, false, true, 0);
884 vbox_right.pack_start (services_vbox, false, true, 0);
886 var hbox = new HBox (false, 12);
887 hbox.pack_start (vbox_left, true, true, 0);
888 hbox.pack_start (vbox_right, false, false, 0);
890 var main_vbox = new VBox (false, 0);
891 main_vbox.set_border_width (12);
892 var menubar = this.ui_manager.get_widget ("/MenuBar");
893 main_vbox.pack_start (menubar, false, false, 0);
894 main_vbox.pack_start (hbox, true, true, 0);
897 main_vbox.show_all();
898 this.vbox_right.hide ();
901 private void set_atk_name_description (Widget widget, string name, string description)
903 var atk_widget = widget.get_accessible ();
905 atk_widget.set_name (name);
906 atk_widget.set_description (description);
909 private void connect_signals()
911 this.destroy.connect (Gtk.main_quit);
914 private void init_ipc_server ()
917 /* Errors will currently be sent via g_log - ie. to an
918 * obtrusive message box, on Windows
920 this.ipc_server = MoonshotServer.get_instance ();
921 MoonshotServer.start (this);
924 var conn = DBus.Bus.get (DBus.BusType.SESSION);
925 dynamic DBus.Object bus = conn.get_object ("org.freedesktop.DBus",
926 "/org/freedesktop/DBus",
927 "org.freedesktop.DBus");
929 // try to register service in session bus
930 uint reply = bus.request_name ("org.janet.Moonshot", (uint) 0);
931 assert (reply == DBus.RequestNameReply.PRIMARY_OWNER);
933 this.ipc_server = new MoonshotServer (this);
934 conn.register_object ("/org/janet/moonshot", ipc_server);
939 stderr.printf ("%s\n", e.message);
944 public static int main(string[] args)
949 // Force specific theme settings on Windows without requiring a gtkrc file
950 Gtk.Settings settings = Gtk.Settings.get_default ();
951 settings.set_string_property ("gtk-theme-name", "ms-windows", "moonshot");
952 settings.set_long_property ("gtk-menu-images", 0, "moonshot");
955 Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
956 Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8");
957 Intl.textdomain (Config.GETTEXT_PACKAGE);
959 var window = new MainWindow();