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;
22 private MoonshotServer ipc_server;
24 private IdCard default_id_card;
25 public Queue<IdentityRequest> request_queue;
27 private HashTable<Gtk.Button, string> service_button_map;
39 private const string layout =
41 <menubar name='MenuBar'>
42 <menu name='FileMenu' action='FileMenuAction'>
43 <menuitem name='AddIdCard' action='AddIdCardAction' />
45 <menuitem name='Quit' action='QuitAction' />
48 <menu name='HelpMenu' action='HelpMenuAction'>
49 <menuitem name='About' action='AboutAction' />
56 request_queue = new Queue<IdentityRequest>();
58 service_button_map = new HashTable<Gtk.Button, string> (direct_hash, direct_equal);
60 this.title = "Moonshoot";
61 this.set_position (WindowPosition.CENTER);
62 set_default_size (WINDOW_WIDTH, WINDOW_HEIGHT);
65 setup_identities_list();
71 private bool visible_func (TreeModel model, TreeIter iter)
75 string issuer_casefold;
76 string search_text_casefold;
79 Columns.ISSUER_COL, out issuer);
80 search_text = this.search_entry.get_text ();
82 if (issuer == null || search_text == null)
85 issuer_casefold = issuer.casefold ();
86 search_text_casefold = search_text.casefold ();
88 if (issuer_casefold.contains (search_text_casefold))
94 private void setup_identities_list ()
96 this.listmodel = new ListStore (Columns.N_COLUMNS, typeof (IdCard),
101 this.filter = new TreeModelFilter (listmodel, null);
103 filter.set_visible_func (visible_func);
106 private void search_entry_icon_press_cb (EntryIconPosition pos, Gdk.Event event)
108 if (pos == EntryIconPosition.PRIMARY)
110 print ("Search entry icon pressed\n");
114 this.search_entry.set_text ("");
118 private void search_entry_text_changed_cb ()
120 this.filter.refilter ();
121 redraw_id_card_widgets ();
123 var has_text = this.search_entry.get_text_length () > 0;
124 this.search_entry.set_icon_sensitive (EntryIconPosition.PRIMARY, has_text);
125 this.search_entry.set_icon_sensitive (EntryIconPosition.SECONDARY, has_text);
127 this.vbox_right.set_visible (false);
128 this.resize (WINDOW_WIDTH, WINDOW_HEIGHT);
131 private bool search_entry_key_press_event_cb (Gdk.EventKey e)
133 if(Gdk.keyval_name(e.keyval) == "Escape")
134 this.search_entry.set_text("");
136 // Continue processing this event, since the
137 // text entry functionality needs to see it too.
141 private void load_id_cards ()
143 identities_manager = new IdentitiesManager ();
145 if (identities_manager.id_card_list == null)
148 foreach (IdCard id_card in identities_manager.id_card_list)
150 add_id_card_data (id_card);
151 add_id_card_widget (id_card);
154 this.default_id_card = identities_manager.id_card_list.data;
157 private void fill_details (IdCardWidget id_card_widget)
159 var id_card = id_card_widget.id_card;
160 this.username_entry.set_text (id_card.username);
161 this.password_entry.set_text (id_card.password ?? "");
163 var children = this.services_internal_vbox.get_children ();
164 foreach (var hbox in children)
166 fill_services_vbox (id_card_widget.id_card);
167 identities_manager.store_id_cards();
170 private void show_details (IdCard id_card)
172 this.vbox_right.set_visible (!vbox_right.get_visible ());
174 if (this.vbox_right.get_visible () == false)
176 this.resize (WINDOW_WIDTH, WINDOW_HEIGHT);
180 private void details_identity_cb (IdCardWidget id_card_widget)
182 fill_details (id_card_widget);
183 show_details (id_card_widget.id_card);
186 private IdCard get_id_card_data (AddIdentityDialog dialog)
188 var id_card = new IdCard ();
190 id_card.display_name = dialog.display_name;
191 id_card.issuer = dialog.issuer;
192 if (id_card.issuer == "")
193 id_card.issuer = "Issuer";
194 id_card.username = dialog.username;
195 id_card.password = dialog.password;
196 id_card.services = {};
197 id_card.set_data("pixbuf", find_icon ("avatar-default", 48));
202 private void add_id_card_data (IdCard id_card)
207 this.listmodel.append (out iter);
208 pixbuf = id_card.get_data("pixbuf");
210 Columns.IDCARD_COL, id_card,
211 Columns.LOGO_COL, pixbuf,
212 Columns.ISSUER_COL, id_card.issuer,
213 Columns.USERNAME_COL, id_card.username,
214 Columns.PASSWORD_COL, id_card.password);
217 private void remove_id_card_data (IdCard id_card)
222 if (listmodel.get_iter_first (out iter))
227 Columns.ISSUER_COL, out issuer);
229 if (id_card.issuer == issuer)
231 listmodel.remove (iter);
235 while (listmodel.iter_next (ref iter));
239 private void add_id_card_widget (IdCard id_card)
241 var id_card_widget = new IdCardWidget (id_card);
243 this.custom_vbox.add_id_card_widget (id_card_widget);
245 id_card_widget.details_id.connect (details_identity_cb);
246 id_card_widget.remove_id.connect (remove_identity_cb);
247 id_card_widget.send_id.connect ((w) => send_identity_cb (w.id_card));
248 id_card_widget.expanded.connect (this.custom_vbox.receive_expanded_event);
249 id_card_widget.expanded.connect (fill_details);
252 /* This method finds a valid display name */
253 public bool display_name_is_valid (string name,
254 out string? candidate)
256 foreach (IdCard id_card in identities_manager.id_card_list)
258 if (id_card.display_name == name)
260 if (&candidate != null)
262 for (int i=0; i<1000; i++)
264 string tmp = "%s %d".printf (name, i);
265 if (display_name_is_valid (tmp, null))
279 public void insert_id_card (IdCard id_card)
283 if (!display_name_is_valid (id_card.display_name, out candidate))
285 id_card.display_name = candidate;
288 this.identities_manager.id_card_list.prepend (id_card);
289 this.identities_manager.store_id_cards ();
291 add_id_card_data (id_card);
292 add_id_card_widget (id_card);
295 public bool add_identity (IdCard id_card)
297 /* TODO: Check if display name already exists */
299 var dialog = new Gtk.MessageDialog (this,
300 Gtk.DialogFlags.DESTROY_WITH_PARENT,
301 Gtk.MessageType.QUESTION,
302 Gtk.ButtonsType.YES_NO,
303 _("Would you like to add '%s' ID Card to the ID Card Organizer?"),
304 id_card.display_name);
307 var ret = dialog.run ();
310 if (ret == Gtk.ResponseType.YES) {
311 id_card.set_data ("pixbuf", find_icon ("avatar-default", 48));
312 this.insert_id_card (id_card);
319 private void add_identity_manual_cb ()
321 var dialog = new AddIdentityDialog ();
322 var result = dialog.run ();
325 case ResponseType.OK:
326 insert_id_card (get_id_card_data (dialog));
334 private void remove_id_card_widget (IdCardWidget id_card_widget)
336 remove_id_card_data (id_card_widget.id_card);
338 this.custom_vbox.remove_id_card_widget (id_card_widget);
341 private void remove_identity (IdCardWidget id_card_widget)
343 var id_card = id_card_widget.id_card;
345 this.identities_manager.id_card_list.remove (id_card);
346 this.identities_manager.store_id_cards ();
348 remove_id_card_widget (id_card_widget);
351 private void redraw_id_card_widgets ()
356 var children = this.custom_vbox.get_children ();
357 foreach (var id_card_widget in children)
358 id_card_widget.destroy();
360 if (filter.get_iter_first (out iter))
365 Columns.IDCARD_COL, out id_card);
367 add_id_card_widget (id_card);
369 while (filter.iter_next (ref iter));
373 private void remove_identity_cb (IdCardWidget id_card_widget)
375 var id_card = id_card_widget.id_card;
377 var dialog = new MessageDialog (null,
378 DialogFlags.DESTROY_WITH_PARENT,
380 Gtk.ButtonsType.YES_NO,
381 _("Are you sure you want to delete %s ID Card?"), id_card.issuer);
382 var result = dialog.run ();
384 case ResponseType.YES:
385 remove_identity (id_card_widget);
393 public void select_identity (IdentityRequest request)
395 IdCard identity = null;
397 this.request_queue.push_tail (request);
399 if (custom_vbox.current_idcard != null &&
400 custom_vbox.current_idcard.send_button != null)
401 custom_vbox.current_idcard.send_button.set_sensitive (true);
403 if (request.select_default)
405 identity = this.default_id_card;
408 /* Automatic service matching rules can go here */
410 if (identity == null)
412 // Resort to manual selection
417 // Send back the identity (we can't directly run the
418 // callback because we may be being called from a 'yield')
419 Idle.add (() => { send_identity_cb (identity); return false; });
424 public void send_identity_cb (IdCard identity)
426 return_if_fail (request_queue.length > 0);
428 var request = this.request_queue.pop_head ();
429 bool reset_password = false;
431 if (identity.password == null)
433 var dialog = new AddPasswordDialog ();
434 var result = dialog.run ();
437 case ResponseType.OK:
438 identity.password = dialog.password;
439 reset_password = ! dialog.remember;
449 if (this.request_queue.is_empty())
452 if (identity != null)
453 this.default_id_card = identity;
455 request.return_identity (identity);
458 identity.password = null;
461 private void label_make_bold (Label label)
463 var font_desc = new Pango.FontDescription ();
465 font_desc.set_weight (Pango.Weight.BOLD);
467 /* This will only affect the weight of the font, the rest is
468 * from the current state of the widget, which comes from the
469 * theme or user prefs, since the font desc only has the
470 * weight flag turned on.
472 label.modify_font (font_desc);
475 private void fill_services_vbox (IdCard id_card)
478 var n_columns = id_card.services.length;
480 var services_table = new Table (n_columns, 2, false);
481 services_table.set_col_spacings (10);
482 services_table.set_row_spacings (10);
483 this.services_internal_vbox.add (services_table);
485 service_button_map.remove_all ();
487 foreach (string service in id_card.services)
489 var label = new Label (service);
490 label.set_alignment (0, (float) 0.5);
492 var remove_button = new Button.from_stock (Stock.REMOVE);
494 var remove_button = new Button.from_stock (STOCK_REMOVE);
498 service_button_map.insert (remove_button, service);
500 remove_button.clicked.connect ((remove_button) =>
502 var dialog = new Gtk.MessageDialog (this,
503 Gtk.DialogFlags.DESTROY_WITH_PARENT,
504 Gtk.MessageType.QUESTION,
505 Gtk.ButtonsType.YES_NO,
506 _("Are you sure you want to stop '%s' ID Card to use %s?"),
507 custom_vbox.current_idcard.id_card.display_name);
508 var ret = dialog.run();
511 if (ret == Gtk.ResponseType.YES)
513 IdCard idcard = custom_vbox.current_idcard.id_card;
514 var candidate = service_button_map.lookup (remove_button);
516 SList<string> services = new SList<string>();
518 foreach (string srv in id_card.services)
520 if (srv == candidate)
522 services.append (srv);
525 id_card.services = new string[services.length()];
526 for (int j=0; j<id_card.services.length; j++)
528 id_card.services[j] = services.nth_data(j);
531 var children = services_internal_vbox.get_children ();
532 foreach (var hbox in children)
535 fill_services_vbox (id_card);
536 custom_vbox.current_idcard.update_id_card_label ();
540 services_table.attach_defaults (label, 0, 1, i, i+1);
541 services_table.attach_defaults (remove_button, 1, 2, i, i+1);
544 this.services_internal_vbox.show_all ();
547 private void on_about_action ()
550 "Javier Jardón <jjardon@codethink.co.uk>",
551 "Sam Thursfield <samthursfield@codethink.co.uk>",
552 "Alberto Ruiz <alberto.ruiz@codethink.co.uk>",
556 string copyright = "Copyright 2011 JANET";
560 Copyright (c) 2011, JANET(UK)
563 Redistribution and use in source and binary forms, with or without
564 modification, are permitted provided that the following conditions
567 1. Redistributions of source code must retain the above copyright
568 notice, this list of conditions and the following disclaimer.
570 2. Redistributions in binary form must reproduce the above copyright
571 notice, this list of conditions and the following disclaimer in the
572 documentation and/or other materials provided with the distribution.
574 3. Neither the name of JANET(UK) nor the names of its contributors
575 may be used to endorse or promote products derived from this software
576 without specific prior written permission.
578 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"
579 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
580 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
581 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
582 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
583 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
584 OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
585 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
586 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
587 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
591 Gtk.show_about_dialog (this,
592 "comments", _("Moonshot project UI"),
593 "copyright", copyright,
594 "website", "http://www.project-moonshot.org/",
596 "website-label", _("Visit the Moonshot project web site"),
598 "translator-credits", _("translator-credits"),
603 private Gtk.ActionEntry[] create_actions() {
604 Gtk.ActionEntry[] actions = new Gtk.ActionEntry[0];
606 Gtk.ActionEntry filemenu = { "FileMenuAction",
611 Gtk.ActionEntry add = { "AddIdCardAction",
619 N_("Add a new ID Card"),
620 add_identity_manual_cb };
622 Gtk.ActionEntry quit = { "QuitAction",
630 N_("Quit the application"),
634 Gtk.ActionEntry helpmenu = { "HelpMenuAction",
639 Gtk.ActionEntry about = { "AboutAction",
647 N_("About this application"),
655 private void create_ui_manager ()
657 Gtk.ActionGroup action_group = new Gtk.ActionGroup ("GeneralActionGroup");
658 action_group.add_actions (create_actions (), this);
659 ui_manager.insert_action_group (action_group, 0);
662 ui_manager.add_ui_from_string (layout, -1);
666 stderr.printf ("%s\n", e.message);
668 ui_manager.ensure_update ();
671 private void build_ui()
673 create_ui_manager ();
675 this.search_entry = new Entry();
677 set_atk_name_description (search_entry, _("Search entry"), _("Search for a specific ID Card"));
678 this.search_entry.set_icon_from_pixbuf (EntryIconPosition.PRIMARY,
679 find_icon_sized ("edit-find-symbolic", Gtk.IconSize.MENU));
680 this.search_entry.set_icon_tooltip_text (EntryIconPosition.PRIMARY,
681 _("Search identity or service"));
682 this.search_entry.set_icon_sensitive (EntryIconPosition.PRIMARY, false);
684 this.search_entry.set_icon_from_pixbuf (EntryIconPosition.SECONDARY,
685 find_icon_sized ("edit-clear-symbolic", Gtk.IconSize.MENU));
686 this.search_entry.set_icon_tooltip_text (EntryIconPosition.SECONDARY,
687 _("Clear the current search"));
688 this.search_entry.set_icon_sensitive (EntryIconPosition.SECONDARY, false);
691 this.search_entry.icon_press.connect (search_entry_icon_press_cb);
692 this.search_entry.notify["text"].connect (search_entry_text_changed_cb);
693 this.search_entry.key_press_event.connect(search_entry_key_press_event_cb);
695 this.custom_vbox = new CustomVBox (this, false, 6);
697 var viewport = new Viewport (null, null);
698 viewport.set_border_width (6);
699 viewport.set_shadow_type (ShadowType.NONE);
700 viewport.add (custom_vbox);
701 var scroll = new ScrolledWindow (null, null);
702 scroll.set_policy (PolicyType.NEVER, PolicyType.AUTOMATIC);
703 scroll.set_shadow_type (ShadowType.IN);
704 scroll.add_with_viewport (viewport);
706 var vbox_left = new VBox (false, 0);
707 vbox_left.pack_start (search_entry, false, false, 6);
708 vbox_left.pack_start (scroll, true, true, 0);
709 vbox_left.set_size_request (WINDOW_WIDTH, 0);
711 var login_vbox_title = new Label (_("Login: "));
712 label_make_bold (login_vbox_title);
713 login_vbox_title.set_alignment (0, (float) 0.5);
714 var username_label = new Label (_("Username:"));
715 username_label.set_alignment (1, (float) 0.5);
716 this.username_entry = new Entry ();
717 var password_label = new Label (_("Password:"));
718 password_label.set_alignment (1, (float) 0.5);
719 this.password_entry = new Entry ();
720 password_entry.set_invisible_char ('*');
721 password_entry.set_visibility (false);
722 var remember_checkbutton = new CheckButton.with_label (_("Remember password"));
723 var login_table = new Table (3, 3, false);
724 login_table.set_col_spacings (10);
725 login_table.set_row_spacings (10);
726 login_table.attach_defaults (username_label, 0, 1, 0, 1);
727 login_table.attach_defaults (username_entry, 1, 2, 0, 1);
728 login_table.attach_defaults (password_label, 0, 1, 1, 2);
729 login_table.attach_defaults (password_entry, 1, 2, 1, 2);
730 login_table.attach_defaults (remember_checkbutton, 1, 2, 2, 3);
731 var login_vbox_alignment = new Alignment (0, 0, 0, 0);
732 login_vbox_alignment.set_padding (0, 0, 12, 0);
733 login_vbox_alignment.add (login_table);
734 var login_vbox = new VBox (false, 6);
735 login_vbox.pack_start (login_vbox_title, false, true, 0);
736 login_vbox.pack_start (login_vbox_alignment, false, true, 0);
738 var services_vbox_title = new Label (_("Services:"));
739 label_make_bold (services_vbox_title);
740 services_vbox_title.set_alignment (0, (float) 0.5);
741 var services_vbox_alignment = new Alignment (0, 0, 0, 0);
742 services_vbox_alignment.set_padding (0, 0, 12, 0);
743 this.services_internal_vbox = new VBox (true, 6);
744 services_vbox_alignment.add (services_internal_vbox);
745 var services_vbox = new VBox (false, 6);
746 services_vbox.pack_start (services_vbox_title, false, true, 0);
747 services_vbox.pack_start (services_vbox_alignment, false, true, 0);
749 this.vbox_right = new VBox (false, 18);
750 vbox_right.pack_start (login_vbox, false, true, 0);
751 vbox_right.pack_start (services_vbox, false, true, 0);
753 var hbox = new HBox (false, 12);
754 hbox.pack_start (vbox_left, true, true, 0);
755 hbox.pack_start (vbox_right, false, false, 0);
757 var main_vbox = new VBox (false, 0);
758 main_vbox.set_border_width (12);
759 var menubar = this.ui_manager.get_widget ("/MenuBar");
760 main_vbox.pack_start (menubar, false, false, 0);
761 main_vbox.pack_start (hbox, true, true, 0);
764 main_vbox.show_all();
765 this.vbox_right.hide ();
768 private void set_atk_name_description (Widget widget, string name, string description)
770 var atk_widget = widget.get_accessible ();
772 atk_widget.set_name (name);
773 atk_widget.set_description (description);
776 private void connect_signals()
778 this.destroy.connect (Gtk.main_quit);
781 private void init_ipc_server ()
784 /* Errors will currently be sent via g_log - ie. to an
785 * obtrusive message box, on Windows
787 this.ipc_server = MoonshotServer.get_instance ();
788 MoonshotServer.start (this);
791 var conn = DBus.Bus.get (DBus.BusType.SESSION);
792 dynamic DBus.Object bus = conn.get_object ("org.freedesktop.DBus",
793 "/org/freedesktop/DBus",
794 "org.freedesktop.DBus");
796 // try to register service in session bus
797 uint reply = bus.request_name ("org.janet.Moonshot", (uint) 0);
798 assert (reply == DBus.RequestNameReply.PRIMARY_OWNER);
800 this.ipc_server = new MoonshotServer (this);
801 conn.register_object ("/org/janet/moonshot", ipc_server);
806 stderr.printf ("%s\n", e.message);
811 public static int main(string[] args)
816 // Force specific theme settings on Windows without requiring a gtkrc file
817 Gtk.Settings settings = Gtk.Settings.get_default ();
818 settings.set_string_property ("gtk-theme-name", "ms-windows", "moonshot");
819 settings.set_long_property ("gtk-menu-images", 0, "moonshot");
822 Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
823 Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8");
824 Intl.textdomain (Config.GETTEXT_PACKAGE);
826 var window = new MainWindow();