Use GKeyFile backend
[moonshot-ui.git] / src / moonshot-window.vala
1 using Gtk;
2
3 class MainWindow : Window
4 {
5     private const int WINDOW_WIDTH = 400;
6     private const int WINDOW_HEIGHT = 500;
7
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;
13
14     private Entry username_entry;
15     private Entry password_entry;
16
17     private ListStore listmodel;
18     private TreeModelFilter filter;
19
20     public IdentitiesManager identities_manager;
21
22     private MoonshotServer ipc_server;
23
24     private IdCard default_id_card;
25     private Queue<IdentityRequest> request_queue;
26
27     private enum Columns
28     {
29         IDCARD_COL,
30         LOGO_COL,
31         ISSUER_COL,
32         USERNAME_COL,
33         PASSWORD_COL,
34         N_COLUMNS
35     }
36
37     private const string layout =
38 "
39 <menubar name='MenuBar'>
40         <menu name='FileMenu' action='FileMenuAction'>
41             <menuitem name='AddIdCard' action='AddIdCardAction' />
42             <separator />
43             <menuitem name='Quit' action='QuitAction' />
44         </menu>
45
46         <menu name='HelpMenu' action='HelpMenuAction'>
47              <menuitem name='About' action='AboutAction' />
48         </menu>
49 </menubar>
50 ";
51
52     public MainWindow()
53     {
54         request_queue = new Queue<IdentityRequest>();
55
56         this.title = "Moonshoot";
57         this.set_position (WindowPosition.CENTER);
58         set_default_size (WINDOW_WIDTH, WINDOW_HEIGHT);
59
60         build_ui();
61         setup_identities_list();
62         load_id_cards();
63         connect_signals();
64         init_ipc_server();
65     }
66
67     private bool visible_func (TreeModel model, TreeIter iter)
68     {
69         string issuer;
70         string search_text;
71         string issuer_casefold;
72         string search_text_casefold;
73
74         model.get (iter,
75                    Columns.ISSUER_COL, out issuer);
76         search_text = this.search_entry.get_text ();
77
78         if (issuer == null || search_text == null)
79             return false;
80
81         issuer_casefold = issuer.casefold ();
82         search_text_casefold = search_text.casefold ();
83
84         if (issuer_casefold.contains (search_text_casefold))
85             return true;
86
87         return false;
88     }
89
90     private void setup_identities_list ()
91     {
92        this.listmodel = new ListStore (Columns.N_COLUMNS, typeof (IdCard),
93                                                           typeof (Gdk.Pixbuf),
94                                                           typeof (string),
95                                                           typeof (string),
96                                                           typeof (string));
97       this.filter = new TreeModelFilter (listmodel, null);
98
99       filter.set_visible_func (visible_func);
100     }
101
102     private void search_entry_icon_press_cb (EntryIconPosition pos, Gdk.Event event)
103     {
104         if (pos == EntryIconPosition.PRIMARY)
105         {
106             print ("Search entry icon pressed\n");
107         }
108         else
109         {
110             this.search_entry.set_text ("");
111         }
112     }
113
114     private void search_entry_text_changed_cb ()
115     {
116         this.filter.refilter ();
117         redraw_id_card_widgets ();
118
119         var has_text = this.search_entry.get_text_length () > 0;
120         this.search_entry.set_icon_sensitive (EntryIconPosition.PRIMARY, has_text);
121         this.search_entry.set_icon_sensitive (EntryIconPosition.SECONDARY, has_text);
122
123         this.vbox_right.set_visible (false);
124         this.resize (WINDOW_WIDTH, WINDOW_HEIGHT);
125     }
126
127     private bool search_entry_key_press_event_cb (Gdk.EventKey e)
128     {
129         if(Gdk.keyval_name(e.keyval) == "Escape")
130            this.search_entry.set_text("");
131
132         // Continue processing this event, since the
133         // text entry functionality needs to see it too.
134         return false;
135     }
136
137     private void load_id_cards ()
138     {
139         this.identities_manager = new IdentitiesManager ();
140
141         foreach (IdCard id_card in identities_manager.id_card_list)
142         {
143             add_id_card_data (id_card);
144             add_id_card_widget (id_card);
145         }
146
147         this.default_id_card = identities_manager.id_card_list.data;
148     }
149
150     private void fill_details (IdCardWidget id_card_widget)
151     {
152        var id_card = id_card_widget.id_card;
153        this.username_entry.set_text (id_card.username);
154        this.password_entry.set_text (id_card.password ?? "");
155
156        var children = this.services_internal_vbox.get_children ();
157        foreach (var hbox in children)
158            hbox.destroy();
159        fill_services_vbox (id_card_widget.id_card);
160     }
161
162     private void show_details (IdCard id_card)
163     {
164        this.vbox_right.set_visible (!vbox_right.get_visible ());
165
166        if (this.vbox_right.get_visible () == false)
167        {
168            this.resize (WINDOW_WIDTH, WINDOW_HEIGHT);
169        }
170     }
171
172     private void details_identity_cb (IdCardWidget id_card_widget)
173     {
174        fill_details (id_card_widget);
175        show_details (id_card_widget.id_card);
176     }
177
178     private IdCard get_id_card_data (AddIdentityDialog dialog)
179     {
180         var id_card = new IdCard ();
181
182         id_card.issuer = dialog.issuer;
183         if (id_card.issuer == "")
184             id_card.issuer = "Issuer";
185         id_card.username = dialog.username;
186         id_card.password = dialog.password;
187         id_card.pixbuf = find_icon ("avatar-default", 48);
188         id_card.services = {};
189
190         return id_card;
191     }
192
193     private void add_id_card_data (IdCard id_card)
194     {
195         TreeIter iter;
196
197         this.listmodel.append (out iter);
198         listmodel.set (iter,
199                        Columns.IDCARD_COL, id_card,
200                        Columns.LOGO_COL, id_card.pixbuf,
201                        Columns.ISSUER_COL, id_card.issuer,
202                        Columns.USERNAME_COL, id_card.username,
203                        Columns.PASSWORD_COL, id_card.password);
204     }
205
206     private void remove_id_card_data (IdCard id_card)
207     {
208         TreeIter iter;
209         string issuer;
210
211         if (listmodel.get_iter_first (out iter))
212         {
213             do
214             {
215                 listmodel.get (iter,
216                                Columns.ISSUER_COL, out issuer);
217
218                 if (id_card.issuer == issuer)
219                 {
220                     listmodel.remove (iter);
221                     break;
222                 }
223             }
224             while (listmodel.iter_next (ref iter));
225         }
226     }
227
228     private void add_id_card_widget (IdCard id_card)
229     {
230         var id_card_widget = new IdCardWidget (id_card);
231
232         this.custom_vbox.add_id_card_widget (id_card_widget);
233
234         id_card_widget.details_id.connect (details_identity_cb);
235         id_card_widget.remove_id.connect (remove_identity_cb);
236         id_card_widget.send_id.connect ((w) => send_identity_cb (w.id_card));
237         id_card_widget.expanded.connect (this.custom_vbox.receive_expanded_event);
238         id_card_widget.expanded.connect (fill_details);
239     }
240
241     private void add_identity (AddIdentityDialog dialog)
242     {
243         insert_id_card (get_id_card_data (dialog));
244     }
245     
246     public void insert_id_card (IdCard id_card)
247     {    
248         this.identities_manager.id_card_list.prepend (id_card);
249         this.identities_manager.store_id_cards ();
250
251         add_id_card_data (id_card);
252         add_id_card_widget (id_card);
253     }
254
255     private void add_identity_cb ()
256     {
257         var dialog = new AddIdentityDialog ();
258         var result = dialog.run ();
259
260         switch (result) {
261         case ResponseType.OK:
262             add_identity (dialog);
263             break;
264         default:
265             break;
266         }
267         dialog.destroy ();
268     }
269
270     private void remove_id_card_widget (IdCardWidget id_card_widget)
271     {
272         remove_id_card_data (id_card_widget.id_card);
273
274         this.custom_vbox.remove_id_card_widget (id_card_widget);
275     }
276
277     private void remove_identity (IdCardWidget id_card_widget)
278     {
279         var id_card = id_card_widget.id_card;
280
281         this.identities_manager.id_card_list.remove (id_card);
282         this.identities_manager.store_id_cards ();
283
284         remove_id_card_widget (id_card_widget);
285     }
286
287     private void redraw_id_card_widgets ()
288     {
289         TreeIter iter;
290         IdCard id_card;
291
292         var children = this.custom_vbox.get_children ();
293         foreach (var id_card_widget in children)
294             id_card_widget.destroy();
295
296         if (filter.get_iter_first (out iter))
297         {
298             do
299             {
300                 filter.get (iter,
301                             Columns.IDCARD_COL, out id_card);
302
303                 add_id_card_widget (id_card);
304             }
305             while (filter.iter_next (ref iter));
306         }
307     }
308
309     private void remove_identity_cb (IdCardWidget id_card_widget)
310     {
311         var id_card = id_card_widget.id_card;
312
313         var dialog = new MessageDialog (null,
314                                         DialogFlags.DESTROY_WITH_PARENT,
315                                         MessageType.INFO,
316                                         Gtk.ButtonsType.YES_NO,
317                                         _("Are you sure you want to delete %s ID Card?"), id_card.issuer);
318         var result = dialog.run ();
319         switch (result) {
320         case ResponseType.YES:
321             remove_identity (id_card_widget);
322             break;
323         default:
324             break;
325         }
326         dialog.destroy ();
327     }
328
329     public void select_identity (IdentityRequest request)
330     {
331         IdCard identity = null;
332
333         this.request_queue.push_tail (request);
334
335         if (request.select_default)
336         {
337             identity = this.default_id_card;
338         }
339
340         /* Automatic service matching rules can go here */
341
342         if (identity == null)
343         {
344             // Resort to manual selection
345             this.show ();
346         }
347         else
348         {
349             // Send back the identity (we can't directly run the
350             // callback because we may be being called from a 'yield')
351             Idle.add (() => { send_identity_cb (identity); return false; });
352             return;
353         }
354     }
355
356     public void send_identity_cb (IdCard identity)
357     {
358         return_if_fail (request_queue.length > 0);
359
360         var request = this.request_queue.pop_head ();
361         bool reset_password = false;
362
363         if (identity.password == null)
364         {
365             var dialog = new AddPasswordDialog ();
366             var result = dialog.run ();
367
368             switch (result) {
369             case ResponseType.OK:
370                 identity.password = dialog.password;
371                 reset_password = ! dialog.remember;
372                 break;
373             default:
374                 identity = null;
375                 break;
376             }
377
378             dialog.destroy ();
379         }
380
381         if (this.request_queue.is_empty())
382             this.hide ();
383
384         if (identity != null)
385             this.default_id_card = identity;
386
387         request.return_identity (identity);
388
389         if (reset_password)
390             identity.password = null;
391     }
392
393     private void label_make_bold (Label label)
394     {
395         var font_desc = new Pango.FontDescription ();
396
397         font_desc.set_weight (Pango.Weight.BOLD);
398
399         /* This will only affect the weight of the font, the rest is
400          * from the current state of the widget, which comes from the
401          * theme or user prefs, since the font desc only has the
402          * weight flag turned on.
403          */
404         label.modify_font (font_desc);
405     }
406
407     private void fill_services_vbox (IdCard id_card)
408     {
409         int i = 0;
410         var n_columns = id_card.services.length;
411
412         var services_table = new Table (n_columns, 2, false);
413         services_table.set_col_spacings (10);
414         services_table.set_row_spacings (10);
415         this.services_internal_vbox.add (services_table);
416
417         foreach (string service in id_card.services)
418         {
419             var label = new Label (service);
420             label.set_alignment (0, (float) 0.5);
421 #if VALA_0_12
422             var remove_button = new Button.from_stock (Stock.REMOVE);
423 #else
424             var remove_button = new Button.from_stock (STOCK_REMOVE);
425 #endif
426             services_table.attach_defaults (label, 0, 1, i, i+1);
427             services_table.attach_defaults (remove_button, 1, 2, i, i+1);
428             i++;
429         }
430         this.services_internal_vbox.show_all ();
431     }
432
433     private void on_about_action ()
434     {
435         string[] authors = {
436             "Javier Jardón <jjardon@codethink.co.uk>",
437             "Sam Thursfield <samthursfield@codethink.co.uk>",
438             "Alberto Ruiz <alberto.ruiz@codethink.co.uk>",
439             null
440         };
441
442         string copyright = "Copyright 2011 JANET";
443
444         string license =
445 """
446 Copyright (c) 2011, JANET(UK)
447 All rights reserved.
448
449 Redistribution and use in source and binary forms, with or without
450 modification, are permitted provided that the following conditions
451 are met:
452
453 1. Redistributions of source code must retain the above copyright
454    notice, this list of conditions and the following disclaimer.
455
456 2. Redistributions in binary form must reproduce the above copyright
457    notice, this list of conditions and the following disclaimer in the
458    documentation and/or other materials provided with the distribution.
459
460 3. Neither the name of JANET(UK) nor the names of its contributors
461    may be used to endorse or promote products derived from this software
462    without specific prior written permission.
463
464 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"
465 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
466 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
467 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
468 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
469 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
470 OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
471 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
472 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
473 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
474 SUCH DAMAGE.
475 """;
476
477         Gtk.show_about_dialog (this,
478             "comments", _("Moonshot project UI"),
479             "copyright", copyright,
480             "website", "http://www.project-moonshot.org/",
481             "license", license,
482             "website-label", _("Visit the Moonshot project web site"),
483             "authors", authors,
484             "translator-credits", _("translator-credits"),
485             null
486         );
487     }
488
489     private Gtk.ActionEntry[] create_actions() {
490         Gtk.ActionEntry[] actions = new Gtk.ActionEntry[0];
491
492         Gtk.ActionEntry filemenu = { "FileMenuAction",
493                                      null,
494                                      N_("_File"),
495                                      null, null, null };
496         actions += filemenu;
497         Gtk.ActionEntry add = { "AddIdCardAction",
498 #if VALA_0_12
499                                 Stock.ADD,
500 #else
501                                 STOCK_ADD,
502 #endif
503                                 N_("Add ID Card"),
504                                 null,
505                                 N_("Add a new ID Card"),
506                                 add_identity_cb };
507         actions += add;
508         Gtk.ActionEntry quit = { "QuitAction",
509 #if VALA_0_12
510                                  Stock.QUIT,
511 #else
512                                  STOCK_QUIT,
513 #endif
514                                  N_("Quit"),
515                                  "<control>Q",
516                                  N_("Quit the application"),
517                                  Gtk.main_quit };
518         actions += quit;
519
520         Gtk.ActionEntry helpmenu = { "HelpMenuAction",
521                                      null,
522                                      N_("_Help"),
523                                      null, null, null };
524         actions += helpmenu;
525         Gtk.ActionEntry about = { "AboutAction",
526 #if VALA_0_12
527                                   Stock.ABOUT,
528 #else
529                                   STOCK_ABOUT,
530 #endif
531                                   N_("About"),
532                                   null,
533                                   N_("About this application"),
534                                   on_about_action };
535         actions += about;
536
537         return actions;
538     }
539
540
541     private void create_ui_manager ()
542     {
543         Gtk.ActionGroup action_group = new Gtk.ActionGroup ("GeneralActionGroup");
544         action_group.add_actions (create_actions (), this);
545         ui_manager.insert_action_group (action_group, 0);
546         try
547         {
548             ui_manager.add_ui_from_string (layout, -1);
549         }
550         catch (Error e)
551         {
552             stderr.printf ("%s\n", e.message);
553         }
554         ui_manager.ensure_update ();
555     }
556
557     private void build_ui()
558     {
559         create_ui_manager ();
560
561         this.search_entry = new Entry();
562
563         set_atk_name_description (search_entry, _("Search entry"), _("Search for a specific ID Card"));
564         this.search_entry.set_icon_from_pixbuf (EntryIconPosition.PRIMARY,
565                                                 find_icon_sized ("edit-find-symbolic", Gtk.IconSize.MENU));
566         this.search_entry.set_icon_tooltip_text (EntryIconPosition.PRIMARY,
567                                                  _("Search identity or service"));
568         this.search_entry.set_icon_sensitive (EntryIconPosition.PRIMARY, false);
569
570         this.search_entry.set_icon_from_pixbuf (EntryIconPosition.SECONDARY,
571                                                 find_icon_sized ("edit-clear-symbolic", Gtk.IconSize.MENU));
572         this.search_entry.set_icon_tooltip_text (EntryIconPosition.SECONDARY,
573                                                  _("Clear the current search"));
574         this.search_entry.set_icon_sensitive (EntryIconPosition.SECONDARY, false);
575
576
577         this.search_entry.icon_press.connect (search_entry_icon_press_cb);
578         this.search_entry.notify["text"].connect (search_entry_text_changed_cb);
579         this.search_entry.key_press_event.connect(search_entry_key_press_event_cb);
580
581         this.custom_vbox = new CustomVBox (false, 6);
582
583         var viewport = new Viewport (null, null);
584         viewport.set_border_width (6);
585         viewport.set_shadow_type (ShadowType.NONE);
586         viewport.add (custom_vbox);
587         var scroll = new ScrolledWindow (null, null);
588         scroll.set_policy (PolicyType.NEVER, PolicyType.AUTOMATIC);
589         scroll.set_shadow_type (ShadowType.IN);
590         scroll.add_with_viewport (viewport);
591
592         var vbox_left = new VBox (false, 0);
593         vbox_left.pack_start (search_entry, false, false, 6);
594         vbox_left.pack_start (scroll, true, true, 0);
595         vbox_left.set_size_request (WINDOW_WIDTH, 0);
596
597         var login_vbox_title = new Label (_("Login: "));
598         label_make_bold (login_vbox_title);
599         login_vbox_title.set_alignment (0, (float) 0.5);
600         var username_label = new Label (_("Username:"));
601         username_label.set_alignment (1, (float) 0.5);
602         this.username_entry = new Entry ();
603         var password_label = new Label (_("Password:"));
604         password_label.set_alignment (1, (float) 0.5);
605         this.password_entry = new Entry ();
606         password_entry.set_invisible_char ('*');
607         password_entry.set_visibility (false);
608         var remember_checkbutton = new CheckButton.with_label (_("Remember password"));
609         var login_table = new Table (3, 3, false);
610         login_table.set_col_spacings (10);
611         login_table.set_row_spacings (10);
612         login_table.attach_defaults (username_label, 0, 1, 0, 1);
613         login_table.attach_defaults (username_entry, 1, 2, 0, 1);
614         login_table.attach_defaults (password_label, 0, 1, 1, 2);
615         login_table.attach_defaults (password_entry, 1, 2, 1, 2);
616         login_table.attach_defaults (remember_checkbutton,  1, 2, 2, 3);
617         var login_vbox_alignment = new Alignment (0, 0, 0, 0);
618         login_vbox_alignment.set_padding (0, 0, 12, 0);
619         login_vbox_alignment.add (login_table);
620         var login_vbox = new VBox (false, 6);
621         login_vbox.pack_start (login_vbox_title, false, true, 0);
622         login_vbox.pack_start (login_vbox_alignment, false, true, 0);
623
624         var services_vbox_title = new Label (_("Services:"));
625         label_make_bold (services_vbox_title);
626         services_vbox_title.set_alignment (0, (float) 0.5);
627         var services_vbox_alignment = new Alignment (0, 0, 0, 0);
628         services_vbox_alignment.set_padding (0, 0, 12, 0);
629         this.services_internal_vbox = new VBox (true, 6);
630         services_vbox_alignment.add (services_internal_vbox);
631         var services_vbox = new VBox (false, 6);
632         services_vbox.pack_start (services_vbox_title, false, true, 0);
633         services_vbox.pack_start (services_vbox_alignment, false, true, 0);
634
635         this.vbox_right = new VBox (false, 18);
636         vbox_right.pack_start (login_vbox, false, true, 0);
637         vbox_right.pack_start (services_vbox, false, true, 0);
638
639         var hbox = new HBox (false, 12);
640         hbox.pack_start (vbox_left, false, false, 0);
641         hbox.pack_start (vbox_right, false, false, 0);
642
643         var main_vbox = new VBox (false, 0);
644         main_vbox.set_border_width (12);
645         var menubar = this.ui_manager.get_widget ("/MenuBar");
646         main_vbox.pack_start (menubar, false, false, 0);
647         main_vbox.pack_start (hbox, true, true, 0);
648         add (main_vbox);
649
650         main_vbox.show_all();
651         this.vbox_right.hide ();
652     }
653
654     private void set_atk_name_description (Widget widget, string name, string description)
655     {
656        var atk_widget = widget.get_accessible ();
657
658        atk_widget.set_name (name);
659        atk_widget.set_description (description);
660     }
661
662     private void connect_signals()
663     {
664         this.destroy.connect (Gtk.main_quit);
665     }
666
667     private void init_ipc_server ()
668     {
669 #if IPC_MSRPC
670         /* Errors will currently be sent via g_log - ie. to an
671          * obtrusive message box, on Windows
672          */
673         this.ipc_server = MoonshotServer.get_instance ();
674         MoonshotServer.start (this);
675 #else
676         try {
677             var conn = DBus.Bus.get (DBus.BusType.SESSION);
678             dynamic DBus.Object bus = conn.get_object ("org.freedesktop.DBus",
679                                                        "/org/freedesktop/DBus",
680                                                        "org.freedesktop.DBus");
681
682             // try to register service in session bus
683             uint reply = bus.request_name ("org.janet.Moonshot", (uint) 0);
684             assert (reply == DBus.RequestNameReply.PRIMARY_OWNER);
685
686             this.ipc_server = new MoonshotServer (this);
687             conn.register_object ("/org/janet/moonshot", ipc_server);
688
689         }
690         catch (DBus.Error e)
691         {
692             stderr.printf ("%s\n", e.message);
693         }
694 #endif
695     }
696
697     public static int main(string[] args)
698     {
699         Gtk.init(ref args);
700
701 #if OS_WIN32
702         // Force specific theme settings on Windows without requiring a gtkrc file
703         Gtk.Settings settings = Gtk.Settings.get_default ();
704         settings.set_string_property ("gtk-theme-name", "ms-windows", "moonshot");
705         settings.set_long_property ("gtk-menu-images", 0, "moonshot");
706 #endif
707
708         Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
709         Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8");
710         Intl.textdomain (Config.GETTEXT_PACKAGE);
711
712         var window = new MainWindow();
713         window.show ();
714
715         Gtk.main();
716
717         return 0;
718     }
719 }