2
* Copyright (C) 2011 Canonical Ltd
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License version 3 as
6
* published by the Free Software Foundation.
8
* This program is distributed in the hope that it will be useful,
9
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
* GNU General Public License for more details.
13
* You should have received a copy of the GNU General Public License
14
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16
* Authored by: Robert Ancell <robert.ancell@canonical.com>
19
private class UserEntry
25
/* Label to display */
28
public string? background_filename;
29
public Cairo.Pattern background_pattern;
32
public double target_y;
33
public double direction;
36
public bool is_active;
40
get { return y != target_y; }
44
private class SessionMenuItem : Gtk.RadioMenuItem
46
public string session_name;
49
public class UserList : Gtk.Container
51
public string default_background = "#000000";
53
private int grid_size = 42;
54
private int grid_x_offset;
55
private int grid_y_offset;
56
private int box_width = 7;
58
private List<UserEntry> entries = null;
59
private UserEntry? selected_entry = null;
60
private UserEntry? old_selected_entry = null;
62
private uint scroll_animate_timer = 0;
64
private uint background_animate_timer = 0;
65
private double background_alpha;
67
private List<Gtk.Widget> children;
69
private Gtk.MenuBar menubar;
70
private GLib.List<Indicator.Object> indicators = null;
72
private string? error;
73
private string? message;
75
private Cairo.FontFace font_face;
76
private Cairo.ImageSurface logo_surface;
77
private Cairo.Pattern logo_pattern;
79
private Gtk.Entry prompt_entry;
80
private Gtk.Button login_button;
81
private Gtk.Button options_button;
82
private Gtk.Menu options_menu;
83
unowned GLib.SList<SessionMenuItem> session_group = null;
85
private bool complete = false;
87
public signal void user_selected (string? username);
88
public signal void respond_to_prompt (string text);
89
public signal void start_session ();
91
public string? selected
93
get { if (selected_entry == null) return null; return selected_entry.name; }
96
public string? session
100
foreach (var item in session_group)
103
return item.session_name;
109
foreach (var item in session_group)
111
if (item.session_name == value)
123
background_alpha = 1.0;
125
FreeType.Library library;
126
FreeType.init (out library);
128
FreeType.new_face (library, "/usr/share/fonts/truetype/ubuntu-font-family/Ubuntu-R.ttf", 0, out face);
129
font_face = Cairo.ft_font_face_create_for_ft_face (face, 0);
131
logo_surface = new Cairo.ImageSurface.from_png (Path.build_filename (Config.PKGDATADIR, "logo.png", null));
132
logo_pattern = new Cairo.Pattern.for_surface (logo_surface);
134
menubar = new Gtk.MenuBar ();
136
menubar.draw.connect_after (menubar_draw_cb);
137
menubar.pack_direction = Gtk.PackDirection.RTL;
141
var i = new Gtk.MenuItem.with_label (Posix.utsname ().nodename);
142
i.right_justified = true;
146
prompt_entry = new Gtk.Entry ();
147
prompt_entry.invisible_char = '✻';
148
prompt_entry.has_frame = false;
149
var b = Gtk.Border ();
154
prompt_entry.set_inner_border (b);
155
prompt_entry.activate.connect (prompt_entry_activate_cb);
158
login_button = new Gtk.Button ();
159
var label = new Gtk.Label ("<span font_size=\"large\">" + _("Login") + "</span>");
160
label.use_markup = true;
162
login_button.add (label);
163
login_button.clicked.connect (login_button_clicked_cb);
166
options_button = new Gtk.Button ();
167
var image = new Gtk.Image.from_file (Path.build_filename (Config.PKGDATADIR, "cog.png", null));
169
options_button.relief = Gtk.ReliefStyle.NONE;
170
options_button.add (image);
171
options_button.clicked.connect (options_button_clicked_cb);
172
add (options_button);
174
options_menu = new Gtk.Menu ();
179
private Cairo.Context menubar_cairo_context;
181
private void draw_child_cb (Gtk.Widget child)
183
menubar.propagate_draw (child, menubar_cairo_context);
186
private bool menubar_draw_cb (Cairo.Context c)
190
menubar_cairo_context = c;
191
menubar.forall (draw_child_cb);
196
async void greeter_set_env (string key, string val)
198
GLib.Environment.set_variable (key, val, true);
200
/* And also set it in the DBus activation environment so that any
201
* indicator services pick it up. */
204
var proxy = new GLib.DBusProxy.for_bus_sync (GLib.BusType.SESSION,
205
GLib.DBusProxyFlags.NONE, null,
206
"org.freedesktop.DBus",
207
"/org/freedesktop/DBus",
208
"org.freedesktop.DBus",
211
var builder = new GLib.VariantBuilder (GLib.VariantType.ARRAY);
212
builder.add ("{ss}", key, val);
214
yield proxy.call ("UpdateActivationEnvironment",
215
new GLib.Variant ("(a{ss})", builder),
216
GLib.DBusCallFlags.NONE, -1, null);
220
warning ("Could not get set environment for indicators: %s", e.message);
225
async void setup_indicators ()
227
greeter_set_env ("INDICATOR_GREETER_MODE", "1"); // reduced functionality
228
greeter_set_env ("GIO_USE_VFS", "local"); // no gvfsd
229
greeter_set_env ("RUNNING_UNDER_GDM", "1"); // for gnome-settings-daemon
231
load_indicator ("/usr/lib/indicators3/6/libsession.so");
232
load_indicator ("/usr/lib/indicators3/6/libdatetime.so");
233
load_indicator ("/usr/lib/indicators3/6/libpower.so");
234
load_indicator ("/usr/lib/indicators3/6/libsoundmenu.so");
237
public void show_prompt (string text, bool secret = false)
239
login_button.hide ();
241
prompt_entry.text = "";
242
prompt_entry.show ();
243
prompt_entry.visibility = !secret;
244
prompt_entry.grab_focus ();
248
public void show_authenticated ()
250
prompt_entry.hide ();
252
login_button.show ();
253
login_button.grab_focus ();
257
public void login_complete ()
263
message = _("Logging in...");
265
login_button.hide ();
266
prompt_entry.hide ();
271
public void set_error (string? text)
277
private void load_indicator (string filename)
279
var io = new Indicator.Object.from_file (filename);
280
indicators.append (io);
281
io.entry_added.connect (indicator_added_cb);
282
io.entry_removed.connect (indicator_removed_cb);
283
foreach (var entry in io.get_entries ())
284
indicator_added_cb (entry);
287
private void indicator_added_cb (Indicator.ObjectEntry entry)
289
var menuitem = new Gtk.MenuItem ();
292
var hbox = new Gtk.HBox (false, 3);
296
if (entry.image != null)
297
hbox.pack_start (entry.image, false, false, 0);
298
if (entry.label != null)
299
hbox.pack_start (entry.label, false, false, 0);
300
if (entry.menu != null)
301
menuitem.submenu = entry.menu;
303
menubar.insert (menuitem, (int) menubar.get_children ().length () - 1);
306
private void indicator_removed_cb (Indicator.ObjectEntry entry)
311
/* Number of entries in the list */
312
private uint n_entries
314
get { return entries.length (); }
317
/* Half above the line, rounding down */
320
get { return (n_entries - 1) / 2; }
323
/* Half below the line, rounding up */
326
get { return n_entries / 2; }
329
/* Box in the middle taking up three rows */
330
private int box_height = 3;
332
/* Total height of the box and entries */
333
private new int height
335
get { return ((int) n_entries - 1) + box_height; }
338
public void add_session (string name, string label)
340
var item = new SessionMenuItem ();
341
item.set_group (session_group);
342
item.session_name = name;
345
options_menu.append (item);
346
session_group = (GLib.SList<SessionMenuItem>) item.get_group ();
349
public void add_entry (string? name, string label, string? background = null, bool is_active = false)
351
var e = new UserEntry ();
354
e.background_filename = background;
355
e.is_active = is_active;
357
e.index = (int) n_entries;
360
if (selected_entry == null)
363
foreach (var entry in entries)
365
update_entry_location (entry);
366
entry.y = entry.target_y;
372
private void prompt_entry_activate_cb ()
374
respond_to_prompt (prompt_entry.text);
375
prompt_entry.text = "";
378
private void login_button_clicked_cb ()
380
debug ("login %s", selected_entry.name);
384
private void options_menu_position_cb (Gtk.Menu menu, out int x, out int y, out bool push_in)
386
Gtk.Allocation button_allocation;
387
options_button.get_allocation (out button_allocation);
389
get_window ().get_origin (out x, out y);
390
x += button_allocation.x;
391
y += button_allocation.y + button_allocation.height;
395
private void options_button_clicked_cb ()
397
options_menu.popup (null, null, options_menu_position_cb, 0, Gtk.get_current_event_time ());
400
private void update_entry_location (UserEntry entry)
402
/* Get the number of steps below the selected item */
403
var offset = entry.index - selected_entry.index;
405
/* If above then place before box */
406
if (offset > (int) n_below)
407
offset -= (int) n_entries;
408
/* If below then place after box */
409
else if (offset < - (int) n_above)
410
offset += (int) n_entries;
412
entry.target_y = offset;
414
/* Move towards new location */
415
if (entry.target_y >= entry.y)
416
entry.direction = 1.0;
418
entry.direction = -1.0;
421
private bool scroll_animate_cb ()
423
var animating = false;
424
foreach (var entry in entries)
426
if (entry.y != entry.target_y)
430
var step = entry.target_y - entry.y;
431
if (entry.direction < 0)
436
/* If close enough finish moving */
438
entry.y = entry.target_y;
441
entry.y += speed * entry.direction;
444
if (entry.direction < 0 && entry.y < - (int) n_above)
445
entry.y += n_entries;
446
if (entry.direction > 0 && entry.y >= n_below)
447
entry.y -= n_entries;
456
// FIXME: Should just redraw box
459
/* Stop when we get there */
462
scroll_animate_timer = 0;
463
debug ("stop scroll animation");
470
private bool background_animate_cb ()
472
background_alpha += 0.05;
473
if (background_alpha > 1.0)
474
background_alpha = 1.0;
478
/* Stop when we get there */
479
if (background_alpha == 1.0)
481
background_animate_timer = 0;
482
debug ("stop background animation");
489
private void select_entry (UserEntry entry)
491
debug ("select %s", entry.name);
493
/* Roll everything in the same direction */
495
if (selected_entry != null && entry.y > selected_entry.y)
498
old_selected_entry = selected_entry;
499
selected_entry = entry;
501
prompt_entry.hide ();
502
login_button.hide ();
504
user_selected (selected_entry.name);
506
if (old_selected_entry != null)
508
background_alpha = 0.0;
509
if (background_animate_timer == 0)
511
debug ("start background animation");
512
background_animate_timer = Timeout.add (10, background_animate_cb);
517
foreach (var e in entries)
519
update_entry_location (e);
520
e.direction = direction;
522
/* Move straight there if haven't selected anything previously */
523
if (old_selected_entry == null)
530
if (animate && scroll_animate_timer == 0)
532
debug ("start scroll animation");
533
scroll_animate_timer = Timeout.add (10, scroll_animate_cb);
537
private UserEntry? get_entry_by_index (int index)
539
foreach (var entry in entries)
541
if (entry.index == index)
548
private void select_prev_entry ()
550
var index = selected_entry.index - 1;
552
index += (int) n_entries;
553
select_entry (get_entry_by_index (index));
556
private void select_next_entry ()
558
var index = selected_entry.index + 1;
559
if (index >= (int) n_entries)
560
index -= (int) n_entries;
561
select_entry (get_entry_by_index (index));
564
private void get_selected_location (out int x, out int y)
566
x = grid_x_offset + grid_size;
567
y = grid_y_offset + (get_allocated_height () - box_height * grid_size) / 2;
570
public override void add (Gtk.Widget widget)
572
children.append (widget);
574
widget.set_parent_window (get_window ());
575
widget.set_parent (this);
578
public override void remove (Gtk.Widget widget)
581
children.remove (widget);
584
public override void forall_internal (bool include_internal, Gtk.Callback callback)
586
foreach (var child in children)
590
public override void realize ()
594
Gtk.Allocation allocation;
595
get_allocation (out allocation);
597
var attributes = Gdk.WindowAttr ();
598
attributes.window_type = Gdk.WindowType.CHILD;
599
attributes.x = allocation.x;
600
attributes.y = allocation.y;
601
attributes.width = allocation.width;
602
attributes.height = allocation.height;
603
attributes.wclass = Gdk.WindowWindowClass.OUTPUT;
604
attributes.visual = get_visual ();
605
attributes.event_mask = Gdk.EventMask.EXPOSURE_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.KEY_PRESS_MASK;
606
int attributes_mask = Gdk.WindowAttributesType.X | Gdk.WindowAttributesType.Y | Gdk.WindowAttributesType.VISUAL;
608
var window = new Gdk.Window (get_parent_window (), attributes, attributes_mask);
610
window.set_user_data (this);
612
foreach (var child in children)
613
child.set_parent_window (get_window ());
616
public override void map ()
620
foreach (var child in children)
621
if (child.visible && !child.get_mapped ())
624
get_window ().show ();
627
public override void size_allocate (Gtk.Allocation allocation)
629
var resized = allocation.height != get_allocated_height () || allocation.width != get_allocated_width ();
631
set_allocation (allocation);
634
get_window ().move_resize (allocation.x, allocation.y, allocation.width, allocation.height);
636
grid_x_offset = (int) (allocation.width % grid_size) / 2;
637
grid_y_offset = (int) (allocation.height % grid_size) / 2;
639
Gtk.Requisition natural_size;
640
menubar.get_preferred_size (null, out natural_size);
641
var child_allocation = Gtk.Allocation ();
642
natural_size.height = 32;
643
natural_size.width = get_allocated_width ();
644
child_allocation.x = 0;
645
child_allocation.y = 0;
646
child_allocation.width = natural_size.width;
647
child_allocation.height = natural_size.height;
648
menubar.size_allocate (child_allocation);
650
/* Put prompt entry and login button inside login box */
652
get_selected_location (out base_x, out base_y);
653
child_allocation.x = base_x + grid_size / 2;
654
child_allocation.y = base_y + grid_size * 2 - grid_size / 2;
655
child_allocation.width = grid_size * (box_width - 1);
656
child_allocation.height = grid_size;
657
prompt_entry.size_allocate (child_allocation);
658
login_button.size_allocate (child_allocation);
660
child_allocation.x = base_x + box_width * grid_size - grid_size - grid_size / 4;
661
child_allocation.y = base_y + grid_size / 4;
662
child_allocation.width = grid_size;
663
child_allocation.height = grid_size;
664
options_button.size_allocate (child_allocation);
665
options_button.show ();
667
/* Regenerate backgrounds */
670
foreach (var entry in entries)
671
entry.background_pattern = null;
675
private void draw_entries (Cairo.Context c)
677
foreach (var entry in entries)
680
c.translate (0, entry.y * grid_size);
684
c.move_to (8, grid_size / 2 + 0.5 - 4);
685
c.rel_line_to (5, 4);
686
c.rel_line_to (-5, 4);
688
c.set_source_rgba (1.0, 1.0, 1.0, 0.5);
692
c.set_font_size (0.5 * grid_size);
693
Cairo.TextExtents extents;
694
c.text_extents (entry.label, out extents);
695
c.move_to (grid_size / 2, grid_size - (grid_size - (extents.height)) / 2);
696
c.set_source_rgba (1.0, 1.0, 1.0, 0.5);
697
c.show_text (entry.label);
703
private Cairo.Pattern make_background (string? filename)
705
if (filename == null)
706
filename = default_background;
708
debug ("making background %s at %dx%d", filename, get_allocated_width (), get_allocated_height ());
711
if (Gdk.Color.parse (filename, out color))
713
return new Cairo.Pattern.rgb (color.red / 65535.0, color.green / 65535.0, color.blue / 65535.0);
716
Gdk.Pixbuf orig_image;
719
orig_image = new Gdk.Pixbuf.from_file (filename);
723
debug ("Error loading background: %s", e.message);
724
return new Cairo.Pattern.rgb (0, 0, 0);
727
var target_aspect = (double) get_allocated_width () / get_allocated_height ();
728
var aspect = (double) orig_image.width / orig_image.height;
729
double scale, offset_x = 0, offset_y = 0;
730
if (aspect > target_aspect)
732
/* Fit height and trim sides */
733
scale = (double) get_allocated_height () / orig_image.height;
734
offset_x = (orig_image.width * scale - get_allocated_width ()) / 2;
738
/* Fit width and trim top and bottom */
739
scale = (double) get_allocated_width () / orig_image.width;
740
offset_y = (orig_image.height * scale - get_allocated_height ()) / 2;
743
var image = new Gdk.Pixbuf (orig_image.colorspace, orig_image.has_alpha, orig_image.bits_per_sample, get_allocated_width (), get_allocated_height ());
744
orig_image.scale (image, 0, 0, get_allocated_width (), get_allocated_height (), -offset_x, -offset_y, scale, scale, Gdk.InterpType.BILINEAR);
747
var overlay_surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, grid_size, grid_size);
748
var oc = new Cairo.Context (overlay_surface);
749
oc.rectangle (0, 0, 1, 1);
750
oc.rectangle (grid_size - 1, 0, 1, 1);
751
oc.rectangle (0, grid_size - 1, 1, 1);
752
oc.rectangle (grid_size - 1, grid_size - 1, 1, 1);
753
oc.set_source_rgba (1.0, 1.0, 1.0, 0.25);
755
var overlay = new Cairo.Pattern.for_surface (overlay_surface);
756
var matrix = Cairo.Matrix.identity ();
757
matrix.translate (-grid_x_offset, -grid_y_offset);
758
overlay.set_matrix (matrix);
759
overlay.set_extend (Cairo.Extend.REPEAT);
761
/* Create background */
762
var surface = new Cairo.ImageSurface (Cairo.Format.RGB24, get_allocated_width (), get_allocated_height ());
763
var bc = new Cairo.Context (surface);
764
Gdk.cairo_set_source_pixbuf (bc, image, 0, 0);
768
bc.set_source (overlay);
769
bc.rectangle (grid_size - 1, grid_size - 1, get_allocated_width () - grid_size * 2 + 2, get_allocated_height () - grid_size * 2 + 2);
772
/* Mask out dots under logo */
773
bc.rectangle (0, get_allocated_height () - logo_surface.get_height (), logo_surface.get_width (), logo_surface.get_height ());
774
Gdk.cairo_set_source_pixbuf (bc, image, 0, 0);
777
var pattern = new Cairo.Pattern.for_surface (surface);
778
pattern.set_extend (Cairo.Extend.REPEAT);
783
public Cairo.Pattern get_background ()
785
return get_background_for_user (selected_entry);
788
private Cairo.Pattern get_background_for_user (UserEntry? entry)
791
return new Cairo.Pattern.rgb (0, 0, 0);
793
if (entry.background_pattern == null)
794
entry.background_pattern = make_background (entry.background_filename);
795
return entry.background_pattern;
798
private void draw_background (Cairo.Context c)
800
if (background_alpha == 1.0)
802
c.set_source (get_background_for_user (selected_entry));
807
/* Draw old background */
808
c.set_source (get_background_for_user (old_selected_entry));
811
/* Draw new background */
812
c.set_source (get_background_for_user (selected_entry));
813
c.paint_with_alpha (background_alpha);
817
public override bool draw (Cairo.Context c)
821
c.set_font_face (font_face);
825
c.translate (0, get_allocated_height () - logo_surface.get_height ());
826
c.set_source (logo_pattern);
827
c.rectangle (0, 0, logo_surface.get_width (), logo_surface.get_height ());
832
get_selected_location (out base_x, out base_y);
835
c.translate (base_x, base_y);
837
/* Draw entries above the box */
839
c.rectangle (0, -n_above * grid_size, box_width * grid_size, n_above * grid_size);
842
c.translate (0, -n_entries * grid_size);
846
/* Draw entries below the box */
848
c.rectangle (0, box_height * grid_size, box_width * grid_size, n_below * grid_size);
850
c.translate (0, (box_height - 1) * grid_size);
852
c.translate (0, n_entries * grid_size);
858
var box_w = box_width * grid_size - border * 2;
859
var box_h = box_height * grid_size - border * 2;
860
var box_r = 0.2 * grid_size;
861
cairo_rounded_rectangle (c,
862
border + 0.5, border + 0.5,
863
box_w - 1, box_h - 1,
865
c.set_source_rgba (0.0, 0.0, 0.0, 0.4);
867
c.set_line_width (1.0);
868
c.set_source_rgba (1.0, 1.0, 1.0, 0.25);
871
cairo_rounded_rectangle (c,
872
border + 0.5 + 2, border + 0.5 + 2,
873
box_w - 5, box_h - 5,
878
if (selected_entry != null)
880
Cairo.TextExtents extents;
881
c.set_font_size (0.5 * grid_size);
882
c.text_extents (selected_entry.label, out extents);
883
var text_y = grid_size - (grid_size - border - extents.height) / 4;
885
if (selected_entry.is_active)
887
c.move_to (8, text_y - extents.height / 2 + 0.5 - 4);
888
c.rel_line_to (5, 4);
889
c.rel_line_to (-5, 4);
891
c.set_source_rgb (1.0, 1.0, 1.0);
894
c.move_to (grid_size / 2, text_y);
895
c.set_source_rgb (1.0, 1.0, 1.0);
896
c.show_text (selected_entry.label);
899
if (error != null || message != null)
901
Cairo.TextExtents extents;
909
c.set_font_size (0.3 * grid_size);
910
c.text_extents (text, out extents);
911
c.move_to (grid_size / 2, grid_size * 1.25);
913
c.set_source_rgb (1.0, 0.0, 0.0);
915
c.set_source_rgb (1.0, 1.0, 1.0);
921
foreach (var child in children)
922
propagate_draw (child, c);
927
private void cairo_rounded_rectangle (Cairo.Context c, double x, double y, double width, double height, double radius)
929
var w = width - radius * 2;
930
var h = height - radius * 2;
931
var kappa = 0.5522847498 * radius;
932
c.move_to (x + radius, y);
933
c.rel_line_to (w, 0);
934
c.rel_curve_to (kappa, 0, radius, radius - kappa, radius, radius);
935
c.rel_line_to (0, h);
936
c.rel_curve_to (0, kappa, kappa - radius, radius, -radius, radius);
937
c.rel_line_to (-w, 0);
938
c.rel_curve_to (-kappa, 0, -radius, kappa - radius, -radius, -radius);
939
c.rel_line_to (0, -h);
940
c.rel_curve_to (0, -kappa, radius - kappa, -radius, radius, -radius);
943
// FIXME: Don't seem to be defined in Vala
944
private const uint KEY_Up = 0xff52;
945
private const uint KEY_Down = 0xff54;
947
public override bool key_press_event (Gdk.EventKey event)
949
switch (event.keyval)
952
select_prev_entry ();
955
select_next_entry ();
964
public override bool button_release_event (Gdk.EventButton event)
967
get_selected_location (out base_x, out base_y);
968
foreach (var entry in entries)
974
if (event.x >= base_x &&
975
event.x <= base_x + entry.width * grid_size &&
976
event.y >= base_y + y * grid_size &&
977
event.y <= base_y + (y + 1) * grid_size)
979
select_entry (entry);