diff options
author | Mike Gabriel <mike.gabriel@das-netzwerkteam.de> | 2014-11-02 20:40:40 +0100 |
---|---|---|
committer | Mike Gabriel <mike.gabriel@das-netzwerkteam.de> | 2014-11-02 20:40:40 +0100 |
commit | 8e2d33d3a5d17531a36e629bcf4c0bc36cbf1cfe (patch) | |
tree | 5def29ecbadc8873d62ebea6badc27c99b874bfe /src | |
download | arctica-greeter-8e2d33d3a5d17531a36e629bcf4c0bc36cbf1cfe.tar.gz arctica-greeter-8e2d33d3a5d17531a36e629bcf4c0bc36cbf1cfe.tar.bz2 arctica-greeter-8e2d33d3a5d17531a36e629bcf4c0bc36cbf1cfe.zip |
Imported Upstream version 14.04.10
Diffstat (limited to 'src')
32 files changed, 8832 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..b890ee0 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,76 @@ +# -*- Mode: Automake; indent-tabs-mode: t; tab-width: 4 -*- + +sbin_PROGRAMS = unity-greeter +noinst_PROGRAMS = logo-generator + +unity_greeter_SOURCES = \ + config.vapi \ + fixes.vapi \ + indicator.vapi \ + animate-timer.vala \ + background.vala \ + cached-image.vala \ + cairo-utils.vala \ + email-autocompleter.vala \ + dash-box.vala \ + dash-button.vala \ + dash-entry.vala \ + fadable.vala \ + fadable-box.vala \ + fading-label.vala \ + flat-button.vala \ + greeter-list.vala \ + list-stack.vala \ + main-window.vala \ + menu.vala \ + menubar.vala \ + prompt-box.vala \ + session-list.vala \ + remote-login-service.vala \ + settings.vala \ + settings-daemon.vala \ + shutdown-dialog.vala \ + toggle-box.vala \ + unity-greeter.vala \ + user-list.vala \ + user-prompt-box.vala + +logo_generator_SOURCES = logo-generator.vala + +unity_greeter_CFLAGS = \ + $(UNITY_GREETER_CFLAGS) \ + -w \ + -DGNOME_DESKTOP_USE_UNSTABLE_API \ + -DGETTEXT_PACKAGE=\"$(GETTEXT_PACKAGE)\" \ + -DLOCALEDIR=\""$(localedir)"\" \ + -DVERSION=\"$(VERSION)\" \ + -DCONFIG_FILE=\""$(sysconfdir)/lightdm/unity-greeter.conf"\" \ + -DPKGDATADIR=\""$(pkgdatadir)"\" \ + -DINDICATORDIR=\""$(INDICATORDIR)"\" + +logo_generator_CFLAGS = $(unity_greeter_CFLAGS) + +unity_greeter_VALAFLAGS = \ + --pkg posix \ + --pkg gtk+-3.0 \ + --pkg gdk-x11-3.0 \ + --pkg gio-unix-2.0 \ + --pkg x11 \ + --pkg liblightdm-gobject-1 \ + --pkg libcanberra \ + --pkg gio-2.0 \ + --pkg pixman-1 \ + --target-glib 2.32 + +logo_generator_VALAFLAGS = $(unity_greeter_VALAFLAGS) + +unity_greeter_LDADD = \ + $(UNITY_GREETER_LIBS) \ + -lm + +logo_generator_LDADD = $(unity_greeter_LDADD) + +unity_greeter_vala.stamp: $(top_srcdir)/config.h + +DISTCLEANFILES = \ + Makefile.in diff --git a/src/animate-timer.vala b/src/animate-timer.vala new file mode 100644 index 0000000..d5aaf71 --- /dev/null +++ b/src/animate-timer.vala @@ -0,0 +1,154 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011,2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Robert Ancell <robert.ancell@canonical.com> + * Michael Terry <michael.terry@canonical.com> + */ + +private class AnimateTimer : Object +{ + /* x and y are 0.0 to 1.0 */ + public delegate double EasingFunc (double x); + + /* The following are the same intervals that Unity uses */ + public static const int INSTANT = 150; /* Good for animations that don't convey any information */ + public static const int FAST = 250; /* Good for animations that convey duplicated information */ + public static const int NORMAL = 500; + public static const int SLOW = 1000; /* Good for animations that convey information that is only presented in the animation */ + + /* speed is in milliseconds */ + public unowned EasingFunc easing_func { get; private set; } + public int speed { get; set; } + public bool is_running { get { return timeout != 0; } } + public double progress { get; private set; } + + /* progress is from 0.0 to 1.0 */ + public signal void animate (double progress); + + /* AnimateTimer requires two things: an easing function and a speed. + + The speed is just the duration of the animation in milliseconds. + + The easing function describes how fast the animation occurs at different + parts of the duration. + + See http://hosted.zeh.com.br/tweener/docs/en-us/misc/transitions.html + for examples of various easing functions. + + A few are provided with this class, notably ease_in_out and + ease_out_quint. + */ + /* speed is in milliseconds */ + public AnimateTimer (EasingFunc func, int speed) + { + Object (speed: speed); + this.easing_func = func; + } + + ~AnimateTimer () + { + stop (); + } + + /* temp_speed is in milliseconds */ + public void reset (int temp_speed = -1) + { + stop (); + + timeout = Timeout.add (16, animate_cb); + progress = 0; + start_time = 0; + extra_time = 0; + extra_progress = 0; + + if (temp_speed == -1) + temp_speed = speed; + + length = temp_speed * TimeSpan.MILLISECOND; + } + + public void stop () + { + if (timeout != 0) + Source.remove (timeout); + timeout = 0; + } + + private uint timeout = 0; + private TimeSpan start_time = 0; + private TimeSpan length = 0; + private TimeSpan extra_time = 0; + private double extra_progress = 0.0; + + private bool animate_cb () + { + if (start_time == 0) + start_time = GLib.get_monotonic_time (); + + var time_progress = normalize_time (GLib.get_monotonic_time ()); + progress = calculate_progress (time_progress); + animate (progress); + + if (time_progress >= 1.0) + { + timeout = 0; + return false; + } + else + return true; + } + + /* Returns 0.0 to 1.0 where 1.0 is at or past end_time */ + private double normalize_time (TimeSpan now) + { + if (length == 0) + return 1.0f; + + return (((double)(now - start_time)) / length).clamp (0.0, 1.0); + } + + /* Returns 0.0 to 1.0 where 1.0 is done. + time is not normalized yet! */ + private double calculate_progress (double time_progress) + { + var y = easing_func (time_progress); + return y.clamp (0.0, 1.0); + } + + public static double ease_in_out (double x) + { + return (1 - Math.cos (Math.PI * x)) / 2; + } + + /*public static double ease_in_quad (double x) + { + return Math.pow (x, 2); + }*/ + /*public static double ease_out_quad (double x) + { + return -1 * Math.pow (x - 1, 2) + 1; + }*/ + + /*public static double ease_in_quint (double x) + { + return Math.pow (x, 5); + }*/ + public static double ease_out_quint (double x) + { + return Math.pow (x - 1, 5) + 1; + } +} + diff --git a/src/background.vala b/src/background.vala new file mode 100644 index 0000000..a1c28e9 --- /dev/null +++ b/src/background.vala @@ -0,0 +1,705 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011,2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Robert Ancell <robert.ancell@canonical.com> + * Michael Terry <michael.terry@canonical.com> + */ + +class BackgroundLoader : Object +{ + public string filename { get; private set; } + public Cairo.Surface logo { get; set; } + + public int[] widths; + public int[] heights; + public Cairo.Pattern[] patterns; + public Gdk.RGBA average_color; + + private Cairo.Surface target_surface; + private bool draw_grid; + private Thread<void*> thread; + private Gdk.Pixbuf[] images; + private bool finished; + private uint ready_id; + + public signal void loaded (); + + public BackgroundLoader (Cairo.Surface target_surface, string filename, int[] widths, int[] heights, bool draw_grid) + { + this.target_surface = target_surface; + this.filename = filename; + this.widths = widths; + this.heights = heights; + patterns = new Cairo.Pattern[widths.length]; + images = new Gdk.Pixbuf[widths.length]; + this.draw_grid = draw_grid; + } + + public bool load () + { + /* Already loaded */ + if (finished) + return true; + + /* Currently loading */ + if (thread != null) + return false; + + /* No monitor data */ + if (widths.length == 0) + return false; + + var text = "Making background %s at %dx%d".printf (filename, widths[0], heights[0]); + for (var i = 1; i < widths.length; i++) + text += ",%dx%d".printf (widths[i], heights[i]); + debug (text); + + var color = Gdk.RGBA (); + if (color.parse (filename)) + { + var pattern = new Cairo.Pattern.rgba (color.red, color.green, color.blue, color.alpha); + for (var i = 0; i < widths.length; i++) + patterns[i] = pattern; + + average_color = color; + finished = true; + debug ("Render of background %s complete", filename); + return true; + } + else + { + try + { + this.ref (); + thread = new Thread<void*>.try ("background-loader", load_and_scale); + } + catch (Error e) + { + this.unref (); + finished = true; + return true; + } + } + + return false; + } + + public Cairo.Pattern? get_pattern (int width, int height) + { + for (var i = 0; i < widths.length; i++) + { + if (widths[i] == width && heights[i] == height) + return patterns[i]; + } + return null; + } + + ~BackgroundLoader () + { + if (ready_id > 0) + Source.remove (ready_id); + ready_id = 0; + } + + private bool ready_cb () + { + ready_id = 0; + + debug ("Render of background %s complete", filename); + + thread.join (); + thread = null; + finished = true; + + for (var i = 0; i < widths.length; i++) + { + if (images[i] != null) + { + patterns[i] = create_pattern (images[i]); + if (i == 0) + pixbuf_average_value (images[i], out average_color); + images[i] = null; + } + else + { + debug ("images[%d] was null for %s", i, filename); + patterns[i] = null; + } + } + + loaded (); + + this.unref (); + return false; + } + + private void* load_and_scale () + { + try + { + var image = new Gdk.Pixbuf.from_file (filename); + for (var i = 0; i < widths.length; i++) + images[i] = scale (image, widths[i], heights[i]); + } + catch (Error e) + { + debug ("Error loading background: %s", e.message); + } + + ready_id = Gdk.threads_add_idle (ready_cb); + + return null; + } + + private Gdk.Pixbuf? scale (Gdk.Pixbuf? image, int width, int height) + { + var target_aspect = (double) width / height; + var aspect = (double) image.width / image.height; + double scale, offset_x = 0, offset_y = 0; + if (aspect > target_aspect) + { + /* Fit height and trim sides */ + scale = (double) height / image.height; + offset_x = (image.width * scale - width) / 2; + } + else + { + /* Fit width and trim top and bottom */ + scale = (double) width / image.width; + offset_y = (image.height * scale - height) / 2; + } + + var scaled_image = new Gdk.Pixbuf (image.colorspace, image.has_alpha, image.bits_per_sample, width, height); + image.scale (scaled_image, 0, 0, width, height, -offset_x, -offset_y, scale, scale, Gdk.InterpType.BILINEAR); + + return scaled_image; + } + + private Cairo.Pattern? create_pattern (Gdk.Pixbuf image) + { + var grid_x_offset = get_grid_offset (image.width); + var grid_y_offset = get_grid_offset (image.height); + + /* Create background */ + var surface = new Cairo.Surface.similar (target_surface, Cairo.Content.COLOR, image.width, image.height); + var bc = new Cairo.Context (surface); + Gdk.cairo_set_source_pixbuf (bc, image, 0, 0); + + bc.paint (); + + /* Draw logo */ + if (logo != null) + { + bc.save (); + var y = (int) (image.height / grid_size - 2) * grid_size + grid_y_offset; + bc.translate (grid_x_offset, y); + bc.set_source_surface (logo, 0, 0); + bc.paint_with_alpha (0.5); + bc.restore (); + } + + var pattern = new Cairo.Pattern.for_surface (surface); + pattern.set_extend (Cairo.Extend.REPEAT); + + return pattern; + } + + /* The following color averaging algorithm was originally written for + Unity in C++, then patched into gnome-desktop3 in C. I've taken it + and put it here in Vala. It would be nice if we could get + gnome-desktop3 to expose this for our use instead of copying the + code... */ + + static const int QUAD_MAX_LEVEL_OF_RECURSION = 16; + static const int QUAD_MIN_LEVEL_OF_RECURSION = 2; + static const int QUAD_CORNER_WEIGHT_NW = 3; + static const int QUAD_CORNER_WEIGHT_NE = 1; + static const int QUAD_CORNER_WEIGHT_SE = 1; + static const int QUAD_CORNER_WEIGHT_SW = 3; + static const int QUAD_CORNER_WEIGHT_CENTER = 2; + static const int QUAD_CORNER_WEIGHT_TOTAL = (QUAD_CORNER_WEIGHT_NW + QUAD_CORNER_WEIGHT_NE + QUAD_CORNER_WEIGHT_SE + QUAD_CORNER_WEIGHT_SW + QUAD_CORNER_WEIGHT_CENTER); + + /* Pixbuf utilities */ + private Gdk.RGBA get_pixbuf_sample (uint8[] pixels, + int rowstride, + int channels, + int x, + int y) + { + var sample = Gdk.RGBA (); + double dd = 0xFF; + int offset = ((y * rowstride) + (x * channels)); + + sample.red = pixels[offset++] / dd; + sample.green = pixels[offset++] / dd; + sample.blue = pixels[offset++] / dd; + sample.alpha = 1.0f; + + return sample; + } + + private bool is_color_different (Gdk.RGBA color_a, + Gdk.RGBA color_b) + { + var diff = Gdk.RGBA (); + + diff.red = color_a.red - color_b.red; + diff.green = color_a.green - color_b.green; + diff.blue = color_a.blue - color_b.blue; + diff.alpha = 1.0f; + + if (GLib.Math.fabs (diff.red) > 0.15 || + GLib.Math.fabs (diff.green) > 0.15 || + GLib.Math.fabs (diff.blue) > 0.15) + return true; + + return false; + } + + private Gdk.RGBA get_quad_average (int x, + int y, + int width, + int height, + int level_of_recursion, + uint8[] pixels, + int rowstride, + int channels) + { + // samples four corners + // c1-----c2 + // | | + // c3-----c4 + + var average = Gdk.RGBA (); + var corner1 = get_pixbuf_sample (pixels, rowstride, channels, x , y ); + var corner2 = get_pixbuf_sample (pixels, rowstride, channels, x + width, y ); + var corner3 = get_pixbuf_sample (pixels, rowstride, channels, x , y + height); + var corner4 = get_pixbuf_sample (pixels, rowstride, channels, x + width, y + height); + var centre = get_pixbuf_sample (pixels, rowstride, channels, x + (width / 2), y + (height / 2)); + + /* If we're over the max we want to just take the average and be happy + with that value */ + if (level_of_recursion < QUAD_MAX_LEVEL_OF_RECURSION) { + /* Otherwise we want to look at each value and check it's distance + from the center color and take the average if they're far apart. */ + + /* corner 1 */ + if (level_of_recursion < QUAD_MIN_LEVEL_OF_RECURSION || + is_color_different(corner1, centre)) { + corner1 = get_quad_average (x, y, width/2, height/2, level_of_recursion + 1, pixels, rowstride, channels); + } + + /* corner 2 */ + if (level_of_recursion < QUAD_MIN_LEVEL_OF_RECURSION || + is_color_different(corner2, centre)) { + corner2 = get_quad_average (x + width/2, y, width/2, height/2, level_of_recursion + 1, pixels, rowstride, channels); + } + + /* corner 3 */ + if (level_of_recursion < QUAD_MIN_LEVEL_OF_RECURSION || + is_color_different(corner3, centre)) { + corner3 = get_quad_average (x, y + height/2, width/2, height/2, level_of_recursion + 1, pixels, rowstride, channels); + } + + /* corner 4 */ + if (level_of_recursion < QUAD_MIN_LEVEL_OF_RECURSION || + is_color_different(corner4, centre)) { + corner4 = get_quad_average (x + width/2, y + height/2, width/2, height/2, level_of_recursion + 1, pixels, rowstride, channels); + } + } + + average.red = ((corner1.red * QUAD_CORNER_WEIGHT_NW) + + (corner3.red * QUAD_CORNER_WEIGHT_SW) + + (centre.red * QUAD_CORNER_WEIGHT_CENTER) + + (corner2.red * QUAD_CORNER_WEIGHT_NE) + + (corner4.red * QUAD_CORNER_WEIGHT_SE)) + / QUAD_CORNER_WEIGHT_TOTAL; + average.green = ((corner1.green * QUAD_CORNER_WEIGHT_NW) + + (corner3.green * QUAD_CORNER_WEIGHT_SW) + + (centre.green * QUAD_CORNER_WEIGHT_CENTER) + + (corner2.green * QUAD_CORNER_WEIGHT_NE) + + (corner4.green * QUAD_CORNER_WEIGHT_SE)) + / QUAD_CORNER_WEIGHT_TOTAL; + average.blue = ((corner1.blue * QUAD_CORNER_WEIGHT_NW) + + (corner3.blue * QUAD_CORNER_WEIGHT_SW) + + (centre.blue * QUAD_CORNER_WEIGHT_CENTER) + + (corner2.blue * QUAD_CORNER_WEIGHT_NE) + + (corner4.blue * QUAD_CORNER_WEIGHT_SE)) + / QUAD_CORNER_WEIGHT_TOTAL; + average.alpha = 1.0f; + + return average; + } + + private void pixbuf_average_value (Gdk.Pixbuf pixbuf, + out Gdk.RGBA result) + { + var average = get_quad_average (0, 0, + pixbuf.get_width () - 1, pixbuf.get_height () - 1, + 1, + pixbuf.get_pixels (), + pixbuf.get_rowstride (), + pixbuf.get_n_channels ()); + + result = Gdk.RGBA (); + result.red = average.red; + result.green = average.green; + result.blue = average.blue; + result.alpha = average.alpha; + } +} + +public class Monitor +{ + public int x; + public int y; + public int width; + public int height; + + public Monitor (int x, int y, int width, int height) + { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + public bool equals (Monitor? other) + { + if (other != null) + return (x == other.x && y == other.y && width == other.width && height == other.height); + + return false; + } +} + +public class Background : Gtk.Fixed +{ + public enum DrawFlags + { + NONE, + GRID, + } + + public string default_background { get; set; default = UGSettings.get_string (UGSettings.KEY_BACKGROUND_COLOR); } + public string? current_background { get; set; default = null; } + public bool draw_grid { get; set; default = true; } + public double alpha { get; private set; default = 1.0; } + public Gdk.RGBA average_color { get { return current.average_color; } } + + private Cairo.Surface target_surface; + + private List<Monitor> monitors = null; + private Monitor? active_monitor = null; + + private AnimateTimer timer; + + private BackgroundLoader current; + private BackgroundLoader old; + + private HashTable<string, BackgroundLoader> loaders; + + private Cairo.Surface? version_logo_surface = null; + private int version_logo_width; + private int version_logo_height; + private Cairo.Surface? background_logo_surface = null; + private int background_logo_width; + private int background_logo_height; + + public Background (Cairo.Surface target_surface) + { + this.target_surface = target_surface; + timer = new AnimateTimer (AnimateTimer.ease_in_out, 700); + timer.animate.connect (animate_cb); + + loaders = new HashTable<string?, BackgroundLoader> (str_hash, str_equal); + + notify["current-background"].connect (() => { reload (); }); + } + + public void set_logo (string version_logo, string background_logo) + { + version_logo_surface = load_image (version_logo, out version_logo_width, out version_logo_height); + background_logo_surface = load_image (background_logo, out background_logo_width, out background_logo_height); + } + + private Cairo.Surface? load_image (string filename, out int width, out int height) + { + width = height = 0; + try + { + var image = new Gdk.Pixbuf.from_file (filename); + width = image.width; + height = image.height; + var surface = new Cairo.Surface.similar (target_surface, Cairo.Content.COLOR_ALPHA, image.width, image.height); + var c = new Cairo.Context (surface); + Gdk.cairo_set_source_pixbuf (c, image, 0, 0); + c.paint (); + return surface; + } + catch (Error e) + { + debug ("Failed to load background component %s: %s", filename, e.message); + } + + return null; + } + + public void set_monitors (List<Monitor> monitors) + { + this.monitors = new List<Monitor> (); + foreach (var m in monitors) + this.monitors.append (m); + queue_draw (); + } + + public void set_active_monitor (Monitor? monitor) + { + active_monitor = monitor; + } + + public override void size_allocate (Gtk.Allocation allocation) + { + var resized = allocation.height != get_allocated_height () || allocation.width != get_allocated_width (); + + base.size_allocate (allocation); + + /* Regenerate backgrounds */ + if (resized) + { + debug ("Regenerating backgrounds"); + loaders.remove_all (); + load_background (null); + reload (); + } + } + + public override bool draw (Cairo.Context c) + { + var flags = DrawFlags.NONE; + if (draw_grid) + flags |= DrawFlags.GRID; + draw_full (c, flags); + return base.draw (c); + } + + public void draw_full (Cairo.Context c, DrawFlags flags) + { + c.save (); + + /* Test whether we ran into an error loading this background */ + if (current == null || (current.load () && current.patterns[0] == null)) + { + /* We couldn't load it, so swap it out for the default background + and remember that choice */ + var new_background = load_background (null); + if (current != null) + loaders.insert (current.filename, new_background); + if (old == current) + old = new_background; + current = new_background; + publish_average_color (); + } + + /* Fade to this background when loaded */ + if (current.load () && current != old && !timer.is_running) + { + alpha = 0.0; + timer.reset (); + } + + c.set_source_rgba (0.0, 0.0, 0.0, 0.0); + var old_painted = false; + + /* Draw old background */ + if (old != null && old.load () && (alpha < 1.0 || !current.load ())) + { + draw_background (c, old, 1.0); + old_painted = true; + } + + /* Draw new background */ + if (current.load () && alpha > 0.0) + draw_background (c, current, old_painted ? alpha : 1.0); + + c.restore (); + + if ((flags & DrawFlags.GRID) != 0) + overlay_grid (c); + } + + private void draw_background (Cairo.Context c, BackgroundLoader background, double alpha) + { + foreach (var monitor in monitors) + { + var pattern = background.get_pattern (monitor.width, monitor.height); + if (pattern == null) + continue; + + c.save (); + pattern = background.get_pattern (monitor.width, monitor.height); + var matrix = Cairo.Matrix.identity (); + matrix.translate (-monitor.x, -monitor.y); + pattern.set_matrix (matrix); + c.set_source (pattern); + c.rectangle (monitor.x, monitor.y, monitor.width, monitor.height); + c.clip (); + c.paint_with_alpha (alpha); + c.restore (); + + if (monitor != active_monitor && background_logo_surface != null) + { + var width = background_logo_width; + var height = background_logo_height; + + c.save (); + pattern = new Cairo.Pattern.for_surface (background_logo_surface); + matrix = Cairo.Matrix.identity (); + var x = monitor.x + (monitor.width - width) / 2; + var y = monitor.y + (monitor.height - height) / 2; + matrix.translate (-x, -y); + pattern.set_matrix (matrix); + c.set_source (pattern); + c.rectangle (x, y, width, height); + c.clip (); + c.paint_with_alpha (alpha); + c.restore (); + } + } + } + + private void animate_cb (double progress) + { + alpha = progress; + queue_draw (); + + /* Stop when we get there */ + if (alpha >= 1.0) + old = current; + } + + private void reload () + { + if (get_realized ()) + { + var new_background = load_background (current_background); + + if (current != new_background) + { + old = current; + current = new_background; + alpha = 1.0; /* if the timer isn't going, we should always be at 1.0 */ + timer.stop (); + } + + queue_draw (); + publish_average_color (); + } + } + + private BackgroundLoader load_background (string? filename) + { + if (filename == null) + filename = default_background; + + var b = loaders.lookup (filename); + if (b == null) + { + /* Load required sizes to draw background */ + var widths = new int[monitors.length ()]; + var heights = new int[monitors.length ()]; + var n_sizes = 0; + foreach (var monitor in monitors) + { + if (monitor_is_unique_size (monitor)) + { + widths[n_sizes] = monitor.width; + heights[n_sizes] = monitor.height; + n_sizes++; + } + } + widths.resize (n_sizes); + heights.resize (n_sizes); + + b = new BackgroundLoader (target_surface, filename, widths, heights, draw_grid); + b.logo = version_logo_surface; + b.loaded.connect (() => { reload (); }); + b.load (); + loaders.insert (filename, b); + } + + return b; + } + + /* Check if a monitor has a unique size */ + private bool monitor_is_unique_size (Monitor monitor) + { + foreach (var m in monitors) + { + if (m == monitor) + break; + else if (m.width == monitor.width && m.height == monitor.height) + return false; + } + + return true; + } + + private void overlay_grid (Cairo.Context c) + { + var width = get_allocated_width (); + var height = get_allocated_height (); + var grid_x_offset = get_grid_offset (width); + var grid_y_offset = get_grid_offset (height); + + /* Overlay grid */ + var overlay_surface = new Cairo.Surface.similar (target_surface, Cairo.Content.COLOR_ALPHA, grid_size, grid_size); + var oc = new Cairo.Context (overlay_surface); + oc.rectangle (0, 0, 1, 1); + oc.rectangle (grid_size - 1, 0, 1, 1); + oc.rectangle (0, grid_size - 1, 1, 1); + oc.rectangle (grid_size - 1, grid_size - 1, 1, 1); + oc.set_source_rgba (1.0, 1.0, 1.0, 0.25); + oc.fill (); + var overlay = new Cairo.Pattern.for_surface (overlay_surface); + var matrix = Cairo.Matrix.identity (); + matrix.translate (-grid_x_offset, -grid_y_offset); + overlay.set_matrix (matrix); + overlay.set_extend (Cairo.Extend.REPEAT); + + /* Draw overlay */ + c.save (); + c.set_source (overlay); + c.rectangle (0, 0, width, height); + c.fill (); + c.restore (); + } + + void publish_average_color () + { + notify_property ("average-color"); + var rgba = current.average_color.to_string (); + var root = get_screen ().get_root_window (); + + Gdk.property_change (root, + Gdk.Atom.intern_static_string ("_GNOME_BACKGROUND_REPRESENTATIVE_COLORS"), + Gdk.Atom.intern_static_string ("STRING"), + 8, + Gdk.PropMode.REPLACE, + rgba.data, + rgba.data.length); + } +} diff --git a/src/cached-image.vala b/src/cached-image.vala new file mode 100644 index 0000000..56157a3 --- /dev/null +++ b/src/cached-image.vala @@ -0,0 +1,59 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Michael Terry <michael.terry@canonical.com> + */ + +public class CachedImage : Gtk.Image +{ + private static HashTable<Gdk.Pixbuf, Cairo.Surface> surface_table; + + public static Cairo.Surface? get_cached_surface (Cairo.Context c, Gdk.Pixbuf pixbuf) + { + if (surface_table == null) + surface_table = new HashTable<Gdk.Pixbuf, Cairo.Surface> (direct_hash, direct_equal); + + var surface = surface_table.lookup (pixbuf); + if (surface == null) + { + surface = new Cairo.Surface.similar (c.get_target (), Cairo.Content.COLOR_ALPHA, pixbuf.width, pixbuf.height); + var new_c = new Cairo.Context (surface); + Gdk.cairo_set_source_pixbuf (new_c, pixbuf, 0, 0); + new_c.paint (); + surface_table.insert (pixbuf, surface); + } + return surface; + } + + public CachedImage (Gdk.Pixbuf? pixbuf) + { + Object (pixbuf: pixbuf); + } + + public override bool draw (Cairo.Context c) + { + if (pixbuf != null) + { + var cached_surface = get_cached_surface (c, pixbuf); + if (cached_surface != null) + { + c.set_source_surface (cached_surface, 0, 0); + c.paint (); + } + } + return false; + } +} diff --git a/src/cairo-utils.vala b/src/cairo-utils.vala new file mode 100644 index 0000000..a30a580 --- /dev/null +++ b/src/cairo-utils.vala @@ -0,0 +1,238 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2013 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Marco Trevisan <marco.trevisan@canonical.com> + * Mirco "MacSlow" Mueller <mirco.mueller@canonical.com> + */ + +namespace CairoUtils +{ + +public void rounded_rectangle (Cairo.Context c, double x, double y, + double width, double height, double radius) +{ + var w = width - radius * 2; + var h = height - radius * 2; + var kappa = 0.5522847498 * radius; + c.move_to (x + radius, y); + c.rel_line_to (w, 0); + c.rel_curve_to (kappa, 0, radius, radius - kappa, radius, radius); + c.rel_line_to (0, h); + c.rel_curve_to (0, kappa, kappa - radius, radius, -radius, radius); + c.rel_line_to (-w, 0); + c.rel_curve_to (-kappa, 0, -radius, kappa - radius, -radius, -radius); + c.rel_line_to (0, -h); + c.rel_curve_to (0, -kappa, radius - kappa, -radius, radius, -radius); +} + +class GaussianBlur +{ + /* Gaussian Blur, based on Mirco Mueller work on notify-osd */ + + public static void surface (Cairo.ImageSurface surface, uint radius, double sigma = 0.0f) + { + if (surface.get_format () != Cairo.Format.ARGB32) + { + warning ("Impossible to blur a non ARGB32-formatted ImageSurface"); + return; + } + + surface.flush (); + + double radiusf = Math.fabs (radius) + 1.0f; + + if (sigma == 0.0f) + sigma = Math.sqrt (-(radiusf * radiusf) / (2.0f * Math.log (1.0f / 255.0f))); + + int w = surface.get_width (); + int h = surface.get_height (); + int s = surface.get_stride (); + + // create pixman image for cairo image surface + unowned uchar[] p = surface.get_data (); + var src = new Pixman.Image.bits (Pixman.Format.A8R8G8B8, w, h, p, s); + + // attach gaussian kernel to pixman image + var params = create_gaussian_blur_kernel ((int) radius, sigma); + src.set_filter (Pixman.Filter.CONVOLUTION, params); + + // render blured image to new pixman image + Pixman.Image.composite (Pixman.Operation.SRC, src, null, src, + 0, 0, 0, 0, 0, 0, (uint16) w, (uint16) h); + + surface.mark_dirty (); + } + + private static Pixman.Fixed[] create_gaussian_blur_kernel (int radius, double sigma) + { + double scale2 = 2.0f * sigma * sigma; + double scale1 = 1.0f / (Math.PI * scale2); + int size = 2 * radius + 1; + int n_params = size * size; + double sum = 0; + + var tmp = new double[n_params]; + + // caluclate gaussian kernel in floating point format + for (int i = 0, x = -radius; x <= radius; ++x) + { + for (int y = -radius; y <= radius; ++y, ++i) + { + double u = x * x; + double v = y * y; + + tmp[i] = scale1 * Math.exp (-(u+v)/scale2); + + sum += tmp[i]; + } + } + + // normalize gaussian kernel and convert to fixed point format + var params = new Pixman.Fixed[n_params + 2]; + + params[0] = Pixman.Fixed.int (size); + params[1] = Pixman.Fixed.int (size); + + for (int i = 2; i < params.length; ++i) + params[i] = Pixman.Fixed.double (tmp[i] / sum); + + return params; + } +} + +class ExponentialBlur +{ + /* Exponential Blur, based on the Nux version */ + + const int APREC = 16; + const int ZPREC = 7; + + public static void surface (Cairo.ImageSurface surface, int radius) + { + if (radius < 1) + return; + + // before we mess with the surface execute any pending drawing + surface.flush (); + + unowned uchar[] pixels = surface.get_data (); + var width = surface.get_width (); + var height = surface.get_height (); + var format = surface.get_format (); + + switch (format) + { + case Cairo.Format.ARGB32: + blur (pixels, width, height, 4, radius); + break; + + case Cairo.Format.RGB24: + blur (pixels, width, height, 3, radius); + break; + + case Cairo.Format.A8: + blur (pixels, width, height, 1, radius); + break; + + default : + // do nothing + break; + } + + // inform cairo we altered the surfaces contents + surface.mark_dirty (); + } + + static void blur (uchar[] pixels, int width, int height, int channels, int radius) + { + // calculate the alpha such that 90% of + // the kernel is within the radius. + // (Kernel extends to infinity) + + int alpha = (int) ((1 << APREC) * (1.0f - Math.expf(-2.3f / (radius + 1.0f)))); + + for (int row = 0; row < height; ++row) + blurrow (pixels, width, height, channels, row, alpha); + + for (int col = 0; col < width; ++col) + blurcol (pixels, width, height, channels, col, alpha); + } + + static void blurrow (uchar[] pixels, int width, int height, int channels, int line, int alpha) + { + var scanline = &(pixels[line * width * channels]); + + int zR = *scanline << ZPREC; + int zG = *(scanline + 1) << ZPREC; + int zB = *(scanline + 2) << ZPREC; + int zA = *(scanline + 3) << ZPREC; + + for (int index = 0; index < width; ++index) + { + blurinner (&scanline[index * channels], alpha, ref zR, ref zG, ref zB, ref zA); + } + + for (int index = width - 2; index >= 0; --index) + { + blurinner (&scanline[index * channels], alpha, ref zR, ref zG, ref zB, ref zA); + } + } + + static void blurcol (uchar[] pixels, int width, int height, int channels, int x, int alpha) + { + var ptr = &(pixels[x * channels]); + + int zR = *ptr << ZPREC; + int zG = *(ptr + 1) << ZPREC; + int zB = *(ptr + 2) << ZPREC; + int zA = *(ptr + 3) << ZPREC; + + for (int index = width; index < (height - 1) * width; index += width) + { + blurinner (&ptr[index * channels], alpha, ref zR, ref zG, ref zB, ref zA); + } + + for (int index = (height - 2) * width; index >= 0; index -= width) + { + blurinner (&ptr[index * channels], alpha, ref zR, ref zG, ref zB, ref zA); + } + } + + static void blurinner (uchar *pixel, int alpha, ref int zR, ref int zG, ref int zB, ref int zA) + { + int R; + int G; + int B; + uchar A; + + R = *pixel; + G = *(pixel + 1); + B = *(pixel + 2); + A = *(pixel + 3); + + zR += (alpha * ((R << ZPREC) - zR)) >> APREC; + zG += (alpha * ((G << ZPREC) - zG)) >> APREC; + zB += (alpha * ((B << ZPREC) - zB)) >> APREC; + zA += (alpha * ((A << ZPREC) - zA)) >> APREC; + + *pixel = zR >> ZPREC; + *(pixel + 1) = zG >> ZPREC; + *(pixel + 2) = zB >> ZPREC; + *(pixel + 3) = zA >> ZPREC; + } +} + +} diff --git a/src/config.vapi b/src/config.vapi new file mode 100644 index 0000000..8fe2441 --- /dev/null +++ b/src/config.vapi @@ -0,0 +1,12 @@ +[CCode (cprefix = "", lower_case_cprefix = "", cheader_filename = "config.h")] +namespace Config +{ + public const string GETTEXT_PACKAGE; + public const string LOCALEDIR; + public const string VERSION; + public const string CONFIG_FILE; + public const string INDICATOR_FILE_DIR; + public const string PKGDATADIR; + public const string INDICATORDIR; + public const string USD_BINARY; +} diff --git a/src/dash-box.vala b/src/dash-box.vala new file mode 100644 index 0000000..889ba41 --- /dev/null +++ b/src/dash-box.vala @@ -0,0 +1,230 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011,2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authored by: Michael Terry <michael.terry@canonical.com> + */ + +public class DashBox : Gtk.Box +{ + public Background? background { get; construct; default = null; } + + public bool has_base { get; private set; default = false; } + public double base_alpha { get; private set; default = 1.0; } + + private enum Mode + { + NORMAL, + PUSH_FADE_OUT, + PUSH_FADE_IN, + POP_FADE_OUT, + POP_FADE_IN, + } + + private GreeterList pushed; + private Gtk.Widget orig = null; + private FadeTracker orig_tracker; + private int orig_height = -1; + private Mode mode; + + public DashBox (Background bg) + { + Object (background: bg); + } + + construct + { + mode = Mode.NORMAL; + } + + /* Does not actually add w to this widget, as doing so would potentially mess with w's placement. */ + public void set_base (Gtk.Widget? w) + { + return_if_fail (pushed == null); + return_if_fail (mode == Mode.NORMAL); + + if (orig != null) + orig.size_allocate.disconnect (base_size_allocate_cb); + orig = w; + + if (orig != null) + { + orig.size_allocate.connect (base_size_allocate_cb); + orig_tracker = new FadeTracker (orig); + orig_tracker.notify["alpha"].connect (() => + { + base_alpha = orig_tracker.alpha; + queue_draw (); + }); + orig_tracker.done.connect (fade_done_cb); + base_alpha = orig_tracker.alpha; + has_base = true; + } + else + { + orig_height = -1; + get_preferred_height (null, out orig_height); /* save height */ + + orig_tracker = null; + base_alpha = 1.0; + has_base = false; + } + + queue_resize (); + } + + public void push (GreeterList l) + { + /* This isn't designed to push more than one widget at a time yet */ + return_if_fail (pushed == null); + return_if_fail (orig != null); + return_if_fail (mode == Mode.NORMAL); + + get_preferred_height (null, out orig_height); + pushed = l; + pushed.fade_done.connect (fade_done_cb); + mode = Mode.PUSH_FADE_OUT; + orig_tracker.reset (FadeTracker.Mode.FADE_OUT); + queue_resize (); + } + + public void pop () + { + return_if_fail (pushed != null); + return_if_fail (orig != null); + return_if_fail (mode == Mode.NORMAL); + + mode = Mode.POP_FADE_OUT; + pushed.fade_out (); + } + + private void fade_done_cb () + { + switch (mode) + { + case Mode.PUSH_FADE_OUT: + mode = Mode.PUSH_FADE_IN; + orig.hide (); + pushed.fade_in (); + break; + case Mode.PUSH_FADE_IN: + mode = Mode.NORMAL; + pushed.grab_focus (); + break; + case Mode.POP_FADE_OUT: + mode = Mode.POP_FADE_IN; + orig_tracker.reset (FadeTracker.Mode.FADE_IN); + orig.show (); + break; + case Mode.POP_FADE_IN: + mode = Mode.NORMAL; + pushed.fade_done.disconnect (fade_done_cb); + pushed.destroy (); + pushed = null; + queue_resize (); + orig.grab_focus (); + break; + } + } + + private void base_size_allocate_cb () + { + queue_resize (); + } + + public override void get_preferred_height (out int min, out int nat) + { + if (orig == null) + { + /* Return cached height if we have it. This makes transitions between two base widgets smoother. */ + if (orig_height >= 0) + { + min = orig_height; + nat = orig_height; + } + else + { + min = grid_size * GreeterList.DEFAULT_BOX_HEIGHT - GreeterList.BORDER * 2; + nat = grid_size * GreeterList.DEFAULT_BOX_HEIGHT - GreeterList.BORDER * 2; + } + } + else + { + if (pushed == null) + orig.get_preferred_height (out min, out nat); + else + { + pushed.selected_entry.get_preferred_height (out min, out nat); + min = int.max (orig_height, min); + nat = int.max (orig_height, nat); + } + } + } + + public override void get_preferred_width (out int min, out int nat) + { + min = grid_size * GreeterList.BOX_WIDTH - GreeterList.BORDER * 2; + nat = grid_size * GreeterList.BOX_WIDTH - GreeterList.BORDER * 2; + } + + public override bool draw (Cairo.Context c) + { + if (background != null) + { + int x, y; + background.translate_coordinates (this, 0, 0, out x, out y); + c.save (); + c.translate (x, y); + background.draw_full (c, Background.DrawFlags.NONE); + c.restore (); + } + + /* Draw darker background with a rounded border */ + var box_r = 0.3 * grid_size; + int box_y = 0; + int box_w; + int box_h; + get_preferred_width (null, out box_w); + get_preferred_height (null, out box_h); + + if (mode == Mode.PUSH_FADE_OUT) + { + /* Grow dark bg to fit new pushed object */ + var new_box_h = box_h - (int) ((box_h - orig_height) * base_alpha); + box_h = new_box_h; + } + else if (mode == Mode.POP_FADE_IN) + { + /* Shrink dark bg to fit orig */ + var new_box_h = box_h - (int) ((box_h - orig_height) * base_alpha); + box_h = new_box_h; + } + + c.save (); + + CairoUtils.rounded_rectangle (c, 0, box_y, box_w, box_h, box_r); + + c.set_source_rgba (0.1, 0.1, 0.1, 0.4); + c.fill_preserve (); + + c.set_source_rgba (0.4, 0.4, 0.4, 0.4); + c.set_line_width (1); + c.stroke (); + + c.restore (); + + return base.draw (c); + } +} diff --git a/src/dash-button.vala b/src/dash-button.vala new file mode 100644 index 0000000..c562a28 --- /dev/null +++ b/src/dash-button.vala @@ -0,0 +1,89 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Michael Terry <michael.terry@canonical.com> + */ + +public class DashButton : FlatButton, Fadable +{ + protected FadeTracker fade_tracker { get; protected set; } + private Gtk.Label text_label; + + private string _text = ""; + public string text + { + get { return _text; } + set + { + _text = value; + text_label.set_markup ("<span font=\"Ubuntu 13\">%s</span>".printf (value)); + } + } + + public DashButton (string text) + { + fade_tracker = new FadeTracker (this); + + var hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + + /* Add text */ + text_label = new Gtk.Label (""); + text_label.use_markup = true; + text_label.hexpand = true; + text_label.halign = Gtk.Align.START; + hbox.add (text_label); + this.text = text; + + /* Add chevron */ + var path = Path.build_filename (Config.PKGDATADIR, "arrow_right.png", null); + try + { + var pixbuf = new Gdk.Pixbuf.from_file (path); + var image = new CachedImage (pixbuf); + image.valign = Gtk.Align.CENTER; + hbox.add (image); + } + catch (Error e) + { + debug ("Error loading image %s: %s", path, e.message); + } + + hbox.show_all (); + add (hbox); + + try + { + var style = new Gtk.CssProvider (); + style.load_from_data ("* {padding: 6px 8px 6px 8px; + -GtkWidget-focus-line-width: 0px; + }", -1); + this.get_style_context ().add_provider (style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } + catch (Error e) + { + debug ("Internal error loading session chooser style: %s", e.message); + } + } + + public override bool draw (Cairo.Context c) + { + c.push_group (); + base.draw (c); + c.pop_group_to_source (); + c.paint_with_alpha (fade_tracker.alpha); + return false; + } +} diff --git a/src/dash-entry.vala b/src/dash-entry.vala new file mode 100644 index 0000000..9528705 --- /dev/null +++ b/src/dash-entry.vala @@ -0,0 +1,319 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Michael Terry <michael.terry@canonical.com> + */ + +/* Vala's vapi for gtk3 is broken for lookup_color (it forgets the out keyword) */ +[CCode (cheader_filename = "gtk/gtk.h")] +extern bool gtk_style_context_lookup_color (Gtk.StyleContext ctx, string color_name, out Gdk.RGBA color); + +public class DashEntry : Gtk.Entry, Fadable +{ + public static string font = "Ubuntu 14"; + public signal void respond (); + + public string constant_placeholder_text { get; set; } + public bool can_respond { get; set; default = true; } + + private bool _did_respond; + public bool did_respond + { + get + { + return _did_respond; + } + set + { + _did_respond = value; + if (value) + set_state_flags (Gtk.StateFlags.ACTIVE, false); + else + unset_state_flags (Gtk.StateFlags.ACTIVE); + queue_draw (); + } + } + + private static const string NO_BORDER_CLASS = "unity-greeter-no-border"; + + protected FadeTracker fade_tracker { get; protected set; } + private Gdk.Window arrow_win; + private static Gdk.Pixbuf arrow_pixbuf; + + construct + { + fade_tracker = new FadeTracker (this); + + notify["can-respond"].connect (queue_draw); + button_press_event.connect (button_press_event_cb); + + if (arrow_pixbuf == null) + { + var filename = Path.build_filename (Config.PKGDATADIR, "arrow_right.png"); + try + { + arrow_pixbuf = new Gdk.Pixbuf.from_file (filename); + } + catch (Error e) + { + debug ("Internal error loading arrow icon: %s", e.message); + } + } + + override_font (Pango.FontDescription.from_string (font)); + + var style_ctx = get_style_context (); + + try + { + var padding_provider = new Gtk.CssProvider (); + var css = "* {padding-right: %dpx;}".printf (get_arrow_size ()); + padding_provider.load_from_data (css, -1); + style_ctx.add_provider (padding_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } + catch (Error e) + { + debug ("Internal error loading padding style: %s", e.message); + } + + // We add the styles and classes we need for normal operation of the + // spinner animation. These are always "on" and we just turn them off + // right before drawing our parent class's draw function. This is done + // opt-out like that rather than just turning the styles on when we + // need to draw the spinner because the animation doesn't work right + // otherwise. See the draw() function for how we turn it off. + var no_border_provider = new Gtk.CssProvider (); + try + { + var css = ".%s {border: 0px;}".printf (NO_BORDER_CLASS); + no_border_provider.load_from_data (css, -1); + } + catch (Error e) + { + debug ("Internal error loading spinner style: %s", e.message); + } + style_ctx.add_provider (no_border_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + style_ctx.add_class (NO_BORDER_CLASS); + style_ctx.add_class (Gtk.STYLE_CLASS_SPINNER); + } + + public override bool draw (Cairo.Context c) + { + var style_ctx = get_style_context (); + + // See construct method for explanation of why we remove classes + style_ctx.save (); + style_ctx.remove_class (Gtk.STYLE_CLASS_SPINNER); + style_ctx.remove_class (NO_BORDER_CLASS); + c.save (); + c.push_group (); + base.draw (c); + c.pop_group_to_source (); + c.paint_with_alpha (fade_tracker.alpha); + c.restore (); + style_ctx.restore (); + + /* Now draw the prompt text */ + if (get_text_length () == 0 && constant_placeholder_text.length > 0) + draw_prompt_text (c); + + /* Draw activity spinner if we need to */ + if (did_respond) + draw_spinner (c); + else if (can_respond && get_text_length () > 0) + draw_arrow (c); + + return false; + } + + private void draw_spinner (Cairo.Context c) + { + c.save (); + + var style_ctx = get_style_context (); + var arrow_size = get_arrow_size (); + Gtk.cairo_transform_to_window (c, this, arrow_win); + style_ctx.render_activity (c, 0, 0, arrow_size, arrow_size); + + c.restore (); + } + + private void draw_arrow (Cairo.Context c) + { + if (arrow_pixbuf == null) + return; + + c.save (); + + var arrow_size = get_arrow_size (); + Gtk.cairo_transform_to_window (c, this, arrow_win); + c.translate (arrow_size - arrow_pixbuf.get_width () - 1, 0); // right align + Gdk.cairo_set_source_pixbuf (c, arrow_pixbuf, 0, 0); + + c.paint (); + c.restore (); + } + + private void draw_prompt_text (Cairo.Context c) + { + c.save (); + + /* Position text */ + int x, y; + get_layout_offsets (out x, out y); + c.move_to (x, y); + + /* Set foreground color */ + var fg = Gdk.RGBA (); + var context = get_style_context (); + if (!gtk_style_context_lookup_color (context, "placeholder_text_color", out fg)) + fg.parse ("#888"); + c.set_source_rgba (fg.red, fg.green, fg.blue, fg.alpha); + + /* Draw text */ + var layout = create_pango_layout (constant_placeholder_text); + layout.set_font_description (Pango.FontDescription.from_string ("Ubuntu 13")); + Pango.cairo_show_layout (c, layout); + + c.restore (); + } + + public override void activate () + { + base.activate (); + if (can_respond) + { + did_respond = true; + respond (); + } + else + { + get_toplevel ().child_focus (Gtk.DirectionType.TAB_FORWARD); + } + } + + public bool button_press_event_cb (Gdk.EventButton event) + { + if (event.window == arrow_win && get_text_length () > 0) + { + activate (); + return true; + } + else + return false; + } + + private int get_arrow_size () + { + // height is larger than width for the arrow, so we measure using that + if (arrow_pixbuf != null) + return arrow_pixbuf.get_height (); + else + return 20; // Shouldn't happen + } + + private void get_arrow_location (out int x, out int y) + { + var arrow_size = get_arrow_size (); + + Gtk.Allocation allocation; + get_allocation (out allocation); + + // height is larger than width for the arrow, so we measure using that + var margin = (allocation.height - arrow_size) / 2; + + x = allocation.x + allocation.width - margin - arrow_size; + y = allocation.y + margin; + } + + public override void size_allocate (Gtk.Allocation allocation) + { + base.size_allocate (allocation); + + if (arrow_win == null) + return; + + int arrow_x, arrow_y; + get_arrow_location (out arrow_x, out arrow_y); + var arrow_size = get_arrow_size (); + + arrow_win.move_resize (arrow_x, arrow_y, arrow_size, arrow_size); + } + + public override void realize () + { + base.realize (); + + var cursor = new Gdk.Cursor (Gdk.CursorType.LEFT_PTR); + var attrs = Gdk.WindowAttr (); + attrs.x = 0; + attrs.y = 0; + attrs.width = 1; + attrs.height = 1; + attrs.cursor = cursor; + attrs.wclass = Gdk.WindowWindowClass.INPUT_ONLY; + attrs.window_type = Gdk.WindowType.CHILD; + attrs.event_mask = get_events () | + Gdk.EventMask.BUTTON_PRESS_MASK; + + arrow_win = new Gdk.Window (get_window (), attrs, + Gdk.WindowAttributesType.X | + Gdk.WindowAttributesType.Y | + Gdk.WindowAttributesType.CURSOR); + arrow_win.ref (); + arrow_win.set_user_data (this); + } + + public override void unrealize () + { + if (arrow_win != null) + { + arrow_win.destroy (); + arrow_win = null; + } + base.unrealize (); + } + + public override void map () + { + base.map (); + if (arrow_win != null) + arrow_win.show (); + } + + public override void unmap () + { + if (arrow_win != null) + arrow_win.hide (); + base.unmap (); + } + + public override bool key_press_event (Gdk.EventKey event) + { + // This is a workaroud for bug https://launchpad.net/bugs/944159 + // The problem is that orca seems to not notice that it's in a password + // field on startup. We just need to kick orca in the pants. + if (UnityGreeter.singleton.orca_needs_kick) + { + Signal.emit_by_name (get_accessible (), "focus-event", true); + UnityGreeter.singleton.orca_needs_kick = false; + } + + return base.key_press_event (event); + } +} diff --git a/src/email-autocompleter.vala b/src/email-autocompleter.vala new file mode 100644 index 0000000..d11fb30 --- /dev/null +++ b/src/email-autocompleter.vala @@ -0,0 +1,79 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +public class EmailAutocompleter +{ + private Gtk.Entry entry; + private string[] domains; + private string prevText = ""; + + private void entry_changed () + { + if (entry.text.length < prevText.length) + { + /* Do nothing on erases of text */ + prevText = entry.text; + return; + } + + prevText = entry.text; + + int first_at = entry.text.index_of ("@"); + if (first_at != -1) + { + int second_at = entry.text.index_of ("@", first_at + 1); + if (second_at == -1) + { + /* We have exactly one @ */ + string text_after_at = entry.text.slice (first_at + 1, entry.text.length); + + /* Find first prefix match */ + int match = -1; + for (int i = 0; match == -1 && i < domains.length; ++i) + { + if (domains[i].has_prefix (text_after_at)) + match = i; + } + + if (match != -1) + { + /* Calculate the suffix part we need to add */ + var best_match = domains[match]; + var text_to_add = best_match.slice (text_after_at.length, best_match.length); + if (text_to_add.length > 0) + { + entry.text = entry.text + text_to_add; + /* TODO This is quite ugly/hacky :-/ */ + Timeout.add (0, () => + { + entry.select_region (entry.text.length - text_to_add.length, entry.text.length); + return false; + }); + } + } + } + } + } + + public EmailAutocompleter (Gtk.Entry e, string[] email_domains) + { + entry = e; + domains = email_domains; + entry.changed.connect (entry_changed); + } +} diff --git a/src/fadable-box.vala b/src/fadable-box.vala new file mode 100644 index 0000000..c287d21 --- /dev/null +++ b/src/fadable-box.vala @@ -0,0 +1,46 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011,2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authored by: Michael Terry <michael.terry@canonical.com> + */ + +public class FadableBox : Gtk.EventBox, Fadable +{ + public signal void fade_done (); + + protected FadeTracker fade_tracker { get; protected set; } + + construct + { + visible_window = false; + fade_tracker = new FadeTracker (this); + fade_tracker.done.connect (() => { fade_done (); }); + } + + protected virtual void draw_full_alpha (Cairo.Context c) + { + base.draw (c); + } + + public override bool draw (Cairo.Context c) + { + c.push_group (); + draw_full_alpha (c); + c.pop_group_to_source (); + c.paint_with_alpha (fade_tracker.alpha); + return false; + } +} diff --git a/src/fadable.vala b/src/fadable.vala new file mode 100644 index 0000000..a67ad63 --- /dev/null +++ b/src/fadable.vala @@ -0,0 +1,99 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Michael Terry <michael.terry@canonical.com> + */ + +public class FadeTracker : Object +{ + public signal void done (); + + public double alpha { get; set; default = 1.0; } + public Gtk.Widget widget { get; construct; } + + public enum Mode + { + FADE_IN, + FADE_OUT, + } + + public FadeTracker (Gtk.Widget widget) + { + Object (widget: widget); + } + + public void reset (Mode mode) + { + this.mode = mode; + animate_cb (0.0); + widget.show (); + timer.reset (); + } + + private AnimateTimer timer; + private Mode mode; + + construct + { + timer = new AnimateTimer (AnimateTimer.ease_out_quint, AnimateTimer.INSTANT); + timer.animate.connect (animate_cb); + } + + private void animate_cb (double progress) + { + if (mode == Mode.FADE_IN) + { + alpha = progress; + if (progress == 1.0) + { + done (); + } + } + else + { + alpha = 1.0 - progress; + if (progress == 1.0) + { + widget.hide (); /* finish the job */ + done (); + } + } + + widget.queue_draw (); + } +} + +public interface Fadable : Gtk.Widget +{ + protected abstract FadeTracker fade_tracker { get; protected set; } + + public void fade_in () + { + fade_tracker.reset (FadeTracker.Mode.FADE_IN); + } + + public void fade_out () + { + fade_tracker.reset (FadeTracker.Mode.FADE_OUT); + } + + /* In case you want to control fade manually */ + public void set_alpha (double alpha) + { + fade_tracker.alpha = alpha; + queue_draw (); + } +} diff --git a/src/fading-label.vala b/src/fading-label.vala new file mode 100644 index 0000000..80977e6 --- /dev/null +++ b/src/fading-label.vala @@ -0,0 +1,85 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Michael Terry <michael.terry@canonical.com> + */ + +public class FadingLabel : Gtk.Label +{ + private Cairo.Surface cached_surface; + + public FadingLabel (string text) + { + Object (label: text); + } + + public override void get_preferred_width (out int minimum, out int natural) + { + base.get_preferred_width (out minimum, out natural); + minimum = 0; + } + + public override void get_preferred_width_for_height (int height, out int minimum, out int natural) + { + base.get_preferred_width_for_height (height, out minimum, out natural); + minimum = 0; + } + + public override void size_allocate (Gtk.Allocation allocation) + { + base.size_allocate (allocation); + cached_surface = null; + } + + private Cairo.Surface make_surface (Cairo.Context orig_c) + { + int w, h; + get_layout ().get_pixel_size (out w, out h); + + var bw = get_allocated_width (); + var bh = get_allocated_height (); + + var surface = new Cairo.Surface.similar (orig_c.get_target (), Cairo.Content.COLOR_ALPHA, bw, bh); + var c = new Cairo.Context (surface); + + if (w > bw) + { + c.push_group (); + base.draw (c); + c.pop_group_to_source (); + + var mask = new Cairo.Pattern.linear (0, 0, bw, 0); + mask.add_color_stop_rgba (1.0 - 27.0 / bw, 1.0, 1.0, 1.0, 1.0); + mask.add_color_stop_rgba (1.0 - 21.6 / bw, 1.0, 1.0, 1.0, 0.5); + mask.add_color_stop_rgba (1.0, 1.0, 1.0, 1.0, 0.0); + + c.mask (mask); + } + else + base.draw (c); + + return surface; + } + + public override bool draw (Cairo.Context c) + { + if (cached_surface == null) + cached_surface = make_surface (c); + c.set_source_surface (cached_surface, 0, 0); + c.paint (); + return false; + } +} diff --git a/src/fixes.vapi b/src/fixes.vapi new file mode 100644 index 0000000..8d08370 --- /dev/null +++ b/src/fixes.vapi @@ -0,0 +1,57 @@ +#if !VALA_0_22 +namespace Posix +{ + [CCode (cheader_filename = "sys/mman.h")] + public const int MCL_CURRENT; + [CCode (cheader_filename = "sys/mman.h")] + public const int MCL_FUTURE; + [CCode (cheader_filename = "sys/mman.h")] + public int mlockall (int flags); + [CCode (cheader_filename = "sys/mman.h")] + public int munlockall (); +} +#endif + +// See https://bugzilla.gnome.org/show_bug.cgi?id=727113 +[CCode (cprefix = "", lower_case_cprefix = "", cheader_filename = "X11/Xlib.h")] +namespace X +{ + [CCode (cname = "XCreatePixmap")] + public int CreatePixmap (X.Display display, X.Drawable d, uint width, uint height, uint depth); + [CCode (cname = "XSetWindowBackgroundPixmap")] + public int SetWindowBackgroundPixmap (X.Display display, X.Window w, int Pixmap); + [CCode (cname = "XClearWindow")] + public int ClearWindow (X.Display display, X.Window w); + public const int RetainPermanent; +} + +namespace Gtk +{ + namespace RGB + { + // Fixed in Vala 0.24 + public void to_hsv (double r, double g, double b, out double h, out double s, out double v); + } +} + +namespace Gnome +{ + [CCode (cheader_filename = "libgnome-desktop/gnome-idle-monitor.h")] + public class IdleMonitor : GLib.Object + { + public IdleMonitor (); + public IdleMonitor.for_device (Gdk.Device device); + public uint add_idle_watch (uint64 interval_msec, IdleMonitorWatchFunc callback, GLib.DestroyNotify? notify = null); + public uint add_user_active_watch (IdleMonitorWatchFunc callback, GLib.DestroyNotify? notify = null); + public void remove_watch (uint id); + public int64 get_idletime (); + } + + public delegate void IdleMonitorWatchFunc (IdleMonitor monitor, uint id); +} + +// Note, fixed in 1.10.0 +namespace LightDM +{ + bool greeter_start_session_sync (LightDM.Greeter greeter, string session) throws GLib.Error; +} diff --git a/src/flat-button.vala b/src/flat-button.vala new file mode 100644 index 0000000..3a718d2 --- /dev/null +++ b/src/flat-button.vala @@ -0,0 +1,66 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Michael Terry <michael.terry@canonical.com> + */ + +public class FlatButton : Gtk.Button +{ + private bool did_press; + + construct + { + UnityGreeter.add_style_class (this); + try + { + var style = new Gtk.CssProvider (); + style.load_from_data ("* {-GtkButton-child-displacement-x: 0px; + -GtkButton-child-displacement-y: 0px; + -GtkWidget-focus-line-width: 1px; + }", -1); + get_style_context ().add_provider (style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } + catch (Error e) + { + debug ("Internal error loading session chooser style: %s", e.message); + } + } + + public override bool draw (Cairo.Context c) + { + // Make sure we don't react to mouse hovers + unset_state_flags (Gtk.StateFlags.PRELIGHT); + return base.draw (c); + } + + public override void pressed () + { + // Do nothing. The normal handler sets priv->button_down which + // internally causes draw() to draw a special border and background + // that we don't want. + did_press = true; + } + + public override void released () + { + if (did_press) + { + base.pressed (); // fake an insta-click + did_press = false; + } + base.released (); + } +} diff --git a/src/greeter-list.vala b/src/greeter-list.vala new file mode 100644 index 0000000..dee584f --- /dev/null +++ b/src/greeter-list.vala @@ -0,0 +1,949 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Robert Ancell <robert.ancell@canonical.com> + * Michael Terry <michael.terry@canonical.com> + * Scott Sweeny <scott.sweeny@canonical.com> + */ + +private int get_grid_offset (int size) +{ + return (int) (size % grid_size) / 2; +} + +[DBus (name="com.canonical.UnityGreeter.List")] +public class ListDBusInterface : Object +{ + private GreeterList list; + + public ListDBusInterface (GreeterList list) + { + this.list = list; + this.list.entry_selected.connect ((name) => { + entry_selected (name); + }); + } + + public string get_active_entry () + { + string entry = ""; + + if (list.selected_entry != null && list.selected_entry.id != null) + entry = list.selected_entry.id; + + return entry; + } + + public void set_active_entry (string entry_name) + { + list.set_active_entry (entry_name); + } + + public signal void entry_selected (string entry_name); +} + +public abstract class GreeterList : FadableBox +{ + public Background background { get; construct; } + public MenuBar menubar { get; construct; } + public PromptBox? selected_entry { get; private set; default = null; } + public bool start_scrolling { get; set; default = true; } + + protected string greeter_authenticating_user; + + protected bool _always_show_manual = false; + public bool always_show_manual + { + get { return _always_show_manual; } + set + { + _always_show_manual = value; + if (value) + add_manual_entry (); + else if (have_entries ()) + remove_entry ("*other"); + } + } + + protected List<PromptBox> entries = null; + + private ListDBusInterface dbus_object; + + private double scroll_target_location; + private double scroll_start_location; + private double scroll_location; + private double scroll_direction; + + private AnimateTimer scroll_timer; + + private Gtk.Fixed fixed; + public DashBox greeter_box; + private int cached_box_height = -1; + + protected enum Mode + { + ENTRY, + SCROLLING, + } + protected Mode mode = Mode.ENTRY; + + public static const int BORDER = 4; + public static const int BOX_WIDTH = 8; /* in grid_size blocks */ + public static const int DEFAULT_BOX_HEIGHT = 3; /* in grid_size blocks */ + + private uint n_above = 4; + private uint n_below = 4; + + private int box_x + { + get { return 0; } + } + + private int box_y + { + get + { + /* First, get grid row number as if menubar weren't there */ + var row = (MainWindow.MENUBAR_HEIGHT + get_allocated_height ()) / grid_size; + row = row - DEFAULT_BOX_HEIGHT; /* and no default dash box */ + row = row / 2; /* and in the middle */ + /* Now calculate y pixel spot keeping in mind menubar's allocation */ + return row * grid_size - MainWindow.MENUBAR_HEIGHT; + } + } + + public signal void entry_selected (string? name); + public signal void entry_displayed_start (); + public signal void entry_displayed_done (); + + protected virtual string? get_selected_id () + { + if (selected_entry == null) + return null; + return selected_entry.id; + } + + private string? _manual_name = null; + public string? manual_name + { + get { return _manual_name; } + set + { + _manual_name = value; + if (find_entry ("*other") != null) + add_manual_entry (); + } + } + + private PromptBox _scrolling_entry = null; + private PromptBox scrolling_entry + { + get { return _scrolling_entry; } + set + { + /* When we swap out a scrolling entry, make sure to hide its + * image button, else it will appear in the tab chain. */ + if (_scrolling_entry != null) + _scrolling_entry.set_options_image (null); + _scrolling_entry = value; + } + } + + public GreeterList (Background bg, MenuBar mb) + { + Object (background: bg, menubar: mb); + } + + construct + { + can_focus = false; + visible_window = false; + + fixed = new Gtk.Fixed (); + fixed.show (); + add (fixed); + + greeter_box = new DashBox (background); + greeter_box.notify["base-alpha"].connect (() => { queue_draw (); }); + greeter_box.show (); + greeter_box.size_allocate.connect (greeter_box_size_allocate_cb); + add_with_class (greeter_box); + + scroll_timer = new AnimateTimer (AnimateTimer.ease_out_quint, AnimateTimer.FAST); + scroll_timer.animate.connect (animate_scrolling); + + try + { + Bus.get.begin (BusType.SESSION, null, on_bus_acquired); + } + catch (IOError e) + { + debug ("Error getting session bus: %s", e.message); + } + } + + private void on_bus_acquired (Object? obj, AsyncResult res) + { + try + { + var conn = Bus.get.end (res); + this.dbus_object = new ListDBusInterface (this); + conn.register_object ("/list", this.dbus_object); + } + catch (IOError e) + { + debug ("Error registering user list dbus object: %s", e.message); + } + } + + public enum ScrollTarget + { + START, + END, + UP, + DOWN, + } + + public override void get_preferred_width (out int min, out int nat) + { + min = BOX_WIDTH * grid_size; + nat = BOX_WIDTH * grid_size; + } + + public override void get_preferred_height (out int min, out int nat) + { + base.get_preferred_height (out min, out nat); + min = 0; + } + + public void cancel_authentication () + { + UnityGreeter.singleton.cancel_authentication (); + entry_selected (selected_entry.id); + } + + public void scroll (ScrollTarget target) + { + if (!sensitive) + return; + + switch (target) + { + case ScrollTarget.START: + select_entry (entries.nth_data (0), -1.0); + break; + case ScrollTarget.END: + select_entry (entries.nth_data (entries.length () - 1), 1.0); + break; + case ScrollTarget.UP: + var index = entries.index (selected_entry) - 1; + if (index < 0) + index = 0; + select_entry (entries.nth_data (index), -1.0); + break; + case ScrollTarget.DOWN: + var index = entries.index (selected_entry) + 1; + if (index >= (int) entries.length ()) + index = (int) entries.length () - 1; + select_entry (entries.nth_data (index), 1.0); + break; + } + } + + protected void add_with_class (Gtk.Widget widget) + { + fixed.add (widget); + UnityGreeter.add_style_class (widget); + } + + protected void redraw_greeter_box () + { + Gtk.Allocation allocation; + greeter_box.get_allocation (out allocation); + queue_draw_area (allocation.x, allocation.y, allocation.width, allocation.height); + } + + public void show_message (string text, bool is_error = false) + { + if (will_clear) + { + selected_entry.clear (); + will_clear = false; + } + + selected_entry.add_message (text, is_error); + } + + public DashEntry add_prompt (string text, bool secret = false) + { + if (will_clear) + { + selected_entry.clear (); + will_clear = false; + } + + string accessible_text = null; + if (selected_entry != null && selected_entry.label != null) + accessible_text = _("Enter password for %s").printf (selected_entry.label); + var prompt = selected_entry.add_prompt (text, accessible_text, secret); + + if (mode != Mode.SCROLLING) + selected_entry.show_prompts (); + + focus_prompt (); + redraw_greeter_box (); + + return prompt; + } + + public Gtk.ComboBox add_combo (GenericArray<string> texts, bool read_only) + { + if (will_clear) + { + selected_entry.clear (); + will_clear = false; + } + + var combo = selected_entry.add_combo (texts, read_only); + + focus_prompt (); + redraw_greeter_box (); + + return combo; + } + + public override void grab_focus () + { + focus_prompt (); + } + + public virtual void focus_prompt () + { + selected_entry.sensitive = true; + selected_entry.grab_focus (); + } + + public abstract void show_authenticated (bool successful = true); + + protected PromptBox? find_entry (string id) + { + foreach (var entry in entries) + { + if (entry.id == id) + return entry; + } + + return null; + } + + protected static int compare_entry (PromptBox a, PromptBox b) + { + if (a.id.has_prefix ("*") || b.id.has_prefix ("*")) + { + /* Special entries go after normal ones */ + if (!a.id.has_prefix ("*")) + return -1; + if (!b.id.has_prefix ("*")) + return 1; + + /* Manual comes before guest */ + if (a.id == "*other") + return -1; + if (a.id == "*guest") + return 1; + } + + /* Alphabetical by label */ + return a.label.ascii_casecmp (b.label); + } + + protected bool have_entries () + { + foreach (var e in entries) + { + if (e.id != "*other") + return true; + } + return false; + } + + protected virtual void insert_entry (PromptBox entry) + { + entries.insert_sorted (entry, compare_entry); + } + + protected abstract void add_manual_entry (); + + protected void add_entry (PromptBox entry) + { + entry.expand = true; + entry.set_size_request (grid_size * BOX_WIDTH - BORDER * 2, -1); + add_with_class (entry); + + insert_entry (entry); + + entry.name_clicked.connect (entry_clicked_cb); + + if (selected_entry == null) + select_entry (entry, 1.0); + else + select_entry (selected_entry, 1.0); + + move_names (); + } + + public void set_active_entry (string ?name) + { + var e = find_entry (name); + if (e != null) + { + var direction = 1.0; + if (selected_entry != null && + entries.index (selected_entry) > entries.index (e)) + { + direction = -1.0; + } + select_entry (e, direction); + } + } + + public void set_active_first_entry_with_prefix (string prefix) + { + foreach (var e in entries) + { + if (e.id.has_prefix (prefix)) + { + select_entry (e, 1.0); + break; + } + } + } + + public void remove_entry (string? name) + { + remove_entry_by_entry (find_entry (name)); + } + + public void remove_entries_with_prefix (string prefix) + { + int i = 0; + while (i < entries.length ()) + { + PromptBox e = entries.nth_data (i); + if (e.id.has_prefix (prefix)) + remove_entry_by_entry (e); + else + i++; + } + } + + public void remove_entry_by_entry (PromptBox? entry) + { + if (entry == null) + return; + + var index = entries.index (entry); + entry.destroy (); + entries.remove (entry); + + /* Select another entry if the selected one was removed */ + if (entry == selected_entry) + { + if (index >= entries.length () && index > 0) + index--; + else if (index < entries.length ()) + index++; + + if (entries.nth_data (index) != null) + select_entry (entries.nth_data (index), -1.0); + } + + /* Show a manual login if no users and no remote login entry */ + if (!have_entries () && !UnityGreeter.singleton.show_remote_login_hint ()) + add_manual_entry (); + + queue_draw (); + } + + protected int get_greeter_box_height () + { + int height; + greeter_box.get_preferred_height (null, out height); + return height; + } + + protected int get_greeter_box_height_grids () + { + int height = get_greeter_box_height (); + return height / grid_size + 1; /* +1 because we'll be slightly under due to BORDER */ + } + + protected int get_greeter_box_x () + { + return box_x + BORDER; + } + + protected int get_greeter_box_y () + { + return box_y + BORDER; + } + + protected virtual int get_position_y (double position) + { + // Most position heights are just the grid height. Except for the + // greeter box itself. + int box_height = get_greeter_box_height_grids () * grid_size; + double offset; + + if (position < 0) + offset = position * grid_size; + else if (position < 1) + offset = position * box_height; + else + offset = (position - 1) * grid_size + box_height; + + return box_y + (int)Math.round(offset); + } + + private void move_entry (PromptBox entry, double position) + { + var alpha = 1.0; + if (position < 0) + alpha = 1.0 + position / (n_above + 1); + else + alpha = 1.0 - position / (n_below + 1); + entry.set_alpha (alpha); + + /* Some entry types may care where they are (e.g. wifi prompt) */ + entry.position = position; + + Gtk.Allocation allocation; + get_allocation (out allocation); + + var child_allocation = Gtk.Allocation (); + child_allocation.width = grid_size * BOX_WIDTH - BORDER * 2; + entry.get_preferred_height_for_width (child_allocation.width, null, out child_allocation.height); + child_allocation.x = allocation.x + get_greeter_box_x (); + child_allocation.y = allocation.y + get_position_y (position); + fixed.move (entry, child_allocation.x, child_allocation.y); + entry.size_allocate (child_allocation); + } + + public void greeter_box_size_allocate_cb (Gtk.Allocation allocation) + { + /* If the greeter box allocation changes while not moving fix the entries position */ + if (scrolling_entry == null && allocation.height != cached_box_height) + { + /* We run in idle because it's kind of a recursive loop and + * ends up positioning the entries in the wrong place if we try + * to do it during an existing allocation. */ + Idle.add (() => { move_names (); return false; }); + } + cached_box_height = allocation.height; + } + + public void move_names () + { + var index = 0; + foreach (var entry in entries) + { + var position = index - scroll_location; + + /* Draw entries above, in and below the box */ + if (position > -1 * (int)(n_above + 1) && position < n_below + 1) + { + move_entry (entry, position); + // Sometimes we will be overlayed by another widget like the + // session chooser. In such cases, don't try to show ourselves + var is_hidden = (position == 0 && greeter_box.has_base && + greeter_box.base_alpha == 0.0); + if (!is_hidden) + entry.show (); + } + else + entry.hide (); + + index++; + } + queue_draw (); + } + + private void animate_scrolling (double progress) + { + /* Total height of list */ + var h = entries.length (); + + /* How far we have to go in total, either up or down with wrapping */ + var distance = scroll_target_location - scroll_start_location; + if (scroll_direction * distance < 0) + distance += scroll_direction * h; + + /* How far we've gone so far */ + distance *= progress; + + /* Go that far and wrap around */ + scroll_location = scroll_start_location + distance; + if (scroll_location > h) + scroll_location -= h; + if (scroll_location < 0) + scroll_location += h; + + move_names (); + + if (progress >= 0.975 && !greeter_box.has_base) + { + setup_prompt_box (); + entry_displayed_start (); + } + + /* Stop when we get there */ + if (progress >= 1.0) + finished_scrolling (); + } + + private void finished_scrolling () + { + scrolling_entry = null; + selected_entry.show_prompts (); /* set prompts to be visible immediately */ + focus_prompt (); + entry_displayed_done (); + mode = Mode.ENTRY; + } + + protected void select_entry (PromptBox entry, double direction, bool do_scroll = true) + { + if (!get_realized ()) + { + /* Just note it for the future if we haven't been realized yet */ + selected_entry = entry; + return; + } + + if (scroll_target_location != entries.index (entry)) + { + var new_target = entries.index (entry); + var new_direction = direction; + var new_start = scroll_location; + + if (scroll_location != new_target && do_scroll) + { + var new_distance = new_direction * (new_target - new_start); + /* Base rate is 350 (250 + 100). If we find ourselves going further, slow down animation */ + scroll_timer.reset (250 + int.min ((int)(100 * (Math.fabs (new_distance))), 500)); + + mode = Mode.SCROLLING; + } + + scrolling_entry = selected_entry; + scroll_target_location = new_target; + scroll_direction = new_direction; + scroll_start_location = new_start; + } + + if (selected_entry != entry) + { + greeter_box.set_base (null); + if (selected_entry != null) + selected_entry.clear (); + + selected_entry = entry; + entry_selected (selected_entry.id); + + if (mode == Mode.ENTRY) + { + /* don't need to move, but make sure we trigger the same side effects */ + setup_prompt_box (); + scroll_timer.reset (0); + } + } + } + + protected virtual void setup_prompt_box (bool fade = true) + { + greeter_box.set_base (selected_entry); + selected_entry.add_static_prompts (); + if (fade) + selected_entry.fade_in_prompts (); + else + selected_entry.show_prompts (); + } + + public override void realize () + { + base.realize (); + + /* NOTE: This is going to cause the entry_selected signal to be emitted even if selected_entry has not changed */ + var saved_entry = selected_entry; + selected_entry = null; + select_entry (saved_entry, 1, start_scrolling); + move_names (); + } + + private void allocate_greeter_box () + { + Gtk.Allocation allocation; + get_allocation (out allocation); + + var child_allocation = Gtk.Allocation (); + greeter_box.get_preferred_width (null, out child_allocation.width); + greeter_box.get_preferred_height (null, out child_allocation.height); + child_allocation.x = allocation.x + get_greeter_box_x (); + child_allocation.y = allocation.y + get_greeter_box_y (); + fixed.move (greeter_box, child_allocation.x, child_allocation.y); + greeter_box.size_allocate (child_allocation); + + foreach (var entry in entries) + { + entry.set_zone (greeter_box); + } + } + + public override void size_allocate (Gtk.Allocation allocation) + { + base.size_allocate (allocation); + + if (!get_realized ()) + return; + + allocate_greeter_box (); + move_names (); + } + + public override bool draw (Cairo.Context c) + { + c.push_group (); + + c.save (); + fixed.propagate_draw (greeter_box, c); /* Always full alpha */ + c.restore (); + + if (greeter_box.base_alpha != 0.0) + { + c.save (); + c.push_group (); + + c.rectangle (get_greeter_box_x (), get_greeter_box_y () - n_above * grid_size, grid_size * BOX_WIDTH - BORDER * 2, grid_size * (n_above + n_below + get_greeter_box_height_grids ())); + c.clip (); + + foreach (var child in fixed.get_children ()) + { + if (child != greeter_box) + fixed.propagate_draw (child, c); + } + + c.pop_group_to_source (); + c.paint_with_alpha (greeter_box.base_alpha); + c.restore (); + } + + c.pop_group_to_source (); + c.paint_with_alpha (fade_tracker.alpha); + + return false; + } + + private void entry_clicked_cb (PromptBox entry) + { + if (mode != Mode.ENTRY) + return; + + var index = entries.index (entry); + var position = index - scroll_location; + + if (position < 0.0) + select_entry (entry, -1.0); + else if (position >= 1.0) + select_entry (entry, 1.0); + } + + + /* Not all subclasses are going to be interested in talking to lightdm, but for those that are, make it easy. */ + + protected bool will_clear = false; + protected bool prompted = false; + protected bool unacknowledged_messages = false; + + protected void connect_to_lightdm () + { + UnityGreeter.singleton.show_message.connect (show_message_cb); + UnityGreeter.singleton.show_prompt.connect (show_prompt_cb); + UnityGreeter.singleton.authentication_complete.connect (authentication_complete_cb); + } + + protected void show_message_cb (string text, LightDM.MessageType type) + { + unacknowledged_messages = true; + show_message (text, type == LightDM.MessageType.ERROR); + } + + protected virtual void show_prompt_cb (string text, LightDM.PromptType type) + { + /* Notify the greeter on what user has been logged */ + if (get_selected_id () == "*other" && manual_name == null) + { + if (UnityGreeter.singleton.test_mode) + manual_name = test_username; + else + manual_name = UnityGreeter.singleton.authentication_user(); + } + + prompted = true; + if (text == "Password: ") + text = _("Password:"); + if (text == "login:") + text = _("Username:"); + add_prompt (text, type == LightDM.PromptType.SECRET); + } + + protected virtual void authentication_complete_cb () + { + /* Not the best of the solutions but seems the asynchrony + * when talking to lightdm process means that you can + * go to the "Guest" account, start authenticating as guest + * keep moving down to some of the remote servers + * and the answer will come after that, and even calling + * greeter.cancel_authentication won't help + * so basically i'm just ignoring any authentication callback + * if we are not in the same place in the list as we were + * when we called greeter.authenticate* */ + if (greeter_authenticating_user != selected_entry.id) + return; + + bool is_authenticated; + if (UnityGreeter.singleton.test_mode) + is_authenticated = test_is_authenticated; + else + is_authenticated = UnityGreeter.singleton.is_authenticated(); + + if (is_authenticated) + { + /* Login immediately if prompted and user has acknowledged all messages */ + if (prompted && !unacknowledged_messages) + { + login_complete (); + if (UnityGreeter.singleton.test_mode) + start_session (); + else + { + if (background.alpha == 1.0) + start_session (); + else + background.notify["alpha"].connect (background_loaded_cb); + } + } + else + { + prompted = true; + show_authenticated (); + } + } + else + { + if (prompted) + { + /* Show an error if one wasn't provided */ + if (will_clear) + show_message (_("Invalid password, please try again"), true); + + selected_entry.reset_spinners (); + + /* Restart authentication */ + start_authentication (); + } + else + { + /* Show an error if one wasn't provided */ + if (!selected_entry.has_errors) + show_message (_("Failed to authenticate"), true); + + /* Stop authentication */ + show_authenticated (false); + } + } + } + + protected virtual void start_authentication () + { + prompted = false; + unacknowledged_messages = false; + + /* Reset manual username */ + manual_name = null; + + will_clear = false; + + greeter_authenticating_user = get_selected_id (); + + if (UnityGreeter.singleton.test_mode) + test_start_authentication (); + else + { + if (get_selected_id () == "*other") + UnityGreeter.singleton.authenticate (); + else if (get_selected_id () == "*guest") + UnityGreeter.singleton.authenticate_as_guest (); + else + UnityGreeter.singleton.authenticate (get_selected_id ()); + } + } + + private void background_loaded_cb (ParamSpec pspec) + { + if (background.alpha == 1.0) + { + background.notify["alpha"].disconnect (background_loaded_cb); + start_session (); + } + } + + private void start_session () + { + if (!UnityGreeter.singleton.start_session (get_lightdm_session (), background)) + { + show_message (_("Failed to start session"), true); + start_authentication (); + return; + } + + /* Set the background */ + background.draw_grid = false; + background.queue_draw (); + } + + public void login_complete () + { + sensitive = false; + + selected_entry.clear (); + selected_entry.add_message (_("Logging in…"), false); + + redraw_greeter_box (); + } + + protected virtual string get_lightdm_session () + { + return "ubuntu"; + } + + /* Testing code below this */ + + protected string? test_username = null; + protected bool test_is_authenticated = false; + + protected virtual void test_start_authentication () + { + } +} diff --git a/src/indicator.vapi b/src/indicator.vapi new file mode 100644 index 0000000..9b28c72 --- /dev/null +++ b/src/indicator.vapi @@ -0,0 +1,165 @@ +[CCode (cprefix = "Indicator", lower_case_cprefix = "indicator_")] +namespace Indicator { + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public class DesktopShortcuts : GLib.Object { + [CCode (has_construct_function = false)] + public DesktopShortcuts (string file, string identity); + public unowned string get_nicks (); + public bool nick_exec (string nick); + public unowned string nick_get_name (string nick); + public string desktop_file { construct; } + [NoAccessorMethod] + public string identity { owned get; construct; } + } + [CCode (cheader_filename = "libindicator/indicator-object.h")] + public class Object : GLib.Object { + [CCode (has_construct_function = false)] + protected Object (); + public bool check_environment (string env); + [NoWrapper] + public virtual void entry_activate (Indicator.ObjectEntry entry, uint timestamp); + [NoWrapper] + public virtual void entry_close (Indicator.ObjectEntry entry, uint timestamp); + [CCode (has_construct_function = false)] + public Object.from_file (string file); + [NoWrapper] + public virtual unowned string get_accessible_desc (); + public virtual GLib.List<unowned ObjectEntry> get_entries (); + public unowned string[] get_environment (); + [NoWrapper] + public virtual unowned Gtk.Image get_image (); + [NoWrapper] + public virtual unowned Gtk.Label get_label (); + public virtual uint get_location (Indicator.ObjectEntry entry); + [NoWrapper] + public virtual unowned Gtk.Menu get_menu (); + [NoWrapper] + public virtual unowned string get_name_hint (); + public virtual bool get_show_now (Indicator.ObjectEntry entry); + public virtual int get_position (); + [NoWrapper] + public virtual void reserved1 (); + [NoWrapper] + public virtual void reserved2 (); + [NoWrapper] + public virtual void reserved3 (); + [NoWrapper] + public virtual void reserved4 (); + [NoWrapper] + public virtual void reserved5 (); + public void set_environment (string[] env); + public virtual signal void accessible_desc_update (Indicator.ObjectEntry entry); + public virtual signal void entry_added (Indicator.ObjectEntry entry); + public virtual signal void entry_moved (Indicator.ObjectEntry entry, uint old_pos, uint new_pos); + public virtual signal void entry_removed (Indicator.ObjectEntry entry); + public virtual signal void entry_scrolled (Indicator.ObjectEntry entry, uint delta, Indicator.ScrollDirection direction); + public virtual signal void menu_show (Indicator.ObjectEntry entry, uint timestamp); + public virtual signal void show_now_changed (Indicator.ObjectEntry entry, bool show_now_state); + } + [CCode (cheader_filename = "libindicator/indicator-ng.h")] + public class Ng : Object { + [CCode (has_construct_function = false)] + public Ng.for_profile (string filename, string profile) throws GLib.Error; + } + [Compact] + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public class ObjectEntry { + public weak string accessible_desc; + public weak Gtk.Image image; + public weak Gtk.Label label; + public weak Gtk.Menu menu; + public weak string name_hint; + public weak GLib.Callback reserved1; + public weak GLib.Callback reserved2; + public weak GLib.Callback reserved3; + public weak GLib.Callback reserved4; + public static void activate (Indicator.Object io, Indicator.ObjectEntry entry, uint timestamp); + public static void close (Indicator.Object io, Indicator.ObjectEntry entry, uint timestamp); + } + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public class Service : GLib.Object { + [CCode (has_construct_function = false)] + public Service (string name); + [NoWrapper] + public virtual void indicator_service_reserved1 (); + [NoWrapper] + public virtual void indicator_service_reserved2 (); + [NoWrapper] + public virtual void indicator_service_reserved3 (); + [NoWrapper] + public virtual void indicator_service_reserved4 (); + [CCode (has_construct_function = false)] + public Service.version (string name, uint version); + [NoAccessorMethod] + public string name { owned get; set; } + public virtual signal void shutdown (); + } + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public class ServiceManager : GLib.Object { + [CCode (has_construct_function = false)] + public ServiceManager (string dbus_name); + public bool connected (); + [NoWrapper] + public virtual void indicator_service_manager_reserved1 (); + [NoWrapper] + public virtual void indicator_service_manager_reserved2 (); + [NoWrapper] + public virtual void indicator_service_manager_reserved3 (); + [NoWrapper] + public virtual void indicator_service_manager_reserved4 (); + public void set_refresh (uint time_in_ms); + [CCode (has_construct_function = false)] + public ServiceManager.version (string dbus_name, uint version); + [NoAccessorMethod] + public string name { owned get; set; } + public virtual signal void connection_change (bool connected); + } + [CCode (cprefix = "INDICATOR_OBJECT_SCROLL_", has_type_id = false, cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public enum ScrollDirection { + UP, + DOWN, + LEFT, + RIGHT + } + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h", has_target = false)] + public delegate GLib.Type get_type_t (); + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h", has_target = false)] + public delegate unowned string get_version_t (); + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public const string GET_TYPE_S; + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public const string GET_VERSION_S; + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public const string OBJECT_SIGNAL_ACCESSIBLE_DESC_UPDATE; + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public const string OBJECT_SIGNAL_ENTRY_ADDED; + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public const string OBJECT_SIGNAL_ENTRY_MOVED; + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public const string OBJECT_SIGNAL_ENTRY_REMOVED; + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public const string OBJECT_SIGNAL_ENTRY_SCROLLED; + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public const string OBJECT_SIGNAL_MENU_SHOW; + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public const string OBJECT_SIGNAL_SHOW_NOW_CHANGED; + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public const string SERVICE_MANAGER_SIGNAL_CONNECTION_CHANGE; + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public const string SERVICE_SIGNAL_SHUTDOWN; + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public const int SET_VERSION; + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public const string VERSION; + [CCode (cname = "get_version", cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public static unowned string get_version (); + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public static unowned Gtk.Image image_helper (string name); + [CCode (cheader_filename = "gtk/gtk.h,libindicator/indicator.h,libindicator/indicator-desktop-shortcuts.h,libindicator/indicator-image-helper.h,libindicator/indicator-object.h,libindicator/indicator-service.h,libindicator/indicator-service-manager.h")] + public static void image_helper_update (Gtk.Image image, string name); +} + +[CCode (cheader_filename="libido/libido.h", lower_case_cprefix = "ido_")] +namespace Ido { + public void init (); +} diff --git a/src/list-stack.vala b/src/list-stack.vala new file mode 100644 index 0000000..cd9745d --- /dev/null +++ b/src/list-stack.vala @@ -0,0 +1,92 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011,2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authored by: Michael Terry <michael.terry@canonical.com> + */ + +public class ListStack : Gtk.Fixed +{ + public uint num_children + { + get + { + var children = get_children (); + return children.length (); + } + } + + private int width; + + construct + { + width = grid_size * GreeterList.BOX_WIDTH; + } + + public GreeterList? top () + { + var children = get_children (); + if (children == null) + return null; + else + return children.last ().data as GreeterList; + } + + public void push (GreeterList pushed) + { + return_if_fail (pushed != null); + + var children = get_children (); + + pushed.start_scrolling = false; + pushed.set_size_request (width, -1); + add (pushed); + + if (children != null) + { + var current = children.last ().data as GreeterList; + /* Clear any errors so when we come back, they will be gone. */ + current.selected_entry.reset_state (); + current.greeter_box.push (pushed); + } + } + + public void pop () + { + var children = get_children (); + + return_if_fail (children != null); + + unowned List<Gtk.Widget> prev = children.last ().prev; + if (prev != null) + (prev.data as GreeterList).greeter_box.pop (); + } + + public override void size_allocate (Gtk.Allocation allocation) + { + base.size_allocate (allocation); + var children = get_children (); + foreach (var child in children) + { + child.size_allocate (allocation); + } + } + + public override void get_preferred_width (out int min, out int nat) + { + min = width; + nat = width; + } +} diff --git a/src/logo-generator.vala b/src/logo-generator.vala new file mode 100644 index 0000000..244c4b3 --- /dev/null +++ b/src/logo-generator.vala @@ -0,0 +1,46 @@ +public class Main : Object +{ + + private static string? file = null; + private static string? text = null; + private static string? result = null; + private const OptionEntry[] options = { + {"logo", 0, 0, OptionArg.FILENAME, ref file, "Path to logo", "LOGO"}, + {"text", 0, 0, OptionArg.STRING, ref text, "Sublogo text", "TEXT"}, + {"output", 0, 0, OptionArg.FILENAME, ref result, "Path to rendered output", "OUTPUT"}, + {null} + }; + + public static int main(string[] args) { + try { + var opt_context = new OptionContext ("- OptionContext example"); + opt_context.set_help_enabled (true); + opt_context.add_main_entries (options, null); + opt_context.parse (ref args); + } catch (OptionError e) { + stdout.printf ("error: %s\n", e.message); + stdout.printf ("Run '%s --help' to see a full list of available command line options.\n", args[0]); + return 0; + } + Cairo.ImageSurface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 245, 43); + Cairo.Context context = new Cairo.Context (surface); + context.translate (42, 11); + Cairo.ImageSurface logo = new Cairo.ImageSurface.from_png (file); + context.set_source_surface (logo, 0, 0); + context.paint(); + + context.set_source_rgba (1, 1, 1, 1); + context.translate (logo.get_width() + 0.25*logo.get_height(), logo.get_height()); + + var font_description = new Pango.FontDescription(); + font_description.set_family("Ubuntu"); + font_description.set_size((int)(0.75*logo.get_height() * Pango.SCALE)); + var layout = Pango.cairo_create_layout (context); + layout.set_font_description (font_description); + layout.set_text (text, -1); + Pango.cairo_show_layout_line(context, layout.get_line(0)); + + surface.write_to_png(result); + return 0; + } +} diff --git a/src/main-window.vala b/src/main-window.vala new file mode 100644 index 0000000..7574719 --- /dev/null +++ b/src/main-window.vala @@ -0,0 +1,398 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011,2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Robert Ancell <robert.ancell@canonical.com> + * Michael Terry <michael.terry@canonical.com> + */ + +public class MainWindow : Gtk.Window +{ + public MenuBar menubar; + + private List<Monitor> monitors; + private Monitor? primary_monitor; + private Monitor active_monitor; + private Background background; + private Gtk.Box login_box; + private Gtk.Box hbox; + private Gtk.Button back_button; + private ShutdownDialog? shutdown_dialog = null; + + public ListStack stack; + + // Menubar is smaller, but with shadow, we reserve more space + public static const int MENUBAR_HEIGHT = 32; + + construct + { + events |= Gdk.EventMask.POINTER_MOTION_MASK; + + var accel_group = new Gtk.AccelGroup (); + add_accel_group (accel_group); + + var bg_color = Gdk.RGBA (); + bg_color.parse (UGSettings.get_string (UGSettings.KEY_BACKGROUND_COLOR)); + override_background_color (Gtk.StateFlags.NORMAL, bg_color); + get_accessible ().set_name (_("Login Screen")); + has_resize_grip = false; + UnityGreeter.add_style_class (this); + + realize (); + background = new Background (Gdk.cairo_create (get_window ()).get_target ()); + background.draw_grid = UGSettings.get_boolean (UGSettings.KEY_DRAW_GRID); + background.default_background = UGSettings.get_string (UGSettings.KEY_BACKGROUND); + background.set_logo (UGSettings.get_string (UGSettings.KEY_LOGO), UGSettings.get_string (UGSettings.KEY_BACKGROUND_LOGO)); + background.show (); + add (background); + UnityGreeter.add_style_class (background); + + login_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + login_box.show (); + background.add (login_box); + + /* Box for menubar shadow */ + var menubox = new Gtk.EventBox (); + var menualign = new Gtk.Alignment (0.0f, 0.0f, 1.0f, 0.0f); + var shadow_path = Path.build_filename (Config.PKGDATADIR, + "shadow.png", null); + var shadow_style = ""; + if (FileUtils.test (shadow_path, FileTest.EXISTS)) + { + shadow_style = "background-image: url('%s'); + background-repeat: repeat;".printf(shadow_path); + } + try + { + var style = new Gtk.CssProvider (); + style.load_from_data ("* {background-color: transparent; + %s + }".printf(shadow_style), -1); + var context = menubox.get_style_context (); + context.add_provider (style, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } + catch (Error e) + { + debug ("Internal error loading menubox style: %s", e.message); + } + menubox.set_size_request (-1, MENUBAR_HEIGHT); + menubox.show (); + menualign.show (); + menubox.add (menualign); + login_box.add (menubox); + UnityGreeter.add_style_class (menualign); + UnityGreeter.add_style_class (menubox); + + menubar = new MenuBar (background, accel_group); + menubar.show (); + menualign.add (menubar); + UnityGreeter.add_style_class (menubar); + + hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + hbox.expand = true; + hbox.show (); + login_box.add (hbox); + + var align = new Gtk.Alignment (0.5f, 0.5f, 0.0f, 0.0f); + align.set_size_request (grid_size, -1); + align.margin_bottom = MENUBAR_HEIGHT; /* offset for menubar at top */ + align.show (); + hbox.add (align); + + back_button = new FlatButton (); + back_button.get_accessible ().set_name (_("Back")); + back_button.focus_on_click = false; + var image = new Gtk.Image.from_file (Path.build_filename (Config.PKGDATADIR, "arrow_left.png", null)); + image.show (); + back_button.set_size_request (grid_size - GreeterList.BORDER * 2, grid_size - GreeterList.BORDER * 2); + back_button.add (image); + back_button.clicked.connect (pop_list); + align.add (back_button); + + align = new Gtk.Alignment (0.0f, 0.5f, 0.0f, 1.0f); + align.show (); + hbox.add (align); + + stack = new ListStack (); + stack.show (); + align.add (stack); + + add_user_list (); + + if (UnityGreeter.singleton.test_mode) + { + /* Simulate an 800x600 monitor to the left of a 640x480 monitor */ + monitors = new List<Monitor> (); + monitors.append (new Monitor (0, 0, 800, 600)); + monitors.append (new Monitor (800, 120, 640, 480)); + background.set_monitors (monitors); + move_to_monitor (monitors.nth_data (0)); + resize (800 + 640, 600); + } + else + { + var screen = get_screen (); + screen.monitors_changed.connect (monitors_changed_cb); + monitors_changed_cb (screen); + } + } + + public void push_list (GreeterList widget) + { + stack.push (widget); + + if (stack.num_children > 1) + back_button.show (); + } + + public void pop_list () + { + if (stack.num_children <= 2) + back_button.hide (); + + stack.pop (); + } + + public override void size_allocate (Gtk.Allocation allocation) + { + base.size_allocate (allocation); + + if (hbox != null) + { + hbox.margin_left = get_grid_offset (get_allocated_width ()) + grid_size; + hbox.margin_right = get_grid_offset (get_allocated_width ()); + hbox.margin_top = get_grid_offset (get_allocated_height ()); + hbox.margin_bottom = get_grid_offset (get_allocated_height ()); + } + } + + private void monitors_changed_cb (Gdk.Screen screen) + { + int primary = screen.get_primary_monitor (); + debug ("Screen is %dx%d pixels", screen.get_width (), screen.get_height ()); + monitors = new List<Monitor> (); + primary_monitor = null; + + for (var i = 0; i < screen.get_n_monitors (); i++) + { + Gdk.Rectangle geometry; + screen.get_monitor_geometry (i, out geometry); + debug ("Monitor %d is %dx%d pixels at %d,%d", i, geometry.width, geometry.height, geometry.x, geometry.y); + + if (monitor_is_unique_position (screen, i)) + { + var monitor = new Monitor (geometry.x, geometry.y, geometry.width, geometry.height); + monitors.append (monitor); + + if (primary_monitor == null || i == primary) + primary_monitor = monitor; + } + } + + background.set_monitors (monitors); + resize (screen.get_width (), screen.get_height ()); + move (0, 0); + move_to_monitor (primary_monitor); + } + + /* Check if a monitor has a unique position */ + private bool monitor_is_unique_position (Gdk.Screen screen, int n) + { + Gdk.Rectangle g0; + screen.get_monitor_geometry (n, out g0); + + for (var i = n + 1; i < screen.get_n_monitors (); i++) + { + Gdk.Rectangle g1; + screen.get_monitor_geometry (i, out g1); + + if (g0.x == g1.x && g0.y == g1.y) + return false; + } + + return true; + } + + public override bool motion_notify_event (Gdk.EventMotion event) + { + var x = (int) (event.x + 0.5); + var y = (int) (event.y + 0.5); + + /* Get motion event relative to this widget */ + if (event.window != get_window ()) + { + int w_x, w_y; + get_window ().get_origin (out w_x, out w_y); + x -= w_x; + y -= w_y; + event.window.get_origin (out w_x, out w_y); + x += w_x; + y += w_y; + } + + foreach (var m in monitors) + { + if (x >= m.x && x <= m.x + m.width && y >= m.y && y <= m.y + m.height) + { + move_to_monitor (m); + break; + } + } + + return false; + } + + private void move_to_monitor (Monitor monitor) + { + active_monitor = monitor; + login_box.set_size_request (monitor.width, monitor.height); + background.set_active_monitor (monitor); + background.move (login_box, monitor.x, monitor.y); + + if (shutdown_dialog != null) + { + shutdown_dialog.set_active_monitor (monitor); + background.move (shutdown_dialog, monitor.x, monitor.y); + } + } + + private void add_user_list () + { + GreeterList greeter_list; + greeter_list = new UserList (background, menubar); + greeter_list.show (); + UnityGreeter.add_style_class (greeter_list); + push_list (greeter_list); + } + + public override bool key_press_event (Gdk.EventKey event) + { + var top = stack.top (); + + if (stack.top () is UserList) + { + var user_list = stack.top () as UserList; + if (!user_list.show_hidden_users) + { + var shift_mask = Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD1_MASK; + var control_mask = Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.MOD1_MASK; + var alt_mask = Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK; + if (((event.keyval == Gdk.Key.Shift_L || event.keyval == Gdk.Key.Shift_R) && (event.state & shift_mask) == shift_mask) || + ((event.keyval == Gdk.Key.Control_L || event.keyval == Gdk.Key.Control_R) && (event.state & control_mask) == control_mask) || + ((event.keyval == Gdk.Key.Alt_L || event.keyval == Gdk.Key.Alt_R) && (event.state & alt_mask) == alt_mask)) + { + debug ("Hidden user key combination detected"); + user_list.show_hidden_users = true; + return true; + } + } + } + + switch (event.keyval) + { + case Gdk.Key.Escape: + if (login_box.sensitive) + top.cancel_authentication (); + if (shutdown_dialog != null) + shutdown_dialog.cancel (); + return true; + case Gdk.Key.Page_Up: + case Gdk.Key.KP_Page_Up: + if (login_box.sensitive) + top.scroll (GreeterList.ScrollTarget.START); + return true; + case Gdk.Key.Page_Down: + case Gdk.Key.KP_Page_Down: + if (login_box.sensitive) + top.scroll (GreeterList.ScrollTarget.END); + return true; + case Gdk.Key.Up: + case Gdk.Key.KP_Up: + if (login_box.sensitive) + top.scroll (GreeterList.ScrollTarget.UP); + return true; + case Gdk.Key.Down: + case Gdk.Key.KP_Down: + if (login_box.sensitive) + top.scroll (GreeterList.ScrollTarget.DOWN); + return true; + case Gdk.Key.Left: + case Gdk.Key.KP_Left: + if (shutdown_dialog != null) + shutdown_dialog.focus_prev (); + return true; + case Gdk.Key.Right: + case Gdk.Key.KP_Right: + if (shutdown_dialog != null) + shutdown_dialog.focus_next (); + return true; + case Gdk.Key.F10: + if (login_box.sensitive) + menubar.select_first (false); + return true; + case Gdk.Key.PowerOff: + show_shutdown_dialog (ShutdownDialogType.SHUTDOWN); + return true; + case Gdk.Key.z: + if (UnityGreeter.singleton.test_mode && (event.state & Gdk.ModifierType.MOD1_MASK) != 0) + { + show_shutdown_dialog (ShutdownDialogType.SHUTDOWN); + return true; + } + break; + case Gdk.Key.Z: + if (UnityGreeter.singleton.test_mode && (event.state & Gdk.ModifierType.MOD1_MASK) != 0) + { + show_shutdown_dialog (ShutdownDialogType.RESTART); + return true; + } + break; + } + + return base.key_press_event (event); + } + + public void set_keyboard_state () + { + menubar.set_keyboard_state (); + } + + public void show_shutdown_dialog (ShutdownDialogType type) + { + if (shutdown_dialog != null) + shutdown_dialog.destroy (); + + /* Stop input to login box */ + login_box.sensitive = false; + + shutdown_dialog = new ShutdownDialog (type, background); + shutdown_dialog.closed.connect (close_shutdown_dialog); + background.add (shutdown_dialog); + move_to_monitor (active_monitor); + shutdown_dialog.visible = true; + } + + public void close_shutdown_dialog () + { + if (shutdown_dialog == null) + return; + + shutdown_dialog.destroy (); + shutdown_dialog = null; + + login_box.sensitive = true; + } +} diff --git a/src/menu.vala b/src/menu.vala new file mode 100644 index 0000000..71e70b1 --- /dev/null +++ b/src/menu.vala @@ -0,0 +1,47 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011,2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Andrea Cimitan <andrea.cimitan@canonical.com> + */ + + +public class Menu : Gtk.Menu +{ + public Background? background { get; construct; default = null; } + + public Menu (Background bg) + { + Object (background: bg); + } + + public override bool draw (Cairo.Context c) + { + if (background != null) + { + int x, y, bg_x, bg_y; + + background.get_window ().get_origin (out bg_x, out bg_y); + get_window ().get_origin (out x, out y); + c.save (); + c.translate (bg_x - x, bg_y - y); + background.draw_full (c, Background.DrawFlags.NONE); + c.restore (); + } + + base.draw (c); + return false; + } +} diff --git a/src/menubar.vala b/src/menubar.vala new file mode 100644 index 0000000..79783da --- /dev/null +++ b/src/menubar.vala @@ -0,0 +1,559 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011,2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Robert Ancell <robert.ancell@canonical.com> + * Michael Terry <michael.terry@canonical.com> + */ + +private class IndicatorMenuItem : Gtk.MenuItem +{ + public unowned Indicator.ObjectEntry entry; + private Gtk.Box hbox; + + public IndicatorMenuItem (Indicator.ObjectEntry entry) + { + this.entry = entry; + this.hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 3); + this.add (this.hbox); + this.hbox.show (); + + if (entry.label != null) + { + entry.label.show.connect (this.visibility_changed_cb); + entry.label.hide.connect (this.visibility_changed_cb); + hbox.pack_start (entry.label, false, false, 0); + } + if (entry.image != null) + { + entry.image.show.connect (visibility_changed_cb); + entry.image.hide.connect (visibility_changed_cb); + hbox.pack_start (entry.image, false, false, 0); + } + if (entry.accessible_desc != null) + get_accessible ().set_name (entry.accessible_desc); + if (entry.menu != null) + set_submenu (entry.menu as Gtk.Widget); + + if (has_visible_child ()) + show (); + } + + public bool has_visible_child () + { + return (entry.image != null && entry.image.get_visible ()) || + (entry.label != null && entry.label.get_visible ()); + } + + public void visibility_changed_cb (Gtk.Widget widget) + { + visible = has_visible_child (); + } +} + +public class MenuBar : Gtk.MenuBar +{ + public Background? background { get; construct; default = null; } + public bool high_contrast { get; private set; default = false; } + public Gtk.Window? keyboard_window { get; private set; default = null; } + public Gtk.AccelGroup? accel_group { get; construct; } + + private static const int HEIGHT = 24; + + public MenuBar (Background bg, Gtk.AccelGroup ag) + { + Object (background: bg, accel_group: ag); + } + + public override bool draw (Cairo.Context c) + { + if (background != null) + { + int x, y; + background.translate_coordinates (this, 0, 0, out x, out y); + c.save (); + c.translate (x, y); + background.draw_full (c, Background.DrawFlags.NONE); + c.restore (); + } + + c.set_source_rgb (0.1, 0.1, 0.1); + c.paint_with_alpha (0.4); + + foreach (var child in get_children ()) + { + propagate_draw (child, c); + } + + return false; + } + + /* Due to LP #973922 the keyboard has to be loaded after the main window + * is shown and given focus. Therefore we don't enable the active state + * until now. + */ + public void set_keyboard_state () + { + onscreen_keyboard_item.set_active (UGSettings.get_boolean (UGSettings.KEY_ONSCREEN_KEYBOARD)); + } + + private string default_theme_name; + private List<Indicator.Object> indicator_objects; + private Gtk.CheckMenuItem high_contrast_item; + private Pid keyboard_pid = 0; + private Pid reader_pid = 0; + private Gtk.CheckMenuItem onscreen_keyboard_item; + + construct + { + Gtk.Settings.get_default ().get ("gtk-theme-name", out default_theme_name); + + pack_direction = Gtk.PackDirection.RTL; + + if (UGSettings.get_boolean (UGSettings.KEY_SHOW_HOSTNAME)) + { + var label = new Gtk.Label (Posix.utsname ().nodename); + label.show (); + var hostname_item = new Gtk.MenuItem (); + hostname_item.add (label); + hostname_item.sensitive = false; + hostname_item.right_justified = true; + hostname_item.show (); + append (hostname_item); + + /* Hack to get a label showing on the menubar */ + label.ensure_style (); + var fg = label.get_style_context ().get_color (Gtk.StateFlags.NORMAL); + label.override_color (Gtk.StateFlags.INSENSITIVE, fg); + } + + /* Prevent dragging the window by the menubar */ + try + { + var style = new Gtk.CssProvider (); + style.load_from_data ("* {-GtkWidget-window-dragging: false;}", -1); + get_style_context ().add_provider (style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } + catch (Error e) + { + debug ("Internal error loading menubar style: %s", e.message); + } + + setup_indicators (); + + UnityGreeter.singleton.starting_session.connect (cleanup); + } + + private void close_pid (ref Pid pid) + { + if (pid > 0) + { + Posix.kill (pid, Posix.SIGTERM); + int status; + Posix.waitpid (pid, out status, 0); + pid = 0; + } + } + + public void cleanup () + { + close_pid (ref keyboard_pid); + close_pid (ref reader_pid); + } + + public override void get_preferred_height (out int min, out int nat) + { + min = HEIGHT; + nat = HEIGHT; + } + + private void greeter_set_env (string key, string val) + { + GLib.Environment.set_variable (key, val, true); + + /* And also set it in the DBus activation environment so that any + * indicator services pick it up. */ + try + { + var proxy = new GLib.DBusProxy.for_bus_sync (GLib.BusType.SESSION, + GLib.DBusProxyFlags.NONE, null, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + null); + + var builder = new GLib.VariantBuilder (GLib.VariantType.ARRAY); + builder.add ("{ss}", key, val); + + proxy.call_sync ("UpdateActivationEnvironment", new GLib.Variant ("(a{ss})", builder), GLib.DBusCallFlags.NONE, -1, null); + } + catch (Error e) + { + warning ("Could not get set environment for indicators: %s", e.message); + return; + } + } + + private Gtk.Widget make_a11y_indicator () + { + var a11y_item = new Gtk.MenuItem (); + var hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 3); + hbox.show (); + a11y_item.add (hbox); + var image = new Gtk.Image.from_file (Path.build_filename (Config.PKGDATADIR, "a11y.svg")); + image.show (); + hbox.add (image); + a11y_item.show (); + a11y_item.set_submenu (new Gtk.Menu () as Gtk.Widget); + onscreen_keyboard_item = new Gtk.CheckMenuItem.with_label (_("Onscreen keyboard")); + onscreen_keyboard_item.toggled.connect (keyboard_toggled_cb); + onscreen_keyboard_item.show (); + unowned Gtk.Menu submenu = a11y_item.submenu; + submenu.append (onscreen_keyboard_item); + high_contrast_item = new Gtk.CheckMenuItem.with_label (_("High Contrast")); + high_contrast_item.toggled.connect (high_contrast_toggled_cb); + high_contrast_item.add_accelerator ("activate", accel_group, Gdk.Key.h, Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE); + high_contrast_item.show (); + submenu.append (high_contrast_item); + high_contrast_item.set_active (UGSettings.get_boolean (UGSettings.KEY_HIGH_CONTRAST)); + var item = new Gtk.CheckMenuItem.with_label (_("Screen Reader")); + item.toggled.connect (screen_reader_toggled_cb); + item.add_accelerator ("activate", accel_group, Gdk.Key.s, Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE); + item.show (); + submenu.append (item); + item.set_active (UGSettings.get_boolean (UGSettings.KEY_SCREEN_READER)); + return a11y_item; + } + + private Indicator.Object? load_indicator_file (string indicator_name) + { + string dir = Config.INDICATOR_FILE_DIR; + string path; + Indicator.Object io; + + /* To stay backwards compatible, use com.canonical.indicator as the default prefix */ + if (indicator_name.index_of_char ('.') < 0) + path = @"$dir/com.canonical.indicator.$indicator_name"; + else + path = @"$dir/$indicator_name"; + + try + { + io = new Indicator.Ng.for_profile (path, "desktop_greeter"); + } + catch (FileError error) + { + /* the calling code handles file-not-found; don't warn here */ + return null; + } + catch (Error error) + { + warning ("unable to load %s: %s", indicator_name, error.message); + return null; + } + + return io; + } + + private Indicator.Object? load_indicator_library (string indicator_name) + { + // Find file, if it exists + string[] names_to_try = {"lib" + indicator_name + ".so", + indicator_name + ".so", + indicator_name}; + foreach (var filename in names_to_try) + { + var full_path = Path.build_filename (Config.INDICATORDIR, filename); + var io = new Indicator.Object.from_file (full_path); + if (io != null) + return io; + } + + return null; + } + + private void load_indicator (string indicator_name) + { + if (indicator_name == "ug-accessibility") + { + var a11y_item = make_a11y_indicator (); + insert (a11y_item, (int) get_children ().length () - 1); + } + else + { + var io = load_indicator_file (indicator_name); + + if (io == null) + io = load_indicator_library (indicator_name); + + if (io != null) + { + indicator_objects.append (io); + io.entry_added.connect (indicator_added_cb); + io.entry_removed.connect (indicator_removed_cb); + foreach (var entry in io.get_entries ()) + indicator_added_cb (io, entry); + } + } + } + + private void setup_indicators () + { + /* Set indicators to run with reduced functionality */ + greeter_set_env ("INDICATOR_GREETER_MODE", "1"); + + /* Don't allow virtual file systems? */ + greeter_set_env ("GIO_USE_VFS", "local"); + greeter_set_env ("GVFS_DISABLE_FUSE", "1"); + + /* Hint to have unity-settings-daemon run in greeter mode */ + greeter_set_env ("RUNNING_UNDER_GDM", "1"); + + /* Let indicators know about our unique dbus name */ + try + { + var conn = Bus.get_sync (BusType.SESSION); + greeter_set_env ("UNITY_GREETER_DBUS_NAME", conn.get_unique_name ()); + } + catch (IOError e) + { + debug ("Could not set DBUS_NAME: %s", e.message); + } + + debug ("LANG=%s LANGUAGE=%s", Environment.get_variable ("LANG"), Environment.get_variable ("LANGUAGE")); + + var indicator_list = UGSettings.get_strv(UGSettings.KEY_INDICATORS); + + var update_indicator_list = false; + for (var i = 0; i < indicator_list.length; i++) + { + if (indicator_list[i] == "ug-keyboard") + { + indicator_list[i] = "com.canonical.indicator.keyboard"; + update_indicator_list = true; + } + } + + if (update_indicator_list) + UGSettings.set_strv(UGSettings.KEY_INDICATORS, indicator_list); + + foreach (var indicator in indicator_list) + load_indicator(indicator); + + indicator_objects.sort((a, b) => { + int pos_a = a.get_position (); + int pos_b = b.get_position (); + + if (pos_a < 0) + pos_a = 1000; + if (pos_b < 0) + pos_b = 1000; + + return pos_a - pos_b; + }); + + debug ("LANG=%s LANGUAGE=%s", Environment.get_variable ("LANG"), Environment.get_variable ("LANGUAGE")); + } + + private void keyboard_toggled_cb (Gtk.CheckMenuItem item) + { + /* FIXME: The below would be sufficient if gnome-session were running + * to notice and run a screen keyboard in /etc/xdg/autostart... But + * since we're not running gnome-session, we hardcode onboard here. */ + /* var settings = new Settings ("org.gnome.desktop.a11y.applications");*/ + /*settings.set_boolean ("screen-keyboard-enabled", item.active);*/ + + UGSettings.set_boolean (UGSettings.KEY_ONSCREEN_KEYBOARD, item.active); + + if (keyboard_window == null) + { + int id = 0; + + try + { + string[] argv; + int onboard_stdout_fd; + + Shell.parse_argv ("onboard --xid", out argv); + Process.spawn_async_with_pipes (null, + argv, + null, + SpawnFlags.SEARCH_PATH, + null, + out keyboard_pid, + null, + out onboard_stdout_fd, + null); + var f = FileStream.fdopen (onboard_stdout_fd, "r"); + var stdout_text = new char[1024]; + if (f.gets (stdout_text) != null) + id = int.parse ((string) stdout_text); + + } + catch (Error e) + { + warning ("Error setting up keyboard: %s", e.message); + return; + } + + var keyboard_socket = new Gtk.Socket (); + keyboard_socket.show (); + keyboard_window = new Gtk.Window (); + keyboard_window.accept_focus = false; + keyboard_window.focus_on_map = false; + keyboard_window.add (keyboard_socket); + keyboard_socket.add_id (id); + + /* Put keyboard at the bottom of the screen */ + var screen = get_screen (); + var monitor = screen.get_monitor_at_window (get_window ()); + Gdk.Rectangle geom; + screen.get_monitor_geometry (monitor, out geom); + keyboard_window.move (geom.x, geom.y + geom.height - 200); + keyboard_window.resize (geom.width, 200); + } + + keyboard_window.visible = item.active; + } + + private void high_contrast_toggled_cb (Gtk.CheckMenuItem item) + { + var settings = Gtk.Settings.get_default (); + if (item.active) + settings.set ("gtk-theme-name", "HighContrastInverse"); + else + settings.set ("gtk-theme-name", default_theme_name); + high_contrast = item.active; + UGSettings.set_boolean (UGSettings.KEY_HIGH_CONTRAST, high_contrast); + } + + private void screen_reader_toggled_cb (Gtk.CheckMenuItem item) + { + /* FIXME: The below would be sufficient if gnome-session were running + * to notice and run a screen reader in /etc/xdg/autostart... But + * since we're not running gnome-session, we hardcode orca here. + /*var settings = new Settings ("org.gnome.desktop.a11y.applications");*/ + /*settings.set_boolean ("screen-reader-enabled", item.active);*/ + + UGSettings.set_boolean (UGSettings.KEY_SCREEN_READER, item.active); + + /* Hardcoded orca: */ + if (item.active) + { + try + { + string[] argv; + Shell.parse_argv ("orca --replace --no-setup --disable splash-window,", out argv); + Process.spawn_async (null, + argv, + null, + SpawnFlags.SEARCH_PATH, + null, + out reader_pid); + // This is a workaroud for bug https://launchpad.net/bugs/944159 + // The problem is that orca seems to not notice that it's in a + // password field on startup. We just need to kick orca in the + // pants. We do this two ways: a racy way and a non-racy way. + // We kick it after a second which is ideal if we win the race, + // because the user gets to hear what widget they are in, and + // the first character will be masked. Otherwise, if we lose + // that race, the first time the user types (see + // DashEntry.key_press_event), we will kick orca again. While + // this is not racy with orca startup, it is racy with whether + // orca will read the first character or not out loud. Hence + // why we do both. Ideally this would be fixed in orca itself. + UnityGreeter.singleton.orca_needs_kick = true; + Timeout.add_seconds (1, () => { + Signal.emit_by_name ((get_toplevel () as Gtk.Window).get_focus ().get_accessible (), "focus-event", true); + return false; + }); + } + catch (Error e) + { + warning ("Failed to run Orca: %s", e.message); + } + } + else + close_pid (ref reader_pid); + } + + private uint get_indicator_index (Indicator.Object object) + { + uint index = 0; + + foreach (var io in indicator_objects) + { + if (io == object) + return index; + index++; + } + + return index; + } + + private Indicator.Object? get_indicator_object_from_entry (Indicator.ObjectEntry entry) + { + foreach (var io in indicator_objects) + { + foreach (var e in io.get_entries ()) + { + if (e == entry) + return io; + } + } + + return null; + } + + private void indicator_added_cb (Indicator.Object object, Indicator.ObjectEntry entry) + { + var index = get_indicator_index (object); + var pos = 0; + foreach (var child in get_children ()) + { + if (!(child is IndicatorMenuItem)) + break; + + var menuitem = (IndicatorMenuItem) child; + var child_object = get_indicator_object_from_entry (menuitem.entry); + var child_index = get_indicator_index (child_object); + if (child_index > index) + break; + pos++; + } + + debug ("Adding indicator object %p at position %d", entry, pos); + + var menuitem = new IndicatorMenuItem (entry); + insert (menuitem, pos); + } + + private void indicator_removed_cb (Indicator.Object object, Indicator.ObjectEntry entry) + { + debug ("Removing indicator object %p", entry); + + foreach (var child in get_children ()) + { + var menuitem = (IndicatorMenuItem) child; + if (menuitem.entry == entry) + { + remove (child); + return; + } + } + + warning ("Indicator object %p not in menubar", entry); + } +} diff --git a/src/prompt-box.vala b/src/prompt-box.vala new file mode 100644 index 0000000..894870d --- /dev/null +++ b/src/prompt-box.vala @@ -0,0 +1,663 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011,2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Robert Ancell <robert.ancell@canonical.com> + * Michael Terry <michael.terry@canonical.com> + */ + +public class PromptBox : FadableBox +{ + public signal void respond (string[] response); + public signal void login (); + public signal void show_options (); + public signal void name_clicked (); + + public bool has_errors { get; set; default = false; } + public string id { get; construct; } + + public string label + { + get { return name_label.label; } + set + { + name_label.label = value; + small_name_label.label = value; + } + } + + public double position { get; set; default = 0; } + + private Gtk.Fixed fixed; + private Gtk.Widget zone; /* when overlapping zone we are fully expanded */ + + /* Expanded widgets */ + protected Gtk.Grid box_grid; + protected Gtk.Grid name_grid; + private ActiveIndicator active_indicator; + protected FadingLabel name_label; + protected FlatButton option_button; + private CachedImage option_image; + private CachedImage message_image; + + /* Condensed widgets */ + protected Gtk.Widget small_box_widget; + private ActiveIndicator small_active_indicator; + protected FadingLabel small_name_label; + private CachedImage small_message_image; + + protected static const int COL_ACTIVE = 0; + protected static const int COL_CONTENT = 1; + protected static const int COL_SPACER = 2; + + protected static const int ROW_NAME = 0; + protected static const int COL_NAME_LABEL = 0; + protected static const int COL_NAME_MESSAGE = 1; + protected static const int COL_NAME_OPTIONS = 2; + + protected static const int COL_ENTRIES_START = 1; + protected static const int COL_ENTRIES_END = 1; + protected static const int COL_ENTRIES_WIDTH = 1; + + protected int start_row; + protected int last_row; + + private enum PromptVisibility + { + HIDDEN, + FADING, + SHOWN, + } + private PromptVisibility prompt_visibility = PromptVisibility.HIDDEN; + + public PromptBox (string id) + { + Object (id: id); + } + + construct + { + set_start_row (); + reset_last_row (); + expand = true; + + fixed = new Gtk.Fixed (); + fixed.show (); + add (fixed); + + box_grid = new Gtk.Grid (); + box_grid.column_spacing = 4; + box_grid.row_spacing = 3; + box_grid.margin_top = GreeterList.BORDER; + box_grid.margin_bottom = 6; + box_grid.expand = true; + + /** Grid layout: + 0 1 2 3 4 + > Name M S < + Message....... + Entry......... + */ + + active_indicator = new ActiveIndicator (); + active_indicator.valign = Gtk.Align.START; + active_indicator.margin_top = (grid_size - ActiveIndicator.HEIGHT) / 2; + active_indicator.show (); + box_grid.attach (active_indicator, COL_ACTIVE, last_row, 1, 1); + + /* Add a second one on right just for equal-spacing purposes */ + var dummy_indicator = new ActiveIndicator (); + dummy_indicator.show (); + box_grid.attach (dummy_indicator, COL_SPACER, last_row, 1, 1); + + box_grid.show (); + + /* Create fully expanded version of ourselves */ + name_grid = create_name_grid (); + box_grid.attach (name_grid, COL_CONTENT, last_row, 1, 1); + + /* Now prep small versions of the above normal widgets. These are + * used when scrolling outside of the main dash box. */ + var small_box_grid = new Gtk.Grid (); + small_box_grid.column_spacing = 4; + small_box_grid.row_spacing = 6; + small_box_grid.hexpand = true; + small_box_grid.show (); + + small_active_indicator = new ActiveIndicator (); + small_active_indicator.valign = Gtk.Align.START; + small_active_indicator.margin_top = (grid_size - ActiveIndicator.HEIGHT) / 2; + small_active_indicator.show (); + small_box_grid.attach (small_active_indicator, 0, 0, 1, 1); + + var small_name_grid = create_small_name_grid (); + small_box_grid.attach (small_name_grid, 1, 0, 1, 1); + + /* Add a second indicator on right just for equal-spacing purposes */ + var small_dummy_indicator = new ActiveIndicator (); + small_dummy_indicator.show (); + small_box_grid.attach (small_dummy_indicator, 3, 0, 1, 1); + + var small_box_eventbox = new Gtk.EventBox (); + small_box_eventbox.visible_window = false; + small_box_eventbox.button_release_event.connect (() => + { + name_clicked (); + return true; + }); + small_box_eventbox.add (small_box_grid); + small_box_eventbox.show (); + small_box_widget = small_box_eventbox; + + fixed.add (small_box_widget); + fixed.add (box_grid); + } + + protected virtual Gtk.Grid create_name_grid () + { + var name_grid = new Gtk.Grid (); + name_grid.column_spacing = 4; + name_grid.hexpand = true; + + name_label = new FadingLabel (""); + name_label.override_font (Pango.FontDescription.from_string ("Ubuntu 13")); + name_label.override_color (Gtk.StateFlags.NORMAL, { 1.0f, 1.0f, 1.0f, 1.0f }); + name_label.valign = Gtk.Align.START; + name_label.vexpand = true; + name_label.yalign = 0.5f; + name_label.xalign = 0.0f; + name_label.margin_left = 2; + name_label.set_size_request (-1, grid_size); + name_label.show (); + name_grid.attach (name_label, COL_NAME_LABEL, ROW_NAME, 1, 1); + + message_image = new CachedImage (null); + try + { + message_image.pixbuf = new Gdk.Pixbuf.from_file (Path.build_filename (Config.PKGDATADIR, "message.png", null)); + } + catch (Error e) + { + debug ("Error loading message image: %s", e.message); + } + + var align = new Gtk.Alignment (0.5f, 0.5f, 0.0f, 0.0f); + align.valign = Gtk.Align.START; + align.set_size_request (-1, grid_size); + align.add (message_image); + align.show (); + name_grid.attach (align, COL_NAME_MESSAGE, ROW_NAME, 1, 1); + + option_button = new FlatButton (); + option_button.hexpand = true; + option_button.halign = Gtk.Align.END; + option_button.valign = Gtk.Align.START; + // Keep as much space on top as on the right + option_button.margin_top = ActiveIndicator.WIDTH + box_grid.column_spacing; + option_button.focus_on_click = false; + option_button.relief = Gtk.ReliefStyle.NONE; + option_button.get_accessible ().set_name (_("Session Options")); + option_button.clicked.connect (option_button_clicked_cb); + option_image = new CachedImage (null); + option_image.show (); + try + { + var style = new Gtk.CssProvider (); + style.load_from_data ("* {padding: 2px;}", -1); + option_button.get_style_context ().add_provider (style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } + catch (Error e) + { + debug ("Internal error loading session chooser style: %s", e.message); + } + option_button.add (option_image); + name_grid.attach (option_button, COL_NAME_OPTIONS, ROW_NAME, 1, 1); + + name_grid.show (); + + return name_grid; + } + + protected virtual Gtk.Grid create_small_name_grid () + { + var small_name_grid = new Gtk.Grid (); + small_name_grid.column_spacing = 4; + + small_name_label = new FadingLabel (""); + small_name_label.override_font (Pango.FontDescription.from_string ("Ubuntu 13")); + small_name_label.override_color (Gtk.StateFlags.NORMAL, { 1.0f, 1.0f, 1.0f, 1.0f }); + small_name_label.yalign = 0.5f; + small_name_label.xalign = 0.0f; + small_name_label.margin_left = 2; + small_name_label.set_size_request (-1, grid_size); + small_name_label.show (); + small_name_grid.attach (small_name_label, 1, 0, 1, 1); + + small_message_image = new CachedImage (null); + small_message_image.pixbuf = message_image.pixbuf; + + var align = new Gtk.Alignment (0.5f, 0.5f, 0.0f, 0.0f); + align.set_size_request (-1, grid_size); + align.add (small_message_image); + align.show (); + small_name_grid.attach (align, 2, 0, 1, 1); + + small_name_grid.show (); + return small_name_grid; + } + + protected virtual void set_start_row () + { + start_row = 0; + } + + protected virtual void reset_last_row () + { + last_row = start_row; + } + + private int round_to_grid (int size) + { + var num_grids = size / grid_size; + var remainder = size % grid_size; + if (remainder > 0) + num_grids += 1; + num_grids = int.max (num_grids, 3); + return num_grids * grid_size; + } + + public override void get_preferred_height (out int min, out int nat) + { + base.get_preferred_height (out min, out nat); + min = round_to_grid (min + GreeterList.BORDER * 2) - GreeterList.BORDER * 2; + nat = round_to_grid (nat + GreeterList.BORDER * 2) - GreeterList.BORDER * 2; + } + + public void set_zone (Gtk.Widget zone) + { + this.zone = zone; + queue_draw (); + } + + public void set_options_image (Gdk.Pixbuf? image) + { + if (option_button == null) + return; + + option_image.pixbuf = image; + + if (image == null) + option_button.hide (); + else + option_button.show (); + } + + private void option_button_clicked_cb (Gtk.Button button) + { + show_options (); + } + + public void set_show_message_icon (bool show) + { + message_image.visible = show; + small_message_image.visible = show; + } + + public void set_is_active (bool active) + { + active_indicator.active = active; + small_active_indicator.active = active; + } + + protected void foreach_prompt_widget (Gtk.Callback cb) + { + var prompt_widgets = new List<Gtk.Widget> (); + + var i = start_row + 1; + while (i <= last_row) + { + var c = box_grid.get_child_at (COL_ENTRIES_START, i); + if (c != null) /* c might have been deleted from selective clear */ + prompt_widgets.append (c); + i++; + } + + foreach (var w in prompt_widgets) + cb (w); + } + + public void clear () + { + prompt_visibility = PromptVisibility.HIDDEN; + foreach_prompt_widget ((w) => { w.destroy (); }); + reset_last_row (); + has_errors = false; + } + + /* Clears error messages */ + public void reset_messages () + { + has_errors = false; + foreach_prompt_widget ((w) => + { + var is_error = w.get_data<bool> ("prompt-box-is-error"); + if (is_error) + w.destroy (); + }); + } + + /* Stops spinners */ + public void reset_spinners () + { + foreach_prompt_widget ((w) => + { + if (w is DashEntry) + { + var e = w as DashEntry; + e.did_respond = false; + } + }); + } + + /* Clears error messages and stops spinners. Basically gets the box back to a filled-by-user-but-no-status state. */ + public void reset_state () + { + reset_messages (); + reset_spinners (); + } + + public virtual void add_static_prompts () + { + /* Subclasses may want to add prompts that are always present here */ + } + + private void update_prompt_visibility (Gtk.Widget w) + { + switch (prompt_visibility) + { + case PromptVisibility.HIDDEN: + w.hide (); + break; + case PromptVisibility.FADING: + var f = w as Fadable; + w.sensitive = true; + if (f != null) + f.fade_in (); + else + w.show (); + break; + case PromptVisibility.SHOWN: + w.show (); + w.sensitive = true; + break; + } + } + + public void fade_in_prompts () + { + prompt_visibility = PromptVisibility.FADING; + show (); + foreach_prompt_widget ((w) => { update_prompt_visibility (w); }); + } + + public void show_prompts () + { + prompt_visibility = PromptVisibility.SHOWN; + show (); + foreach_prompt_widget ((w) => { update_prompt_visibility (w); }); + } + + protected void attach_item (Gtk.Widget w, bool add_style_class = true) + { + w.set_data ("prompt-box-widget", this); + if (add_style_class) + UnityGreeter.add_style_class (w); + + last_row += 1; + box_grid.attach (w, COL_ENTRIES_START, last_row, COL_ENTRIES_WIDTH, 1); + + update_prompt_visibility (w); + queue_resize (); + } + + public void add_message (string text, bool is_error) + { + var label = new FadingLabel (text); + + label.override_font (Pango.FontDescription.from_string ("Ubuntu 10")); + + Gdk.RGBA color = { 1.0f, 1.0f, 1.0f, 1.0f }; + if (is_error) + color.parse ("#df382c"); + label.override_color (Gtk.StateFlags.NORMAL, color); + + label.xalign = 0.0f; + label.set_data<bool> ("prompt-box-is-error", is_error); + + attach_item (label); + + if (is_error) + has_errors = true; + } + + public DashEntry add_prompt (string text, string? accessible_text, bool is_secret) + { + /* Stop other entry's arrows/spinners from showing */ + foreach_prompt_widget ((w) => + { + if (w is DashEntry) + { + var e = w as DashEntry; + if (e != null) + e.can_respond = false; + } + }); + + var entry = new DashEntry (); + entry.sensitive = false; + + if (text.contains ("\n")) + { + add_message (text, false); + entry.constant_placeholder_text = ""; + } + else + { + /* Strip trailing colon if present (also handle CJK version) */ + var placeholder = text; + if (placeholder.has_suffix (":") || placeholder.has_suffix (":")) + { + var len = placeholder.char_count (); + placeholder = placeholder.substring (0, placeholder.index_of_nth_char (len - 1)); + } + entry.constant_placeholder_text = placeholder; + } + + var accessible = entry.get_accessible (); + if (accessible_text != null) + accessible.set_name (accessible_text); + else + accessible.set_name (text); + + if (is_secret) + { + entry.visibility = false; + entry.caps_lock_warning = true; + } + + entry.respond.connect (entry_activate_cb); + + attach_item (entry); + + return entry; + } + + public Gtk.ComboBox add_combo (GenericArray<string> texts, bool read_only) + { + Gtk.ComboBoxText combo; + if (read_only) + combo = new Gtk.ComboBoxText (); + else + combo = new Gtk.ComboBoxText.with_entry (); + + combo.get_style_context ().add_class ("lightdm-combo"); + combo.get_child ().get_style_context ().add_class ("lightdm-combo"); + combo.get_child ().override_font (Pango.FontDescription.from_string (DashEntry.font)); + + attach_item (combo, false); + + texts.foreach ((text) => { combo.append_text (text); }); + + if (texts.length > 0) + combo.active = 0; + + return combo; + } + + protected void entry_activate_cb () + { + var response = new string[0]; + + foreach_prompt_widget ((w) => + { + if (w is Gtk.Entry) + { + var e = w as Gtk.Entry; + if (e != null) + response += e.text; + } + }); + respond (response); + } + + public void add_button (string text, string? accessible_text) + { + var button = new DashButton (text); + + var accessible = button.get_accessible (); + accessible.set_name (accessible_text); + + button.clicked.connect (button_clicked_cb); + + attach_item (button); + } + + private void button_clicked_cb (Gtk.Button button) + { + login (); + } + + public override void grab_focus () + { + var done = false; + Gtk.Widget best = null; + foreach_prompt_widget ((w) => + { + if (done) + return; + best = w; /* last entry wins, all else considered */ + var e = w as Gtk.Entry; + var b = w as Gtk.Button; + var c = w as Gtk.ComboBox; + + /* We've found ideal entry (first empty one), so stop looking */ + if ((e != null && e.text == "") || b != null || c != null) + done = true; + }); + if (best != null) + best.grab_focus (); + } + + public override void size_allocate (Gtk.Allocation allocation) + { + base.size_allocate (allocation); + box_grid.size_allocate (allocation); + + int small_height; + small_box_widget.get_preferred_height (null, out small_height); + allocation.height = small_height; + small_box_widget.size_allocate (allocation); + } + + public override void draw_full_alpha (Cairo.Context c) + { + /* Draw either small or normal version of ourselves, depending on where + our allocation put us relative to our zone */ + int x, y; + zone.translate_coordinates (this, 0, 0, out x, out y); + + Gtk.Allocation alloc, zone_alloc; + this.get_allocation (out alloc); + zone.get_allocation (out zone_alloc); + + /* Draw main grid only in that area */ + c.save (); + c.rectangle (x, y, zone_alloc.width, zone_alloc.height); + c.clip (); + fixed.propagate_draw (box_grid, c); + c.restore (); + + /* Do actual drawing */ + c.save (); + if (y > 0) + c.rectangle (x, 0, zone_alloc.width, y); + else + c.rectangle (x, y + zone_alloc.height, zone_alloc.width, -y); + c.clip (); + fixed.propagate_draw (small_box_widget, c); + c.restore (); + } +} + +private class ActiveIndicator : Gtk.Image +{ + public bool active { get; set; } + public static const int WIDTH = 8; + public static const int HEIGHT = 7; + + construct + { + var filename = Path.build_filename (Config.PKGDATADIR, "active.png"); + try + { + pixbuf = new Gdk.Pixbuf.from_file (filename); + } + catch (Error e) + { + debug ("Could not load active image: %s", e.message); + } + notify["active"].connect (() => { queue_draw (); }); + xalign = 0.0f; + } + + public override void get_preferred_width (out int min, out int nat) + { + min = WIDTH; + nat = min; + } + + public override void get_preferred_height (out int min, out int nat) + { + min = HEIGHT; + nat = min; + } + + public override bool draw (Cairo.Context c) + { + if (!active) + return false; + return base.draw (c); + } +} diff --git a/src/remote-login-service.vala b/src/remote-login-service.vala new file mode 100644 index 0000000..7099ac2 --- /dev/null +++ b/src/remote-login-service.vala @@ -0,0 +1,54 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +protected struct RemoteServerField +{ + public string type; + public bool required; + public Variant default_value; + public HashTable<string, Variant> properties; +} + +protected struct RemoteServerApplication +{ + public string application_id; + public int pin_position; +} + +protected struct RemoteServer +{ + public string type; + public string name; + public string url; + public bool last_used_server; + public RemoteServerField[] fields; + public RemoteServerApplication[] applications; +} + +[DBus (name = "com.canonical.RemoteLogin")] +interface RemoteLoginService : Object +{ + public abstract async void get_servers (out RemoteServer[] serverList) throws IOError; + public abstract async void get_servers_for_login (string url, string emailAddress, string password, bool allowCache, out bool loginSuccess, out string dataType, out RemoteServer[] serverList) throws IOError; + public abstract async void get_cached_domains_for_server (string url, out string[] domains) throws IOError; + public abstract async void set_last_used_server (string uccsUrl, string serverUrl) throws IOError; + + public signal void servers_updated (RemoteServer[] serverList); + public signal void login_servers_updated (string url, string emailAddress, string dataType, RemoteServer[] serverList); + public signal void login_changed (string url, string emailAddress); +} diff --git a/src/session-list.vala b/src/session-list.vala new file mode 100644 index 0000000..7127023 --- /dev/null +++ b/src/session-list.vala @@ -0,0 +1,158 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Michael Terry <michael.terry@canonical.com> + */ + +public class SessionPrompt : PromptBox +{ + public string session { get; construct; } + public string default_session { get; construct; } + + public SessionPrompt (string id, string? session, string? default_session) + { + Object (id: id, session: session, default_session: default_session); + } + + private ToggleBox box; + + construct + { + label = _("Select desktop environment"); + name_label.vexpand = false; + + box = new ToggleBox (default_session, session); + + if (UnityGreeter.singleton.test_mode) + { + box.add_item ("gnome", "GNOME", SessionList.get_badge ("gnome")); + box.add_item ("kde", "KDE", SessionList.get_badge ("kde")); + box.add_item ("ubuntu", "Ubuntu", SessionList.get_badge ("ubuntu")); + } + else + { + foreach (var session in LightDM.get_sessions ()) + { + debug ("Adding session %s (%s)", session.key, session.name); + box.add_item (session.key, session.name, SessionList.get_badge (session.key)); + } + } + + box.notify["selected-key"].connect (selected_cb); + box.show (); + + attach_item (box); + } + + private void selected_cb () + { + respond ({ box.selected_key }); + } +} + +public class SessionList : GreeterList +{ + public signal void session_clicked (string session); + public string session { get; construct; } + public string default_session { get; construct; } + + private SessionPrompt prompt; + + public SessionList (Background bg, MenuBar mb, string? session, string? default_session) + { + Object (background: bg, menubar: mb, session: session, default_session: default_session); + } + + construct + { + prompt = add_session_prompt ("session"); + } + + private SessionPrompt add_session_prompt (string id) + { + var e = new SessionPrompt (id, session, default_session); + e.respond.connect ((responses) => { session_clicked (responses[0]); }); + add_entry (e); + return e; + } + + protected override void add_manual_entry () {} + public override void show_authenticated (bool successful = true) {} + + private static string? get_badge_name (string session) + { + switch (session) + { + case "ubuntu": + case "ubuntu-2d": + return "ubuntu_badge.png"; + case "gnome-classic": + case "gnome-flashback-compiz": + case "gnome-flashback": + case "gnome-fallback-compiz": + case "gnome-fallback": + case "gnome-shell": + case "gnome": + return "gnome_badge.png"; + case "kde": + case "kde-plasma": + return "kde_badge.png"; + case "xterm": + return "recovery_console_badge.png"; + case "remote-login": + return "remote_login_help.png"; + default: + return null; + } + } + + private static HashTable<string, Gdk.Pixbuf> badges; /* cache of badges */ + public static Gdk.Pixbuf? get_badge (string session) + { + var name = get_badge_name (session); + + if (name == null) + { + /* Not a known name, but let's see if we have a custom badge before + giving up entirely and using the unknown badget. */ + var maybe_name = "custom_%s_badge.png".printf (session); + var maybe_path = Path.build_filename (Config.PKGDATADIR, maybe_name, null); + if (FileUtils.test (maybe_path, FileTest.EXISTS)) + name = maybe_name; + else + name = "unknown_badge.png"; + } + + if (badges == null) + badges = new HashTable<string, Gdk.Pixbuf> (str_hash, str_equal); + + var pixbuf = badges.lookup (name); + if (pixbuf == null) + { + try + { + pixbuf = new Gdk.Pixbuf.from_file (Path.build_filename (Config.PKGDATADIR, name, null)); + badges.insert (name, pixbuf); + } + catch (Error e) + { + debug ("Error loading badge %s: %s", name, e.message); + } + } + + return pixbuf; + } +} diff --git a/src/settings-daemon.vala b/src/settings-daemon.vala new file mode 100644 index 0000000..9cf3908 --- /dev/null +++ b/src/settings-daemon.vala @@ -0,0 +1,234 @@ +/* -*- Mode:Vala; indent-tabs-mode:nil; tab-width:4 -*- + * + * Copyright (C) 2011 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authored by: Michael Terry <michael.terry@canonical.com> + */ + +public class SettingsDaemon : Object +{ + private int logind_inhibit_fd = -1; + private ScreenSaverInterface screen_saver; + private SessionManagerInterface session_manager; + private int n_names = 0; + + public void start () + { + string[] disabled = { "org.gnome.settings-daemon.plugins.background", + "org.gnome.settings-daemon.plugins.clipboard", + "org.gnome.settings-daemon.plugins.font", + "org.gnome.settings-daemon.plugins.gconf", + "org.gnome.settings-daemon.plugins.gsdwacom", + "org.gnome.settings-daemon.plugins.housekeeping", + "org.gnome.settings-daemon.plugins.keybindings", + "org.gnome.settings-daemon.plugins.keyboard", + "org.gnome.settings-daemon.plugins.media-keys", + "org.gnome.settings-daemon.plugins.mouse", + "org.gnome.settings-daemon.plugins.print-notifications", + "org.gnome.settings-daemon.plugins.smartcard", + "org.gnome.settings-daemon.plugins.sound", + "org.gnome.settings-daemon.plugins.wacom" }; + + string[] enabled = { "org.gnome.settings-daemon.plugins.a11y-keyboard", + "org.gnome.settings-daemon.plugins.a11y-settings", + "org.gnome.settings-daemon.plugins.color", + "org.gnome.settings-daemon.plugins.cursor", + "org.gnome.settings-daemon.plugins.power", + "org.gnome.settings-daemon.plugins.xrandr", + "org.gnome.settings-daemon.plugins.xsettings" }; + + foreach (var schema in disabled) + set_plugin_enabled (schema, false); + + foreach (var schema in enabled) + set_plugin_enabled (schema, true); + + /* Pretend to be GNOME session */ + session_manager = new SessionManagerInterface (); + n_names++; + GLib.Bus.own_name (BusType.SESSION, "org.gnome.SessionManager", BusNameOwnerFlags.NONE, + (c) => + { + try + { + c.register_object ("/org/gnome/SessionManager", session_manager); + } + catch (Error e) + { + warning ("Failed to register /org/gnome/SessionManager: %s", e.message); + } + }, + () => + { + debug ("Acquired org.gnome.SessionManager"); + start_settings_daemon (); + }, + () => debug ("Failed to acquire name org.gnome.SessionManager")); + + /* The power plugin does the screen saver screen blanking and disables + * the builtin X screen saver. It relies on gnome-screensaver to generate + * the event to trigger this (which actually comes from gnome-session). + * We implement the gnome-screensaver inteface and start the settings + * daemon once it is registered on the bus so gnome-screensaver is not + * started when it accesses this interface */ + screen_saver = new ScreenSaverInterface (); + n_names++; + GLib.Bus.own_name (BusType.SESSION, "org.gnome.ScreenSaver", BusNameOwnerFlags.NONE, + (c) => + { + try + { + c.register_object ("/org/gnome/ScreenSaver", screen_saver); + } + catch (Error e) + { + warning ("Failed to register /org/gnome/ScreenSaver: %s", e.message); + } + }, + () => + { + debug ("Acquired org.gnome.ScreenSaver"); + start_settings_daemon (); + }, + () => debug ("Failed to acquire name org.gnome.ScreenSaver")); + + /* The media-keys plugin inhibits the power key, but we don't want + all the other keys doing things. So inhibit it ourselves */ + /* NOTE: We are using the synchronous method here since there is a bug in Vala/GLib in that + * g_dbus_connection_call_with_unix_fd_list_finish and g_dbus_proxy_call_with_unix_fd_list_finish + * don't have the GAsyncResult as the second argument. + * https://bugzilla.gnome.org/show_bug.cgi?id=688907 + */ + try + { + var b = Bus.get_sync (BusType.SYSTEM); + UnixFDList fd_list; + var result = b.call_with_unix_fd_list_sync ("org.freedesktop.login1", + "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", + "Inhibit", + new Variant ("(ssss)", + "handle-power-key", + Environment.get_user_name (), + "Unity Greeter handling keypresses", + "block"), + new VariantType ("(h)"), + DBusCallFlags.NONE, + -1, + null, + out fd_list); + int32 index = -1; + result.get ("(h)", &index); + logind_inhibit_fd = fd_list.get (index); + } + catch (Error e) + { + warning ("Failed to inhibit power keys: %s", e.message); + } + } + + private void set_plugin_enabled (string schema_name, bool enabled) + { + var source = SettingsSchemaSource.get_default (); + var schema = source.lookup (schema_name, false); + if (schema != null) + { + var settings = new Settings (schema_name); + settings.set_boolean ("active", enabled); + } + } + + private void start_settings_daemon () + { + n_names--; + if (n_names != 0) + return; + + debug ("All bus names acquired, starting unity-settings-daemon"); + + try + { + Process.spawn_command_line_async (Config.USD_BINARY); + } + catch (SpawnError e) + { + debug ("Could not start unity-settings-daemon: %s", e.message); + } + } +} + +[DBus (name="org.gnome.ScreenSaver")] +public class ScreenSaverInterface : Object +{ + public signal void active_changed (bool value); + + private Gnome.IdleMonitor idle_monitor; + private bool _active = false; + private uint idle_watch = 0; + + public ScreenSaverInterface () + { + idle_monitor = new Gnome.IdleMonitor (); + _set_active (false); + } + + private void _set_active (bool value) + { + _active = value; + if (idle_watch != 0) + idle_monitor.remove_watch (idle_watch); + idle_watch = 0; + if (value) + idle_monitor.add_user_active_watch (() => set_active (false)); + else + { + var timeout = UGSettings.get_integer (UGSettings.KEY_IDLE_TIMEOUT); + if (timeout > 0) + idle_watch = idle_monitor.add_idle_watch (timeout * 1000, () => set_active (true)); + } + } + + public void set_active (bool value) + { + if (_active == value) + return; + + if (value) + debug ("Screensaver activated"); + else + debug ("Screensaver disabled"); + + _set_active (value); + active_changed (value); + } + + public bool get_active () + { + return _active; + } + + public uint32 get_active_time () { return 0; } + public void lock () {} + public void show_message (string summary, string body, string icon) {} + public void simulate_user_activity () {} +} + +[DBus (name="org.gnome.SessionManager")] +public class SessionManagerInterface : Object +{ + public bool session_is_active { get { return true; } } + public string session_name { get { return "ubuntu"; } } + public uint32 inhibited_actions { get { return 0; } } +} diff --git a/src/settings.vala b/src/settings.vala new file mode 100644 index 0000000..ebdc9e7 --- /dev/null +++ b/src/settings.vala @@ -0,0 +1,101 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011,2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Robert Ancell <robert.ancell@canonical.com> + * Michael Terry <michael.terry@canonical.com> + */ + +public class UGSettings +{ + public static const string KEY_BACKGROUND = "background"; + public static const string KEY_BACKGROUND_COLOR = "background-color"; + public static const string KEY_DRAW_USER_BACKGROUNDS = "draw-user-backgrounds"; + public static const string KEY_DRAW_GRID = "draw-grid"; + public static const string KEY_SHOW_HOSTNAME = "show-hostname"; + public static const string KEY_LOGO = "logo"; + public static const string KEY_BACKGROUND_LOGO = "background-logo"; + public static const string KEY_THEME_NAME = "theme-name"; + public static const string KEY_ICON_THEME_NAME = "icon-theme-name"; + public static const string KEY_FONT_NAME = "font-name"; + public static const string KEY_XFT_ANTIALIAS = "xft-antialias"; + public static const string KEY_XFT_DPI = "xft-dpi"; + public static const string KEY_XFT_HINTSTYLE = "xft-hintstyle"; + public static const string KEY_XFT_RGBA = "xft-rgba"; + public static const string KEY_ONSCREEN_KEYBOARD = "onscreen-keyboard"; + public static const string KEY_HIGH_CONTRAST = "high-contrast"; + public static const string KEY_SCREEN_READER = "screen-reader"; + public static const string KEY_PLAY_READY_SOUND = "play-ready-sound"; + public static const string KEY_INDICATORS = "indicators"; + public static const string KEY_HIDDEN_USERS = "hidden-users"; + public static const string KEY_IDLE_TIMEOUT = "idle-timeout"; + + public static bool get_boolean (string key) + { + var gsettings = new Settings (SCHEMA); + return gsettings.get_boolean (key); + } + + /* LP: 1006497 - utility function to make sure we have the key before trying to read it (which will segfault if the key isn't there) */ + public static bool safe_get_boolean (string key, bool default) + { + var gsettings = new Settings (SCHEMA); + string[] keys = gsettings.list_keys (); + foreach (var k in keys) + if (k == key) + return gsettings.get_boolean (key); + + /* key not in child list */ + return default; + } + + public static int get_integer (string key) + { + var gsettings = new Settings (SCHEMA); + return gsettings.get_int (key); + } + + public static double get_double (string key) + { + var gsettings = new Settings (SCHEMA); + return gsettings.get_double (key); + } + + public static string get_string (string key) + { + var gsettings = new Settings (SCHEMA); + return gsettings.get_string (key); + } + + public static bool set_boolean (string key, bool value) + { + var gsettings = new Settings (SCHEMA); + return gsettings.set_boolean (key, value); + } + + public static string[] get_strv (string key) + { + var gsettings = new Settings (SCHEMA); + return gsettings.get_strv (key); + } + + public static bool set_strv (string key, string[] value) + { + var gsettings = new Settings (SCHEMA); + return gsettings.set_strv (key, value); + } + + private static const string SCHEMA = "com.canonical.unity-greeter"; +} diff --git a/src/shutdown-dialog.vala b/src/shutdown-dialog.vala new file mode 100644 index 0000000..73e1bf4 --- /dev/null +++ b/src/shutdown-dialog.vala @@ -0,0 +1,622 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2013 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Robert Ancell <robert.ancell@canonical.com> + * Marco Trevisan <marco.trevisan@canonical.com> + */ + +public enum ShutdownDialogType +{ + LOGOUT, + SHUTDOWN, + RESTART +} + +public class ShutdownDialog : Gtk.Fixed +{ + public signal void closed (); + + private Cairo.ImageSurface? bg_surface = null; + private Cairo.ImageSurface? corner_surface = null; + private Cairo.ImageSurface? left_surface = null; + private Cairo.ImageSurface? top_surface = null; + private Cairo.Pattern? corner_pattern = null; + private Cairo.Pattern? left_pattern = null; + private Cairo.Pattern? top_pattern = null; + + private const int BORDER_SIZE = 30; + private const int BORDER_INTERNAL_SIZE = 10; + private const int BORDER_EXTERNAL_SIZE = BORDER_SIZE - BORDER_INTERNAL_SIZE; + private const int CLOSE_OFFSET = 3; + private const int BUTTON_TEXT_SPACE = 9; + private const int BLUR_RADIUS = 8; + + private Monitor monitor; + private weak Background background; + private Gdk.RGBA avg_color; + + private Gtk.Box vbox; + private DialogButton close_button; + private Gtk.Box button_box; + private Gtk.EventBox monitor_events; + private Gtk.EventBox vbox_events; + + private AnimateTimer animation; + private bool closing = false; + + + public ShutdownDialog (ShutdownDialogType type, Background bg) + { + background = bg; + background.notify["alpha"].connect (rebuild_background); + background.notify["average-color"].connect (update_background_color); + update_background_color (); + + // This event box covers the monitor size, and closes the dialog on click. + monitor_events = new Gtk.EventBox (); + monitor_events.visible = true; + monitor_events.set_visible_window (false); + monitor_events.events |= Gdk.EventMask.BUTTON_PRESS_MASK; + monitor_events.button_press_event.connect (() => { + close (); + return true; + }); + add (monitor_events); + + vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 10); + vbox.visible = true; + + vbox.margin = BORDER_INTERNAL_SIZE; + vbox.margin_top += 9; + vbox.margin_left += 20; + vbox.margin_right += 20; + vbox.margin_bottom += 2; + + // This event box consumes the click events inside the vbox + vbox_events = new Gtk.EventBox(); + vbox_events.visible = true; + vbox_events.set_visible_window (false); + vbox_events.events |= Gdk.EventMask.BUTTON_PRESS_MASK; + vbox_events.button_press_event.connect (() => { return true; }); + vbox_events.add (vbox); + monitor_events.add (vbox_events); + + string text; + + if (type == ShutdownDialogType.SHUTDOWN) + { + text = _("Goodbye. Would you like to…"); + } + else + { + var title_label = new Gtk.Label (_("Shut Down")); + title_label.visible = true; + title_label.override_font (Pango.FontDescription.from_string ("Ubuntu Light 15")); + title_label.override_color (Gtk.StateFlags.NORMAL, { 1.0f, 1.0f, 1.0f, 1.0f }); + title_label.set_alignment (0.0f, 0.5f); + vbox.pack_start (title_label, false, false, 0); + + text = _("Are you sure you want to shut down the computer?"); + } + + var have_open_sessions = false; + try + { + var b = Bus.get_sync (BusType.SYSTEM); + var result = b.call_sync ("org.freedesktop.DisplayManager", + "/org/freedesktop/DisplayManager", + "org.freedesktop.DBus.Properties", + "Get", + new Variant ("(ss)", "org.freedesktop.DisplayManager", "Sessions"), + new VariantType ("(v)"), + DBusCallFlags.NONE, + -1, + null); + Variant value; + result.get ("(v)", out value); + have_open_sessions = value.n_children () > 0; + } + catch (Error e) + { + warning ("Failed to check sessions from logind: %s", e.message); + } + if (have_open_sessions) + text = "%s\n\n%s".printf (_("Other users are currently logged in to this computer, shutting down now will also close these other sessions."), text); + + var label = new Gtk.Label (text); + label.set_line_wrap (true); + label.override_font (Pango.FontDescription.from_string ("Ubuntu Light 12")); + label.override_color (Gtk.StateFlags.NORMAL, { 1.0f, 1.0f, 1.0f, 1.0f }); + label.set_alignment (0.0f, 0.5f); + label.visible = true; + vbox.pack_start (label, false, false, 0); + + button_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 20); + button_box.visible = true; + vbox.pack_start (button_box, false, false, 0); + + if (type == ShutdownDialogType.SHUTDOWN) + { + if (LightDM.get_can_suspend ()) + { + var button = add_button (_("Suspend"), Path.build_filename (Config.PKGDATADIR, "suspend.png"), Path.build_filename (Config.PKGDATADIR, "suspend_highlight.png")); + button.clicked.connect (() => + { + try + { + LightDM.suspend (); + close (); + } + catch (Error e) + { + warning ("Failed to suspend: %s", e.message); + } + }); + } + + if (LightDM.get_can_hibernate ()) + { + var button = add_button (_("Hibernate"), Path.build_filename (Config.PKGDATADIR, "hibernate.png"), Path.build_filename (Config.PKGDATADIR, "hibernate_highlight.png")); + button.clicked.connect (() => + { + try + { + LightDM.hibernate (); + close (); + } + catch (Error e) + { + warning ("Failed to hibernate: %s", e.message); + } + }); + } + } + + if (LightDM.get_can_restart ()) + { + var button = add_button (_("Restart"), Path.build_filename (Config.PKGDATADIR, "restart.png"), Path.build_filename (Config.PKGDATADIR, "restart_highlight.png")); + button.clicked.connect (() => + { + try + { + LightDM.restart (); + close (); + } + catch (Error e) + { + warning ("Failed to restart: %s", e.message); + } + }); + } + + if (LightDM.get_can_shutdown ()) + { + var button = add_button (_("Shut Down"), Path.build_filename (Config.PKGDATADIR, "shutdown.png"), Path.build_filename (Config.PKGDATADIR, "shutdown_highlight.png")); + button.clicked.connect (() => + { + try + { + LightDM.shutdown (); + close (); + } + catch (Error e) + { + warning ("Failed to shutdown: %s", e.message); + } + }); + + if (type != ShutdownDialogType.SHUTDOWN) + show.connect(() => { button.grab_focus (); }); + } + + close_button = new DialogButton (Path.build_filename (Config.PKGDATADIR, "dialog_close.png"), Path.build_filename (Config.PKGDATADIR, "dialog_close_highlight.png"), Path.build_filename (Config.PKGDATADIR, "dialog_close_press.png")); + close_button.can_focus = false; + close_button.clicked.connect (() => { close (); }); + close_button.visible = true; + add (close_button); + + animation = new AnimateTimer ((x) => { return x; }, AnimateTimer.INSTANT); + animation.animate.connect (() => { queue_draw (); }); + show.connect (() => { animation.reset(); }); + } + + public void close () + { + var start_value = 1.0f - animation.progress; + animation = new AnimateTimer ((x) => { return start_value + x; }, AnimateTimer.INSTANT); + animation.animate.connect ((p) => + { + queue_draw (); + + if (p >= 1.0f) + { + animation.stop (); + closed (); + } + }); + + closing = true; + animation.reset(); + } + + private void rebuild_background () + { + bg_surface = null; + queue_draw (); + } + + private void update_background_color () + { + // Apply the same color corrections we do in Unity + // For reference, see unity's unity-shared/BGHash.cpp + double hue, saturation, value; + const double COLOR_ALPHA = 0.72f; + + Gdk.RGBA color = background.average_color; + Gtk.RGB.to_hsv (color.red, color.green, color.blue, + out hue, out saturation, out value); + + if (saturation < 0.08) + { + // Got a grayscale image + avg_color = {0.18f, 0.20f, 0.21f, COLOR_ALPHA }; + } + else + { + const Gdk.RGBA[] cmp_colors = + { + {84/255.0f, 14/255.0f, 68/255.0f, 1.0f}, + {110/255.0f, 11/255.0f, 42/255.0f, 1.0f}, + {132/255.0f, 22/255.0f, 23/255.0f, 1.0f}, + {132/255.0f, 55/255.0f, 27/255.0f, 1.0f}, + {134/255.0f, 77/255.0f, 32/255.0f, 1.0f}, + {133/255.0f, 127/255.0f, 49/255.0f, 1.0f}, + {29/255.0f, 99/255.0f, 49/255.0f, 1.0f}, + {17/255.0f, 88/255.0f, 46/255.0f, 1.0f}, + {14/255.0f, 89/255.0f, 85/255.0f, 1.0f}, + {25/255.0f, 43/255.0f, 89/255.0f, 1.0f}, + {27/255.0f, 19/255.0f, 76/255.0f, 1.0f}, + {2/255.0f, 192/255.0f, 212/255.0f, 1.0f} + }; + + avg_color = {0, 0, 0, 1}; + double closest_diff = 200.0f; + + foreach (var c in cmp_colors) + { + double cmp_hue, cmp_sat, cmp_value; + Gtk.RGB.to_hsv (c.red, c.green, c.blue, + out cmp_hue, out cmp_sat, out cmp_value); + double color_diff = Math.fabs (hue - cmp_hue); + + if (color_diff < closest_diff) + { + avg_color = c; + closest_diff = color_diff; + } + } + + double new_hue, new_saturation, new_value; + Gtk.RGB.to_hsv (avg_color.red, avg_color.green, avg_color.blue, + out new_hue, out new_saturation, out new_value); + + saturation = double.min (saturation, new_saturation); + saturation *= (2.0f - saturation); + value = double.min (double.min (value, new_value), 0.26f); + Gtk.HSV.to_rgb (hue, saturation, value, + out avg_color.red, out avg_color.green, out avg_color.blue); + avg_color.alpha = COLOR_ALPHA; + } + + rebuild_background (); + } + + public void set_active_monitor (Monitor m) + { + if (m == this.monitor || m.equals (this.monitor)) + return; + + monitor = m; + rebuild_background (); + set_size_request (monitor.width, monitor.height); + } + + public void focus_next () + { + (get_toplevel () as Gtk.Window).move_focus (Gtk.DirectionType.TAB_FORWARD); + } + + public void focus_prev () + { + (get_toplevel () as Gtk.Window).move_focus (Gtk.DirectionType.TAB_BACKWARD); + } + + public void cancel () + { + var widget = (get_toplevel () as Gtk.Window).get_focus (); + if (widget is DialogButton) + (get_toplevel () as Gtk.Window).set_focus (null); + else + close (); + } + + public override void size_allocate (Gtk.Allocation allocation) + { + base.size_allocate (allocation); + monitor_events.size_allocate (allocation); + + var content_allocation = Gtk.Allocation (); + int minimum_width, natural_width, minimum_height, natural_height; + vbox_events.get_preferred_width (out minimum_width, out natural_width); + vbox_events.get_preferred_height_for_width (minimum_width, out minimum_height, out natural_height); + content_allocation.x = allocation.x + (allocation.width - minimum_width) / 2; + content_allocation.y = allocation.y + (allocation.height - minimum_height) / 2; + content_allocation.width = minimum_width; + content_allocation.height = minimum_height; + vbox_events.size_allocate (content_allocation); + + var a = Gtk.Allocation (); + close_button.get_preferred_width (out minimum_width, out natural_width); + close_button.get_preferred_height (out minimum_height, out natural_height); + a.x = content_allocation.x - BORDER_EXTERNAL_SIZE + CLOSE_OFFSET; + a.y = content_allocation.y - BORDER_EXTERNAL_SIZE + CLOSE_OFFSET; + a.width = minimum_width; + a.height = minimum_height; + close_button.size_allocate (a); + } + + public override bool draw (Cairo.Context c) + { + if (corner_surface == null) + { + corner_surface = new Cairo.ImageSurface.from_png (Path.build_filename (Config.PKGDATADIR, "switcher_corner.png")); + left_surface = new Cairo.ImageSurface.from_png (Path.build_filename (Config.PKGDATADIR, "switcher_left.png")); + top_surface = new Cairo.ImageSurface.from_png (Path.build_filename (Config.PKGDATADIR, "switcher_top.png")); + corner_pattern = new Cairo.Pattern.for_surface (corner_surface); + left_pattern = new Cairo.Pattern.for_surface (left_surface); + left_pattern.set_extend (Cairo.Extend.REPEAT); + top_pattern = new Cairo.Pattern.for_surface (top_surface); + top_pattern.set_extend (Cairo.Extend.REPEAT); + } + + int width = vbox_events.get_allocated_width (); + int height = vbox_events.get_allocated_height (); + int x = (get_allocated_width () - width) / 2; + int y = (get_allocated_height () - height) / 2; + + if (animation.is_running) + c.push_group (); + + /* Darken background */ + c.set_source_rgba (0, 0, 0, 0.25); + c.paint (); + + if (bg_surface == null || animation.is_running) + { + /* Create a new blurred surface of the current surface */ + bg_surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, width, height); + var bg_cr = new Cairo.Context (bg_surface); + + bg_cr.set_source_surface (c.get_target (), -x - monitor.x, -y - monitor.y); + bg_cr.rectangle (0, 0, width, height); + bg_cr.fill (); + + CairoUtils.ExponentialBlur.surface (bg_surface, BLUR_RADIUS); + } + + /* Background */ + c.save (); + c.translate (x, y); + + CairoUtils.rounded_rectangle (c, 0, 0, width, height, 4); + c.set_source_surface (bg_surface, 0, 0); + c.fill_preserve (); + c.set_source_rgba (avg_color.red, avg_color.green, avg_color.blue, avg_color.alpha); + c.fill (); + + c.restore(); + + /* Draw borders */ + x -= BORDER_EXTERNAL_SIZE; + y -= BORDER_EXTERNAL_SIZE; + width += BORDER_EXTERNAL_SIZE * 2; + height += BORDER_EXTERNAL_SIZE * 2; + + c.save (); + c.translate (x, y); + + /* Top left */ + var m = Cairo.Matrix.identity (); + corner_pattern.set_matrix (m); + c.set_source (corner_pattern); + c.rectangle (0, 0, BORDER_SIZE, BORDER_SIZE); + c.fill (); + + /* Top right */ + m = Cairo.Matrix.identity (); + m.translate (width, 0); + m.scale (-1, 1); + corner_pattern.set_matrix (m); + c.set_source (corner_pattern); + c.rectangle (width - BORDER_SIZE, 0, BORDER_SIZE, BORDER_SIZE); + c.fill (); + + /* Bottom left */ + m = Cairo.Matrix.identity (); + m.translate (0, height); + m.scale (1, -1); + corner_pattern.set_matrix (m); + c.set_source (corner_pattern); + c.rectangle (0, height - BORDER_SIZE, BORDER_SIZE, BORDER_SIZE); + c.fill (); + + /* Bottom right */ + m = Cairo.Matrix.identity (); + m.translate (width, height); + m.scale (-1, -1); + corner_pattern.set_matrix (m); + c.set_source (corner_pattern); + c.rectangle (width - BORDER_SIZE, height - BORDER_SIZE, BORDER_SIZE, BORDER_SIZE); + c.fill (); + + /* Left */ + m = Cairo.Matrix.identity (); + left_pattern.set_matrix (m); + c.set_source (left_pattern); + c.rectangle (0, BORDER_SIZE, BORDER_SIZE, height - BORDER_SIZE * 2); + c.fill (); + + /* Right */ + m = Cairo.Matrix.identity (); + m.translate (width, 0); + m.scale (-1, 1); + left_pattern.set_matrix (m); + c.set_source (left_pattern); + c.rectangle (width - BORDER_SIZE, BORDER_SIZE, BORDER_SIZE, height - BORDER_SIZE * 2); + c.fill (); + + /* Top */ + m = Cairo.Matrix.identity (); + top_pattern.set_matrix (m); + c.set_source (top_pattern); + c.rectangle (BORDER_SIZE, 0, width - BORDER_SIZE * 2, BORDER_SIZE); + c.fill (); + + /* Bottom */ + m = Cairo.Matrix.identity (); + m.translate (0, height); + m.scale (1, -1); + top_pattern.set_matrix (m); + c.set_source (top_pattern); + c.rectangle (BORDER_SIZE, height - BORDER_SIZE, width - BORDER_SIZE * 2, BORDER_SIZE); + c.fill (); + + c.restore (); + + var ret = base.draw (c); + + if (animation.is_running) + { + c.pop_group_to_source (); + c.paint_with_alpha (closing ? 1.0f - animation.progress : animation.progress); + } + + return ret; + } + + private DialogButton add_button (string text, string inactive_filename, string active_filename) + { + var b = new Gtk.Box (Gtk.Orientation.VERTICAL, BUTTON_TEXT_SPACE); + b.visible = true; + button_box.pack_start (b, false, false, 0); + + var label = new Gtk.Label (text); + var button = new DialogButton (inactive_filename, active_filename, null, label); + button.visible = true; + + b.pack_start (button, false, false, 0); + b.pack_start (label, false, false, 0); + + return button; + } +} + +private class DialogButton : Gtk.Button +{ + private string inactive_filename; + private string focused_filename; + private string? active_filename; + private Gtk.Image i; + private Gtk.Label? l; + + public DialogButton (string inactive_filename, string focused_filename, string? active_filename, Gtk.Label? label = null) + { + this.inactive_filename = inactive_filename; + this.focused_filename = focused_filename; + this.active_filename = active_filename; + relief = Gtk.ReliefStyle.NONE; + focus_on_click = false; + i = new Gtk.Image.from_file (inactive_filename); + i.visible = true; + add (i); + + l = label; + + if (l != null) + { + l.visible = true; + l.override_font (Pango.FontDescription.from_string ("Ubuntu Light 12")); + l.override_color (Gtk.StateFlags.NORMAL, { 1.0f, 1.0f, 1.0f, 0.0f }); + l.override_color (Gtk.StateFlags.FOCUSED, { 1.0f, 1.0f, 1.0f, 1.0f }); + l.override_color (Gtk.StateFlags.ACTIVE, { 1.0f, 1.0f, 1.0f, 1.0f }); + this.get_accessible ().set_name (l.get_text ()); + } + + UnityGreeter.add_style_class (this); + try + { + // Remove the default GtkButton paddings and border + var style = new Gtk.CssProvider (); + style.load_from_data ("* {padding: 0px 0px 0px 0px; border: 0px; }", -1); + get_style_context ().add_provider (style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } + catch (Error e) + { + debug ("Internal error loading session chooser style: %s", e.message); + } + } + + public override bool enter_notify_event (Gdk.EventCrossing event) + { + grab_focus (); + return base.enter_notify_event (event); + } + + public override bool leave_notify_event (Gdk.EventCrossing event) + { + (get_toplevel () as Gtk.Window).set_focus (null); + return base.leave_notify_event (event); + } + + public override bool draw (Cairo.Context c) + { + i.draw (c); + return true; + } + + public override void state_flags_changed (Gtk.StateFlags previous_state) + { + var new_flags = get_state_flags (); + + if ((new_flags & Gtk.StateFlags.PRELIGHT) != 0 && !can_focus || + (new_flags & Gtk.StateFlags.FOCUSED) != 0) + { + if ((new_flags & Gtk.StateFlags.ACTIVE) != 0 && active_filename != null) + i.set_from_file (active_filename); + else + i.set_from_file (focused_filename); + } + else + { + i.set_from_file (inactive_filename); + } + + if (l != null) + l.set_state_flags (new_flags, true); + + base.state_flags_changed (previous_state); + } +} diff --git a/src/toggle-box.vala b/src/toggle-box.vala new file mode 100644 index 0000000..1862951 --- /dev/null +++ b/src/toggle-box.vala @@ -0,0 +1,131 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Michael Terry <michael.terry@canonical.com> + */ + +public class ToggleBox : Gtk.Box +{ + public string default_key {get; construct;} + public string starting_key {get; construct;} + public string selected_key {get; protected set;} + + public ToggleBox (string? default_key, string? starting_key) + { + Object (default_key: default_key, starting_key: starting_key, + selected_key: starting_key); + } + + public void add_item (string key, string label, Gdk.Pixbuf? icon) + { + var item = make_button (key, label, icon); + + if (get_children () == null || + (starting_key == null && default_key == key) || + starting_key == key) + select (item); + + item.show (); + add (item); + } + + private Gtk.Button selected_button; + + construct + { + orientation = Gtk.Orientation.VERTICAL; + } + + public override bool draw (Cairo.Context c) + { + Gtk.Allocation allocation; + get_allocation (out allocation); + + CairoUtils.rounded_rectangle (c, 0, 0, allocation.width, + allocation.height, 0.1 * grid_size); + c.set_source_rgba (0.5, 0.5, 0.5, 0.5); + c.set_line_width (1); + c.stroke (); + + return base.draw (c); + } + + private void select (Gtk.Button button) + { + if (selected_button != null) + selected_button.relief = Gtk.ReliefStyle.NONE; + selected_button = button; + selected_button.relief = Gtk.ReliefStyle.NORMAL; + selected_key = selected_button.get_data<string> ("toggle-list-key"); + } + + private Gtk.Button make_button (string key, string name_in, Gdk.Pixbuf? icon) + { + var item = new FlatButton (); + item.relief = Gtk.ReliefStyle.NONE; + item.clicked.connect (button_clicked_cb); + + var hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); + + if (icon != null) + { + var image = new CachedImage (icon); + hbox.pack_start (image, false, false, 0); + } + + var name = name_in; + if (key == default_key) + { + /* Translators: %s is a session name like KDE or Ubuntu */ + name = _("%s (Default)").printf (name); + } + + var label = new Gtk.Label (null); + label.set_markup ("<span font=\"Ubuntu 13\">%s</span>".printf (name)); + label.halign = Gtk.Align.START; + hbox.pack_start (label, true, true, 0); + + item.hexpand = true; + item.add (hbox); + hbox.show_all (); + + try + { + /* Tighten padding on buttons to not be so large */ + var style = new Gtk.CssProvider (); + style.load_from_data ("* {padding: 8px;}", -1); + item.get_style_context ().add_provider (style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } + catch (Error e) + { + debug ("Internal error loading session chooser style: %s", e.message); + } + + item.set_data<string> ("toggle-list-key", key); + return item; + } + + private void button_clicked_cb (Gtk.Button button) + { + selected_key = button.get_data<string> ("toggle-list-key"); + } + + public override void grab_focus () + { + if (selected_button != null) + selected_button.grab_focus (); + } +} diff --git a/src/unity-greeter.vala b/src/unity-greeter.vala new file mode 100644 index 0000000..cf501af --- /dev/null +++ b/src/unity-greeter.vala @@ -0,0 +1,660 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authored by: Robert Ancell <robert.ancell@canonical.com> + */ + +public const int grid_size = 40; + +public class UnityGreeter +{ + public static UnityGreeter singleton; + + public signal void show_message (string text, LightDM.MessageType type); + public signal void show_prompt (string text, LightDM.PromptType type); + public signal void authentication_complete (); + public signal void starting_session (); + + public bool test_mode = false; + + private string state_file; + private KeyFile state; + + private Cairo.XlibSurface background_surface; + + private SettingsDaemon settings_daemon; + + public bool orca_needs_kick; + private MainWindow main_window; + + private LightDM.Greeter greeter; + + private Canberra.Context canberra_context; + + private static Timer log_timer; + + private DialogDBusInterface dbus_object; + + private UnityGreeter (bool test_mode_) + { + singleton = this; + test_mode = test_mode_; + + /* Prepare to set the background */ + debug ("Creating background surface"); + background_surface = create_root_surface (Gdk.Screen.get_default ()); + + greeter = new LightDM.Greeter (); + greeter.show_message.connect ((text, type) => { show_message (text, type); }); + greeter.show_prompt.connect ((text, type) => { show_prompt (text, type); }); + greeter.autologin_timer_expired.connect (() => { greeter.authenticate_autologin (); }); + greeter.authentication_complete.connect (() => { authentication_complete (); }); + var connected = false; + try + { + connected = greeter.connect_sync (); + } + catch (Error e) + { + warning ("Failed to connect to LightDM daemon"); + } + if (!connected && !test_mode) + Posix.exit (Posix.EXIT_FAILURE); + + if (!test_mode) + { + settings_daemon = new SettingsDaemon (); + settings_daemon.start (); + } + + var state_dir = Path.build_filename (Environment.get_user_cache_dir (), "unity-greeter"); + DirUtils.create_with_parents (state_dir, 0775); + + var xdg_seat = GLib.Environment.get_variable("XDG_SEAT"); + var state_file_name = xdg_seat != null && xdg_seat != "seat0" ? xdg_seat + "-state" : "state"; + + state_file = Path.build_filename (state_dir, state_file_name); + state = new KeyFile (); + try + { + state.load_from_file (state_file, KeyFileFlags.NONE); + } + catch (Error e) + { + if (!(e is FileError.NOENT)) + warning ("Failed to load state from %s: %s\n", state_file, e.message); + } + + main_window = new MainWindow (); + + Bus.own_name (BusType.SESSION, "com.canonical.UnityGreeter", BusNameOwnerFlags.NONE); + + dbus_object = new DialogDBusInterface (); + dbus_object.open_dialog.connect ((type) => + { + ShutdownDialogType dialog_type; + switch (type) + { + default: + case 1: + dialog_type = ShutdownDialogType.LOGOUT; + break; + case 2: + dialog_type = ShutdownDialogType.RESTART; + break; + } + main_window.show_shutdown_dialog (dialog_type); + }); + dbus_object.close_dialog.connect ((type) => { main_window.close_shutdown_dialog (); }); + Bus.own_name (BusType.SESSION, "com.canonical.Unity", BusNameOwnerFlags.NONE, + (c) => + { + try + { + c.register_object ("/org/gnome/SessionManager/EndSessionDialog", dbus_object); + } + catch (Error e) + { + warning ("Failed to register /org/gnome/SessionManager/EndSessionDialog: %s", e.message); + } + }, + null, + () => debug ("Failed to acquire name com.canonical.Unity")); + + start_fake_wm (); + Gdk.threads_add_idle (ready_cb); + } + + public string? get_state (string key) + { + try + { + return state.get_value ("greeter", key); + } + catch (Error e) + { + return null; + } + } + + public void set_state (string key, string value) + { + state.set_value ("greeter", key, value); + var data = state.to_data (); + try + { + FileUtils.set_contents (state_file, data); + } + catch (Error e) + { + debug ("Failed to write state: %s", e.message); + } + } + + public void push_list (GreeterList widget) + { + main_window.push_list (widget); + } + + public void pop_list () + { + main_window.pop_list (); + } + + public static void add_style_class (Gtk.Widget widget) + { + /* Add style context class lightdm-user-list */ + var ctx = widget.get_style_context (); + ctx.add_class ("lightdm"); + } + + public bool start_session (string? session, Background bg) + { + /* Paint our background onto the root window before we close our own window */ + var c = new Cairo.Context (background_surface); + bg.draw_full (c, Background.DrawFlags.NONE); + c = null; + refresh_background (Gdk.Screen.get_default (), background_surface); + + if (test_mode) + { + debug ("Successfully logged in! Quitting..."); + Gtk.main_quit (); + return true; + } + + if (!session_is_valid (session)) + { + debug ("Session %s is not available, using system default %s instead", session, greeter.default_session_hint); + session = greeter.default_session_hint; + } + + var result = false; + try + { + result = LightDM.greeter_start_session_sync (greeter, session); + } + catch (Error e) + { + warning ("Failed to start session: %s", e.message); + } + + if (result) + starting_session (); + + return result; + } + + private bool session_is_valid (string? session) + { + if (session == null) + return true; + + foreach (var s in LightDM.get_sessions ()) + if (s.key == session) + return true; + + return false; + } + + private bool ready_cb () + { + debug ("starting system-ready sound"); + + /* Launch canberra */ + Canberra.Context.create (out canberra_context); + + if (UGSettings.get_boolean (UGSettings.KEY_PLAY_READY_SOUND)) + canberra_context.play (0, + Canberra.PROP_CANBERRA_XDG_THEME_NAME, + "ubuntu", + Canberra.PROP_EVENT_ID, + "system-ready"); + + return false; + } + + public void show () + { + debug ("Showing main window"); + main_window.show (); + main_window.get_window ().focus (Gdk.CURRENT_TIME); + main_window.set_keyboard_state (); + } + + public bool is_authenticated () + { + return greeter.is_authenticated; + } + + public void authenticate (string? userid = null) + { + greeter.authenticate (userid); + } + + public void authenticate_as_guest () + { + greeter.authenticate_as_guest (); + } + + public void authenticate_remote (string? session, string? userid) + { + UnityGreeter.singleton.greeter.authenticate_remote (session, userid); + } + + public void cancel_authentication () + { + greeter.cancel_authentication (); + } + + public void respond (string response) + { + greeter.respond (response); + } + + public string authentication_user () + { + return greeter.authentication_user; + } + + public string default_session_hint () + { + return greeter.default_session_hint; + } + + public string select_user_hint () + { + return greeter.select_user_hint; + } + + public bool show_manual_login_hint () + { + return greeter.show_manual_login_hint; + } + + public bool show_remote_login_hint () + { + return greeter.show_remote_login_hint; + } + + public bool hide_users_hint () + { + return greeter.hide_users_hint; + } + + public bool has_guest_account_hint () + { + return greeter.has_guest_account_hint; + } + + private Gdk.FilterReturn focus_upon_map (Gdk.XEvent gxevent, Gdk.Event event) + { + var xevent = (X.Event*)gxevent; + if (xevent.type == X.EventType.MapNotify) + { + var display = Gdk.x11_lookup_xdisplay (xevent.xmap.display); + var xwin = xevent.xmap.window; + var win = Gdk.X11Window.foreign_new_for_display (display, xwin); + if (win != null && !xevent.xmap.override_redirect) + { + /* Check to see if this window is our onboard window, since we don't want to focus it. */ + X.Window keyboard_xid = 0; + if (main_window.menubar.keyboard_window != null) + keyboard_xid = Gdk.X11Window.get_xid (main_window.menubar.keyboard_window.get_window ()); + + if (xwin != keyboard_xid && win.get_type_hint() != Gdk.WindowTypeHint.NOTIFICATION) + { + win.focus (Gdk.CURRENT_TIME); + + /* Make sure to keep keyboard above */ + if (main_window.menubar.keyboard_window != null) + main_window.menubar.keyboard_window.get_window ().raise (); + } + } + } + else if (xevent.type == X.EventType.UnmapNotify) + { + // Since we aren't keeping track of focus (for example, we don't + // track the Z stack of windows) like a normal WM would, when we + // decide here where to return focus after another window unmaps, + // we don't have much to go on. X will tell us if we should take + // focus back. (I could not find an obvious way to determine this, + // but checking if the X input focus is RevertTo.None seems + // reliable.) + + X.Window xwin; + int revert_to; + xevent.xunmap.display.get_input_focus (out xwin, out revert_to); + + if (revert_to == X.RevertTo.None) + { + main_window.get_window ().focus (Gdk.CURRENT_TIME); + + /* Make sure to keep keyboard above */ + if (main_window.menubar.keyboard_window != null) + main_window.menubar.keyboard_window.get_window ().raise (); + } + } + return Gdk.FilterReturn.CONTINUE; + } + + private void start_fake_wm () + { + /* We want new windows (e.g. the shutdown dialog) to gain focus. + We don't really need anything more than that (don't need alt-tab + since any dialog should be "modal" or at least dealt with before + continuing even if not actually marked as modal) */ + var root = Gdk.get_default_root_window (); + root.set_events (root.get_events () | Gdk.EventMask.SUBSTRUCTURE_MASK); + root.add_filter (focus_upon_map); + } + + private static Cairo.XlibSurface? create_root_surface (Gdk.Screen screen) + { + var visual = screen.get_system_visual (); + + unowned X.Display display = Gdk.X11Display.get_xdisplay (screen.get_display ()); + + var pixmap = X.CreatePixmap (display, + Gdk.X11Window.get_xid (screen.get_root_window ()), + screen.get_width (), + screen.get_height (), + visual.get_depth ()); + + /* Convert into a Cairo surface */ + var surface = new Cairo.XlibSurface (display, + pixmap, + Gdk.X11Visual.get_xvisual (visual), + screen.get_width (), screen.get_height ()); + + return surface; + } + + private static void refresh_background (Gdk.Screen screen, Cairo.XlibSurface surface) + { + Gdk.flush (); + + unowned X.Display display = Gdk.X11Display.get_xdisplay (screen.get_display ()); + + /* Ensure Cairo has actually finished its drawing */ + surface.flush (); + /* Use this pixmap for the background */ + X.SetWindowBackgroundPixmap (display, + Gdk.X11Window.get_xid (screen.get_root_window ()), + surface.get_drawable ()); + + X.ClearWindow (display, Gdk.X11Window.get_xid (screen.get_root_window ())); + } + + private static void log_cb (string? log_domain, LogLevelFlags log_level, string message) + { + string prefix; + switch (log_level & LogLevelFlags.LEVEL_MASK) + { + case LogLevelFlags.LEVEL_ERROR: + prefix = "ERROR:"; + break; + case LogLevelFlags.LEVEL_CRITICAL: + prefix = "CRITICAL:"; + break; + case LogLevelFlags.LEVEL_WARNING: + prefix = "WARNING:"; + break; + case LogLevelFlags.LEVEL_MESSAGE: + prefix = "MESSAGE:"; + break; + case LogLevelFlags.LEVEL_INFO: + prefix = "INFO:"; + break; + case LogLevelFlags.LEVEL_DEBUG: + prefix = "DEBUG:"; + break; + default: + prefix = "LOG:"; + break; + } + + stderr.printf ("[%+.2fs] %s %s\n", log_timer.elapsed (), prefix, message); + } + + public static int main (string[] args) + { + /* Protect memory from being paged to disk, as we deal with passwords */ + Posix.mlockall (Posix.MCL_CURRENT | Posix.MCL_FUTURE); + + /* Disable the stupid global menubar */ + Environment.unset_variable ("UBUNTU_MENUPROXY"); + + /* Initialize i18n */ + Intl.setlocale (LocaleCategory.ALL, ""); + Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR); + Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8"); + Intl.textdomain (Config.GETTEXT_PACKAGE); + + /* Set up the accessibility stack, in case the user needs it for screen reading etc. */ + Environment.set_variable ("GTK_MODULES", "atk-bridge", false); + + Pid atspi_pid = 0; + Pid upstart_pid = 0; + + try + { + string[] argv; + + Shell.parse_argv ("/usr/lib/at-spi2-core/at-spi-bus-launcher --launch-immediately", out argv); + Process.spawn_async (null, + argv, + null, + SpawnFlags.SEARCH_PATH, + null, + out atspi_pid); + } + catch (Error e) + { + warning ("Error starting the at-spi registry: %s", e.message); + } + + Gtk.init (ref args); + Ido.init (); + + log_timer = new Timer (); + Log.set_default_handler (log_cb); + + debug ("Starting unity-greeter %s UID=%d LANG=%s", Config.VERSION, (int) Posix.getuid (), Environment.get_variable ("LANG")); + + /* Set the cursor to not be the crap default */ + debug ("Setting cursor"); + Gdk.get_default_root_window ().set_cursor (new Gdk.Cursor (Gdk.CursorType.LEFT_PTR)); + + bool do_show_version = false; + bool do_test_mode = false; + OptionEntry versionOption = { "version", 'v', 0, OptionArg.NONE, ref do_show_version, + /* Help string for command line --version flag */ + N_("Show release version"), null }; + OptionEntry testOption = { "test-mode", 0, 0, OptionArg.NONE, ref do_test_mode, + /* Help string for command line --test-mode flag */ + N_("Run in test mode"), null }; + OptionEntry nullOption = { null }; + OptionEntry[] options = { versionOption, testOption, nullOption }; + + debug ("Loading command line options"); + var c = new OptionContext (/* Arguments and description for --help text */ + _("- Unity Greeter")); + c.add_main_entries (options, Config.GETTEXT_PACKAGE); + c.add_group (Gtk.get_option_group (true)); + try + { + c.parse (ref args); + } + catch (Error e) + { + stderr.printf ("%s\n", e.message); + stderr.printf (/* Text printed out when an unknown command-line argument provided */ + _("Run '%s --help' to see a full list of available command line options."), args[0]); + stderr.printf ("\n"); + return Posix.EXIT_FAILURE; + } + if (do_show_version) + { + /* Note, not translated so can be easily parsed */ + stderr.printf ("unity-greeter %s\n", Config.VERSION); + return Posix.EXIT_SUCCESS; + } + + if (do_test_mode) + debug ("Running in test mode"); + + /* Set GTK+ settings */ + debug ("Setting GTK+ settings"); + var settings = Gtk.Settings.get_default (); + var value = UGSettings.get_string (UGSettings.KEY_THEME_NAME); + if (value != "") + settings.set ("gtk-theme-name", value, null); + value = UGSettings.get_string (UGSettings.KEY_ICON_THEME_NAME); + if (value != "") + settings.set ("gtk-icon-theme-name", value, null); + value = UGSettings.get_string (UGSettings.KEY_FONT_NAME); + if (value != "") + settings.set ("gtk-font-name", value, null); + var double_value = UGSettings.get_double (UGSettings.KEY_XFT_DPI); + if (double_value != 0.0) + settings.set ("gtk-xft-dpi", (int) (1024 * double_value), null); + var boolean_value = UGSettings.get_boolean (UGSettings.KEY_XFT_ANTIALIAS); + settings.set ("gtk-xft-antialias", boolean_value, null); + value = UGSettings.get_string (UGSettings.KEY_XFT_HINTSTYLE); + if (value != "") + settings.set ("gtk-xft-hintstyle", value, null); + value = UGSettings.get_string (UGSettings.KEY_XFT_RGBA); + if (value != "") + settings.set ("gtk-xft-rgba", value, null); + + debug ("Creating Unity Greeter"); + var greeter = new UnityGreeter (do_test_mode); + + debug ("Showing greeter"); + greeter.show (); + + if (!do_test_mode) + { + /* Start the indicator services */ + try + { + string[] argv; + + Shell.parse_argv ("init --user --startup-event indicator-services-start", out argv); + Process.spawn_async (null, + argv, + null, + SpawnFlags.SEARCH_PATH, + null, + out upstart_pid); + } + catch (Error e) + { + warning ("Error starting Upstart for indicators: %s", e.message); + } + + /* Make nm-applet hide items the user does not have permissions to interact with */ + Environment.set_variable ("NM_APPLET_HIDE_POLICY_ITEMS", "1", true); + + try + { + Process.spawn_command_line_async ("nm-applet"); + } + catch (Error e) + { + warning ("Error starting nm-applet: %s", e.message); + } + } + + /* Setup a handler for TERM so we quit cleanly */ + GLib.Unix.signal_add(GLib.ProcessSignal.TERM, () => { + debug("Got a SIGTERM"); + Gtk.main_quit(); + return true; + }); + + debug ("Starting main loop"); + Gtk.main (); + + debug ("Cleaning up"); + + if (upstart_pid != 0) + { + Posix.kill (upstart_pid, Posix.SIGTERM); + int status; + Posix.waitpid (upstart_pid, out status, 0); + if (Process.if_exited (status)) + debug ("Upstart exited with return value %d", Process.exit_status (status)); + else + debug ("Upstart terminated with signal %d", Process.term_sig (status)); + upstart_pid = 0; + } + + if (atspi_pid != 0) + { + Posix.kill (atspi_pid, Posix.SIGKILL); + int status; + Posix.waitpid (atspi_pid, out status, 0); + if (Process.if_exited (status)) + debug ("AT-SPI exited with return value %d", Process.exit_status (status)); + else + debug ("AT-SPI terminated with signal %d", Process.term_sig (status)); + atspi_pid = 0; + } + + debug ("Exiting"); + + return Posix.EXIT_SUCCESS; + } +} + +[DBus (name="org.gnome.SessionManager.EndSessionDialog")] +public class DialogDBusInterface : Object +{ + public signal void open_dialog (uint32 type); + public signal void close_dialog (); + + public void open (uint32 type, uint32 timestamp, uint32 seconds_to_stay_open, ObjectPath[] inhibitor_object_paths) + { + open_dialog (type); + } + + public void close () + { + close_dialog (); + } +} diff --git a/src/user-list.vala b/src/user-list.vala new file mode 100644 index 0000000..5f2bceb --- /dev/null +++ b/src/user-list.vala @@ -0,0 +1,1603 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011,2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Robert Ancell <robert.ancell@canonical.com> + * Michael Terry <michael.terry@canonical.com> + */ + +int remote_server_field_sort_function (RemoteServerField? item1, RemoteServerField? item2) +{ + string[] sorted_fields = { "domain", "username", "email", "password" }; + foreach (var field in sorted_fields) + { + if (item1.type == field) + return -1; + if (item2.type == field) + return 1; + } + + return (item1.type < item2.type) ? -1 : 0; +} + +public class UserList : GreeterList +{ + private bool _offer_guest = false; + public bool offer_guest + { + get { return _offer_guest; } + set + { + _offer_guest = value; + if (value) + add_user ("*guest", _("Guest Session")); + else + remove_entry ("*guest"); + } + } + + private Gdk.Pixbuf message_pixbuf; + + private uint change_background_timeout = 0; + + private uint remote_login_service_watch; + private RemoteLoginService remote_login_service; + private List<RemoteServer?> remote_directory_server_list = new List<RemoteServer?> (); + private List<RemoteServer?> remote_login_server_list = new List<RemoteServer?> (); + private HashTable<string, Gtk.Widget> current_remote_fields; + private string currently_browsing_server_url; + private string currently_browsing_server_email; + private EmailAutocompleter remote_server_email_field_autocompleter; + + /* User to authenticate against */ + private string ?authenticate_user = null; + + private bool show_hidden_users_ = false; + public bool show_hidden_users + { + set + { + show_hidden_users_ = value; + + if (UnityGreeter.singleton.test_mode) + { + if (value) + add_user ("hidden", "Hidden User", null, false, false, null); + else + remove_entry ("hidden"); + return; + } + + var hidden_users = UGSettings.get_strv (UGSettings.KEY_HIDDEN_USERS); + if (!value) + { + foreach (var username in hidden_users) + remove_entry (username); + return; + } + + var users = LightDM.UserList.get_instance (); + foreach (var user in users.users) + { + foreach (var username in hidden_users) + { + if (user.name == username) + { + debug ("Showing hidden user %s", username); + user_added_cb (user); + } + } + } + } + + get + { + return show_hidden_users_; + } + } + + private string _default_session = "ubuntu"; + public string default_session + { + get + { + return _default_session; + } + set + { + _default_session = value; + if (selected_entry != null) + selected_entry.set_options_image (get_badge ()); + } + } + + private string? _session = null; + public string? session + { + get + { + return _session; + } + set + { + _session = value; + if (selected_entry != null) + selected_entry.set_options_image (get_badge ()); + } + } + + public UserList (Background bg, MenuBar mb) + { + Object (background: bg, menubar: mb); + } + + construct + { + menubar.notify["high-contrast"].connect (() => { change_background (); }); + entry_displayed_start.connect (() => { change_background (); }); + entry_displayed_done.connect (() => { change_background (); }); + + try + { + message_pixbuf = new Gdk.Pixbuf.from_file (Path.build_filename (Config.PKGDATADIR, "message.png", null)); + } + catch (Error e) + { + debug ("Error loading message image: %s", e.message); + } + + fill_list (); + + entry_selected.connect (entry_selected_cb); + + connect_to_lightdm (); + + if (!UnityGreeter.singleton.test_mode && + UnityGreeter.singleton.show_remote_login_hint ()) + remote_login_service_watch = Bus.watch_name (BusType.SESSION, + "com.canonical.RemoteLogin", + BusNameWatcherFlags.AUTO_START, + on_remote_login_service_appeared, + on_remote_login_service_vanished); + + } + + private void remove_remote_servers () + { + remote_directory_server_list = new List<RemoteServer?> (); + remote_login_server_list = new List<RemoteServer?> (); + remove_entries_with_prefix ("*remote"); + } + + private void remove_remote_login_servers () + { + remote_login_server_list = new List<RemoteServer?> (); + remove_entries_with_prefix ("*remote_login"); + + /* If we have no entries at all, we should show manual */ + if (!always_show_manual) + add_manual_entry (); + } + + private async void query_directory_servers () + { + try + { + RemoteServer[] server_list; + yield remote_login_service.get_servers (out server_list); + set_remote_directory_servers (server_list); + } + catch (IOError e) + { + debug ("Calling GetServers on com.canonical.RemoteLogin dbus service failed. Error: %s", e.message); + remove_remote_servers (); + } + } + + private string user_list_name_for_remote_directory_server (RemoteServer remote_server) + { + return "*remote_directory*" + remote_server.url; + } + + private string username_from_remote_server_fields(RemoteServer remote_server) + { + var username = ""; + foreach (var f in remote_server.fields) + { + if (f.type == "username" && f.default_value != null) + { + username = f.default_value.get_string (); + break; + } + } + return username; + } + + private string user_list_name_for_remote_login_server (RemoteServer remote_server) + { + var username = username_from_remote_server_fields (remote_server); + return "*remote_login*" + remote_server.url + "*" + username; + } + + private string url_from_remote_loding_server_list_name (string remote_server_list_name) + { + return remote_server_list_name.split ("*")[2]; + } + + private string username_from_remote_loding_server_list_name (string remote_server_list_name) + { + return remote_server_list_name.split ("*")[3]; + } + + private void set_remote_directory_servers (RemoteServer[] server_list) + { + /* Add new servers */ + foreach (var remote_server in server_list) + { + var list_name = user_list_name_for_remote_directory_server (remote_server); + if (find_entry (list_name) == null) + { + var e = new PromptBox (list_name); + e.label = remote_server.name; + e.respond.connect (remote_directory_respond_cb); + e.show_options.connect (show_remote_account_dialog); + add_entry (e); + + remote_directory_server_list.append (remote_server); + } + } + + /* Remove gone servers */ + unowned List<RemoteServer?> it = remote_directory_server_list; + while (it != null) + { + var remote_server = it.data; + var found = false; + for (int i = 0; !found && i < server_list.length; i++) + { + found = remote_server.url == server_list[i].url; + } + if (!found) + { + if (remote_server.url == currently_browsing_server_url) + { + /* The server we where "browsing" disappeared, so kill its children */ + remove_remote_login_servers (); + currently_browsing_server_url = ""; + currently_browsing_server_email = ""; + } + remove_entry (user_list_name_for_remote_directory_server (remote_server)); + unowned List<RemoteServer?> newIt = it.next; + remote_directory_server_list.delete_link (it); + it = newIt; + } + else + { + it = it.next; + } + } + + /* Remove manual option unless specified */ + if (remote_directory_server_list.length() > 0 && !always_show_manual) { + debug ("removing manual login since we have a remote login entry"); + remove_entry ("*other"); + } + } + + private PromptBox create_prompt_for_login_server (RemoteServer remote_server) + { + var e = new PromptBox (user_list_name_for_remote_login_server (remote_server)); + e.label = remote_server.name; + e.respond.connect (remote_login_respond_cb); + add_entry (e); + remote_login_server_list.append (remote_server); + + return e; + } + + private void remote_login_servers_updated (string url, string email_address, string data_type, RemoteServer[] server_list) + { + if (currently_browsing_server_url == url && currently_browsing_server_email == email_address) + { + /* Add new servers */ + foreach (var remote_server in server_list) + { + var list_name = user_list_name_for_remote_login_server (remote_server); + if (find_entry (list_name) == null) + create_prompt_for_login_server (remote_server); + } + + /* Remove gone servers */ + unowned List<RemoteServer?> it = remote_login_server_list; + while (it != null) + { + RemoteServer remote_server = it.data; + var found = false; + for (var i = 0; !found && i < server_list.length; i++) + found = remote_server.url == server_list[i].url; + if (!found) + { + remove_entry (user_list_name_for_remote_login_server (remote_server)); + unowned List<RemoteServer?> newIt = it.next; + remote_login_server_list.delete_link (it); + it = newIt; + } + else + { + it = it.next; + } + } + } + } + + private void remote_login_changed (string url, string email_address) + { + if (currently_browsing_server_url == url && currently_browsing_server_email == email_address) + { + /* Something happened and we are being asked for re-authentication by the remote-login-service */ + remove_remote_login_servers (); + currently_browsing_server_url = ""; + currently_browsing_server_email = ""; + + var directory_list_name = "*remote_directory*" + url; + set_active_entry (directory_list_name); + } + } + + private void on_remote_login_service_appeared (DBusConnection conn, string name) + { + Bus.get_proxy.begin<RemoteLoginService> (BusType.SESSION, + "com.canonical.RemoteLogin", + "/com/canonical/RemoteLogin", + 0, + null, + (obj, res) => { + try + { + remote_login_service = Bus.get_proxy.end<RemoteLoginService> (res); + remote_login_service.servers_updated.connect (set_remote_directory_servers); + remote_login_service.login_servers_updated.connect (remote_login_servers_updated); + remote_login_service.login_changed.connect (remote_login_changed); + query_directory_servers.begin (); + } + catch (IOError e) + { + debug ("Getting the com.canonical.RemoteLogin dbus service failed. Error: %s", e.message); + remove_remote_servers (); + remote_login_service = null; + } + } + ); + } + + private void on_remote_login_service_vanished (DBusConnection conn, string name) + { + remove_remote_servers (); + remote_login_service = null; + + /* provide a fallback manual login option */ + if (UnityGreeter.singleton.hide_users_hint ()) { + add_manual_entry(); + set_active_entry ("*other"); + } + } + + private async void remote_directory_respond_cb () + { + remove_remote_login_servers (); + currently_browsing_server_url = ""; + currently_browsing_server_email = ""; + + var password_field = current_remote_fields.get ("password") as DashEntry; + var email_field = current_remote_fields.get ("email") as Gtk.Entry; + if (password_field == null) + { + debug ("Something wrong happened in remote_directory_respond_cb. There was no password field"); + return; + } + if (email_field == null) + { + debug ("Something wrong happened in remote_directory_respond_cb. There was no email field"); + return; + } + + RemoteServer[] server_list = {}; + var email = email_field.text; + var email_valid = false; + try + { + /* Check email address is valid + * Using the html5 definition of a valid e-mail address + * http://www.w3.org/TR/html5/states-of-the-type-attribute.html#valid-e-mail-address */ + var re = new Regex ("[a-zA-Z0-9.!#$%&'\\*\\+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*"); + MatchInfo info; + email_valid = re.match_all (email, 0, out info); + email_valid = email_valid && info.get_match_count () > 0 && info.fetch (0) == email; + } + catch (RegexError e) + { + debug ("Calling email regex match failed. Error: %s", e.message); + } + + selected_entry.reset_messages (); + if (!email_valid) + { + will_clear = true; + show_message (_("Please enter a complete e-mail address"), true); + create_remote_fields_for_current_item.begin (remote_directory_server_list); + } + else + { + var login_success = false; + try + { + var url = url_from_remote_loding_server_list_name (selected_entry.id); + if (UnityGreeter.singleton.test_mode) + { + if (password_field.text == "password") + { + test_fill_remote_login_servers (out server_list); + login_success = true; + } + else if (password_field.text == "delay1") + { + test_fill_remote_login_servers (out server_list); + login_success = true; + Timeout.add (5000, () => { test_call_set_remote_directory_servers (); return false; }); + } + else if (password_field.text == "delay2") + { + test_fill_remote_login_servers (out server_list); + login_success = true; + Timeout.add (5000, () => { test_call_remote_login_servers_updated (); return false; }); + } + else if (password_field.text == "delay3") + { + test_fill_remote_login_servers (out server_list); + login_success = true; + Timeout.add (5000, () => { remote_login_changed (currently_browsing_server_url, currently_browsing_server_email); return false; }); + } + else if (password_field.text == "duplicate") + { + test_fill_remote_login_servers_duplicate_entries (out server_list); + login_success = true; + } + } + else + { + string data_type; + bool allowcache = true; + // If we had an error and are retrying the same user and server, do not use the cache on R-L-S + if (selected_entry.has_errors && currently_browsing_server_email == email && currently_browsing_server_url == url) + allowcache = false; + yield remote_login_service.get_servers_for_login (url, email, password_field.text, allowcache, out login_success, out data_type, out server_list); + } + currently_browsing_server_url = url; + currently_browsing_server_email = email; + } + catch (IOError e) + { + debug ("Calling get_servers in com.canonical.RemoteLogin dbus service failed. Error: %s", e.message); + } + + if (login_success) + { + password_field.did_respond = false; + if (server_list.length == 0) + show_remote_account_dialog (); + else + { + var last_used_server_list_name = ""; + foreach (var remote_server in server_list) + { + var e = create_prompt_for_login_server (remote_server); + if (remote_server.last_used_server) + last_used_server_list_name = e.id; + } + if (last_used_server_list_name != "") + set_active_entry (last_used_server_list_name); + else + set_active_first_entry_with_prefix ("*remote_login"); + } + } + else + { + will_clear = true; + show_message (_("Incorrect e-mail address or password"), true); + create_remote_fields_for_current_item.begin (remote_directory_server_list); + } + } + } + + private void remote_login_respond_cb () + { + sensitive = false; + will_clear = true; + greeter_authenticating_user = selected_entry.id; + if (UnityGreeter.singleton.test_mode) + { + Gtk.Entry field = current_remote_fields.get ("password") as Gtk.Entry; + test_is_authenticated = field.text == "password"; + if (field.text == "delay") + Timeout.add (5000, () => { authentication_complete_cb (); return false; }); + else + authentication_complete_cb (); + } + else + { + UnityGreeter.singleton.authenticate_remote (get_lightdm_session (), null); + remote_login_service.set_last_used_server.begin (currently_browsing_server_url, url_from_remote_loding_server_list_name (selected_entry.id)); + } + } + + private void show_remote_account_dialog () + { + var dialog = new Gtk.MessageDialog (null, 0, Gtk.MessageType.OTHER, Gtk.ButtonsType.NONE, ""); + dialog.set_position (Gtk.WindowPosition.CENTER_ALWAYS); + dialog.secondary_text = _("If you have an account on an RDP or Citrix server, Remote Login lets you run applications from that server."); + // For 12.10 we still don't support Citrix + dialog.secondary_text = _("If you have an account on an RDP server, Remote Login lets you run applications from that server."); + if (offer_guest) + { + dialog.add_button (_("Cancel"), 0); + var b = dialog.add_button (_("Set Up…"), 1); + b.grab_focus (); + dialog.text = _("You need an Ubuntu Remote Login account to use this service. Would you like to set up an account now?"); + } + else + { + dialog.add_button (_("OK"), 0); + dialog.text = _("You need an Ubuntu Remote Login account to use this service. Visit uccs.canonical.com to set up an account."); + } + + dialog.show_all (); + dialog.response.connect ((id) => + { + if (id == 1) + { + var config_session = "uccsconfigure"; + if (is_supported_remote_session (config_session)) + { + greeter_authenticating_user = selected_entry.id; + UnityGreeter.singleton.authenticate_remote (config_session, null); + } + } + dialog.destroy (); + }); + dialog.run (); + } + + private bool change_background_timeout_cb () + { + string? new_background_file = null; + if (menubar.high_contrast || !UGSettings.get_boolean (UGSettings.KEY_DRAW_USER_BACKGROUNDS)) + new_background_file = null; + else if (selected_entry is UserPromptBox) + new_background_file = (selected_entry as UserPromptBox).background; + + background.current_background = new_background_file; + + change_background_timeout = 0; + return false; + } + + private void change_background () + { + if (background.current_background != null) + { + if (change_background_timeout == 0) + change_background_timeout = Idle.add (change_background_timeout_cb); + } + else + change_background_timeout_cb (); + } + + protected static int user_list_compare_entry (PromptBox a, PromptBox b) + { + if (a.id.has_prefix ("*remote_directory") && !b.id.has_prefix ("*remote_directory")) + return 1; + if (a.id.has_prefix ("*remote_login") && !b.id.has_prefix ("*remote_login")) + return 1; + + /* Fall back to default behaviour of the GreeterList sorter */ + return GreeterList.compare_entry (a, b); + } + + protected override void insert_entry (PromptBox entry) + { + entries.insert_sorted (entry, user_list_compare_entry); + } + + protected override void setup_prompt_box (bool fade = true) + { + base.setup_prompt_box (fade); + var userbox = selected_entry as UserPromptBox; + if (userbox != null) + selected_entry.set_is_active (userbox.is_active); + } + + private void entry_selected_cb (string? username) + { + UnityGreeter.singleton.set_state ("last-user", username); + if (selected_entry is UserPromptBox) + session = (selected_entry as UserPromptBox).session; + else + session = null; + selected_entry.clear (); + + /* Reset this variable so it can be freed */ + remote_server_email_field_autocompleter = null; + + start_authentication (); + } + + protected override void start_authentication () + { + sensitive = true; + greeter_authenticating_user = ""; + if (selected_entry.id.has_prefix ("*remote_directory")) + { + prompted = true; + create_remote_fields_for_current_item.begin (remote_directory_server_list); + } + else if (selected_entry.id.has_prefix ("*remote_login")) + { + prompted = true; + create_remote_fields_for_current_item.begin (remote_login_server_list); + } + else + base.start_authentication (); + } + + private async void create_remote_fields_for_current_item (List<RemoteServer?> server_list) + { + current_remote_fields = new HashTable<string, Gtk.Widget> (str_hash, str_equal); + var url = url_from_remote_loding_server_list_name (selected_entry.id); + var username = username_from_remote_loding_server_list_name (selected_entry.id); + + foreach (var remote_server in server_list) + { + var remote_username = username_from_remote_server_fields (remote_server); + if (remote_server.url == url && (username == null || username == remote_username)) + { + if (selected_entry.id.has_prefix ("*remote_login")) + { + if (!is_supported_remote_session (remote_server.type)) + { + show_message (_("Server type not supported."), true); + } + } + + var fields = new List<RemoteServerField?> (); + foreach (var field in remote_server.fields) + fields.append (field); + fields.sort (remote_server_field_sort_function); + foreach (var field in fields) + { + Gtk.Widget? widget = null; + var default_value = ""; + if (field.default_value != null && field.default_value.is_of_type (VariantType.STRING)) + default_value = field.default_value.get_string (); + if (field.type == "username") + { + var entry = add_prompt (_("Username:")); + entry.text = default_value; + widget = entry; + } + else if (field.type == "password") + { + var entry = add_prompt (_("Password:"), true); + entry.text = default_value; + widget = entry; + } + else if (field.type == "domain") + { + string[] domainsArray = {}; + if (field.properties != null && field.properties.contains ("domains") && field.properties.get ("domains").is_of_type (VariantType.ARRAY)) + domainsArray = field.properties.get ("domains").dup_strv (); + var domains = new GenericArray<string> (); + for (var i = 0; i < domainsArray.length; i++) + domains.add (domainsArray[i]); + + var read_only = field.properties != null && + field.properties.contains ("read-only") && + field.properties.get ("read-only").is_of_type (VariantType.BOOLEAN) && + field.properties.get ("read-only").get_boolean (); + if (domains.length == 0 || (domains.length == 1 && (domains[0] == default_value || default_value.length == 0))) + { + var prompt = add_prompt (_("Domain:")); + prompt.text = domains.length == 1 ? domains[0] : default_value; + prompt.sensitive = !read_only; + widget = prompt; + } + else + { + if (default_value.length > 0) + { + /* Make sure the domain list contains the default value */ + var found = false; + for (var i = 0; !found && i < domains.length; i++) + found = default_value == domains[i]; + + if (!found) + domains.add (default_value); + } + + /* Sort domains alphabetically */ + domains.sort (strcmp); + var combo = add_combo (domains, read_only); + + if (default_value.length > 0) + { + if (read_only) + { + for (var i = 0; i < domains.length; i++) + { + if (default_value == domains[i]) + { + combo.active = i; + break; + } + } + } + else + { + var entry = combo.get_child () as Gtk.Entry; + entry.text = default_value; + } + } + + widget = combo; + } + } + else if (field.type == "email") + { + string[] email_domains; + try + { + if (UnityGreeter.singleton.test_mode) + email_domains = { "canonical.com", "ubuntu.org", "candy.com", "urban.net" }; + else + yield remote_login_service.get_cached_domains_for_server (url, out email_domains); + } + catch (IOError e) + { + email_domains.resize (0); + debug ("Calling get_cached_domains_for_server in com.canonical.RemoteLogin dbus service failed. Error: %s", e.message); + } + + var entry = add_prompt (_("Email address:")); + entry.text = default_value; + widget = entry; + if (email_domains.length > 0) + remote_server_email_field_autocompleter = new EmailAutocompleter (entry, email_domains); + } + else + { + debug ("Found field of type %s, don't know what to do with it", field.type); + continue; + } + current_remote_fields.insert (field.type, widget); + } + break; + } + } + } + + public override void focus_prompt () + { + if (selected_entry.id.has_prefix ("*remote_login")) + { + var url = url_from_remote_loding_server_list_name(selected_entry.id); + foreach (var remote_server in remote_login_server_list) + { + if (remote_server.url == url) + { + if (!is_supported_remote_session (remote_server.type)) + { + selected_entry.sensitive = false; + return; + } + } + } + } + + base.focus_prompt (); + } + + public override void show_authenticated (bool successful = true) + { + if (successful) + { + /* 'Log In' here is the button for logging in. */ + selected_entry.add_button (_("Log In"), + _("Login as %s").printf (selected_entry.label)); + } + else + { + selected_entry.add_button (_("Retry"), + _("Retry as %s").printf (selected_entry.label)); + } + + if (mode != Mode.SCROLLING) + selected_entry.show_prompts (); + + focus_prompt (); + redraw_greeter_box (); + } + + public void add_user (string name, string label, string? background = null, bool is_active = false, bool has_messages = false, string? session = null) + { + var e = find_entry (name) as UserPromptBox; + if (e == null) + { + e = new UserPromptBox (name); + e.respond.connect (prompt_box_respond_cb); + e.login.connect (prompt_box_login_cb); + e.show_options.connect (prompt_box_show_options_cb); + e.label = label; /* Do this before adding for sorting purposes */ + add_entry (e); + } + e.background = background; + e.is_active = is_active; + e.session = session; + e.label = label; + e.set_show_message_icon (has_messages); + e.set_is_active (is_active); + + /* Remove manual option when have users */ + if (have_entries () && !always_show_manual) + remove_entry ("*other"); + } + + protected override void add_manual_entry () + { + var text = manual_name; + if (text == null) + text = _("Login"); + add_user ("*other", text); + } + + protected void prompt_box_respond_cb (string[] responses) + { + selected_entry.sensitive = false; + will_clear = true; + unacknowledged_messages = false; + + foreach (var response in responses) + { + if (UnityGreeter.singleton.test_mode) + test_respond (response); + else + UnityGreeter.singleton.respond (response); + } + } + + private void prompt_box_login_cb () + { + debug ("Start session for %s", selected_entry.id); + + unacknowledged_messages = false; + var is_authenticated = false; + if (UnityGreeter.singleton.test_mode) + is_authenticated = test_is_authenticated; + else + is_authenticated = UnityGreeter.singleton.is_authenticated(); + + /* Finish authentication (again) or restart it */ + if (is_authenticated) + authentication_complete_cb (); + else + { + selected_entry.clear (); + start_authentication (); + } + } + + private void prompt_box_show_options_cb () + { + var session_chooser = new SessionList (background, menubar, session, default_session); + session_chooser.session_clicked.connect (session_clicked_cb); + UnityGreeter.singleton.push_list (session_chooser); + } + + private void session_clicked_cb (string session) + { + this.session = session; + UnityGreeter.singleton.pop_list (); + } + + private bool should_show_session_badge () + { + if (UnityGreeter.singleton.test_mode) + return get_selected_id () != "no-badge"; + else + return LightDM.get_sessions ().length () > 1; + } + + private Gdk.Pixbuf? get_badge () + { + if (selected_entry is UserPromptBox) + { + if (!should_show_session_badge ()) + return null; + else if (session == null) + return SessionList.get_badge (default_session); + else + return SessionList.get_badge (session); + } + else + { + if (selected_entry.id.has_prefix ("*remote_directory")) + return SessionList.get_badge ("remote-login"); + else + return null; + } + } + + private bool is_supported_remote_session (string session_internal_name) + { + if (UnityGreeter.singleton.test_mode) + return session_internal_name == "rdp"; + + var found = false; + foreach (var session in LightDM.get_remote_sessions ()) + { + if (session.key == session_internal_name) + { + found = true; + break; + } + } + return found; + } + + protected override string get_lightdm_session () + { + if (selected_entry.id.has_prefix ("*remote_login")) + { + var url = url_from_remote_loding_server_list_name (selected_entry.id); + unowned List<RemoteServer?> it = remote_login_server_list; + + var answer = ""; + while (answer == "" && it != null) + { + RemoteServer remote_server = it.data; + if (remote_server.url == url) + answer = remote_server.type; + it = it.next; + } + + if (is_supported_remote_session (answer)) + return answer; + else + return ""; + } + else + return session; + } + + private void fill_list () + { + if (UnityGreeter.singleton.test_mode) + test_fill_list (); + else + { + default_session = UnityGreeter.singleton.default_session_hint (); + always_show_manual = UnityGreeter.singleton.show_manual_login_hint (); + if (!UnityGreeter.singleton.hide_users_hint ()) + { + var users = LightDM.UserList.get_instance (); + users.user_added.connect (user_added_cb); + users.user_changed.connect (user_added_cb); + users.user_removed.connect (user_removed_cb); + foreach (var user in users.users) + user_added_cb (user); + } + + if (UnityGreeter.singleton.has_guest_account_hint ()) + { + debug ("Adding guest account entry"); + offer_guest = true; + } + + /* If we have no entries at all, we should show manual */ + if (!have_entries ()) + add_manual_entry (); + + var last_user = UnityGreeter.singleton.get_state ("last-user"); + if (UnityGreeter.singleton.select_user_hint () != null) + set_active_entry (UnityGreeter.singleton.select_user_hint ()); + else if (last_user != null) + set_active_entry (last_user); + } + } + + private void user_added_cb (LightDM.User user) + { + debug ("Adding/updating user %s (%s)", user.name, user.real_name); + + if (!show_hidden_users) + { + var hidden_users = UGSettings.get_strv (UGSettings.KEY_HIDDEN_USERS); + foreach (var username in hidden_users) + if (username == user.name) + return; + } + + var label = user.real_name; + if (user.real_name == "") + label = user.name; + + add_user (user.name, label, user.background, user.logged_in, user.has_messages, user.session); + } + + private void user_removed_cb (LightDM.User user) + { + debug ("Removing user %s", user.name); + remove_entry (user.name); + } + + protected override void show_prompt_cb (string text, LightDM.PromptType type) + { + if (selected_entry.id.has_prefix ("*remote_login")) + { + if (text == "remote login:") + { + Gtk.Entry field = current_remote_fields.get ("username") as Gtk.Entry; + var answer = field != null ? field.text : ""; + UnityGreeter.singleton.respond (answer); + } + else if (text == "password:") + { + Gtk.Entry field = current_remote_fields.get ("password") as Gtk.Entry; + var answer = field != null ? field.text : ""; + UnityGreeter.singleton.respond (answer); + } + else if (text == "remote host:") + { + var answer = url_from_remote_loding_server_list_name (selected_entry.id); + UnityGreeter.singleton.respond (answer); + } + else if (text == "domain:") + { + Gtk.Entry field = current_remote_fields.get ("domain") as Gtk.Entry; + var answer = field != null ? field.text : ""; + UnityGreeter.singleton.respond (answer); + } + } + else + base.show_prompt_cb (text, type); + } + + /* A lot of test code below here */ + + private struct TestEntry + { + string username; + string real_name; + string? background; + bool is_active; + bool has_messages; + string? session; + } + + private const TestEntry[] test_entries = + { + { "has-password", "Has Password", "*" }, + { "different-prompt", "Different Prompt", "*" }, + { "no-password", "No Password", "*" }, + { "change-password", "Change Password", "*" }, + { "auth-error", "Auth Error", "*" }, + { "two-factor", "Two Factor", "*" }, + { "two-prompts", "Two Prompts", "*" }, + { "info-prompt", "Info Prompt", "*" }, + { "long-info-prompt", "Long Info Prompt", "*" }, + { "wide-info-prompt", "Wide Info Prompt", "*" }, + { "multi-info-prompt", "Multi Info Prompt", "*" }, + { "very-very-long-name", "Long name (far far too long to fit)", "*" }, + { "long-name-and-messages", "Long name and messages (too long to fit)", "*", false, true }, + { "active", "Active Account", "*", true }, + { "has-messages", "Has Messages", "*", false, true }, + { "gnome", "GNOME", "*", false, false, "gnome" }, + { "locked", "Locked Account", "*" }, + { "color-background", "Color Background", "#dd4814" }, + { "white-background", "White Background", "#ffffff" }, + { "black-background", "Black Background", "#000000" }, + { "no-background", "No Background", null }, + { "unicode", "가나다라마", "*" }, + { "no-response", "No Response", "*" }, + { "no-badge", "No Badge", "*" }, + { "messages-after-login", "Messages After Login", "*" }, + { "" } + }; + private List<string> test_backgrounds; + private int n_test_entries = 0; + private bool test_prompted_sso = false; + private string test_two_prompts_first = null; + private bool test_request_new_password = false; + private string? test_new_password = null; + + private void test_fill_list () + { + test_backgrounds = new List<string> (); + try + { + var dir = Dir.open ("/usr/share/backgrounds/"); + while (true) + { + var bg = dir.read_name (); + if (bg == null) + break; + test_backgrounds.append ("/usr/share/backgrounds/" + bg); + } + } + catch (FileError e) + { + } + + if (!UnityGreeter.singleton.hide_users_hint()) + while (add_test_entry ()); + + /* add a manual entry if the list of entries is empty initially */ + if (n_test_entries <= 0) + { + add_manual_entry(); + set_active_entry ("*other"); + n_test_entries++; + } + + offer_guest = UnityGreeter.singleton.has_guest_account_hint(); + always_show_manual = UnityGreeter.singleton.show_manual_login_hint(); + + key_press_event.connect (test_key_press_cb); + + if (UnityGreeter.singleton.show_remote_login_hint()) + Timeout.add (1000, () => + { + RemoteServer[] test_server_list = {}; + RemoteServer remote_server = RemoteServer (); + remote_server.type = "uccs"; + remote_server.name = "Remote Login"; + remote_server.url = "http://crazyurl.com"; + remote_server.last_used_server = false; + remote_server.fields = {}; + RemoteServerField field1 = RemoteServerField (); + field1.type = "email"; + RemoteServerField field2 = RemoteServerField (); + field2.type = "password"; + remote_server.fields = {field1, field2}; + + test_server_list += remote_server; + set_remote_directory_servers (test_server_list); + + return false; + }); + + var last_user = UnityGreeter.singleton.get_state ("last-user"); + if (last_user != null) + set_active_entry (last_user); + + } + + private void test_call_set_remote_directory_servers () + { + RemoteServer[] test_server_list = {}; + RemoteServer remote_server = RemoteServer (); + remote_server.type = "uccs"; + remote_server.name = "Corporate Remote Login"; + remote_server.url = "http://internalcompayserver.com"; + remote_server.last_used_server = false; + remote_server.fields = {}; + RemoteServerField field1 = RemoteServerField (); + field1.type = "email"; + RemoteServerField field2 = RemoteServerField (); + field2.type = "password"; + remote_server.fields = {field1, field2}; + + test_server_list += remote_server; + set_remote_directory_servers (test_server_list); + } + + private void test_call_remote_login_servers_updated () + { + RemoteServer[] server_list = {}; + RemoteServer remote_server1 = RemoteServer (); + remote_server1.type = "rdp"; + remote_server1.name = "Cool RDP server"; + remote_server1.url = "http://coolrdpserver.com"; + remote_server1.last_used_server = false; + remote_server1.fields = {}; + RemoteServerField field1 = RemoteServerField (); + field1.type = "username"; + RemoteServerField field2 = RemoteServerField (); + field2.type = "password"; + RemoteServerField field3 = RemoteServerField (); + field3.type = "domain"; + remote_server1.fields = {field1, field2, field3}; + + RemoteServer remote_server2 = RemoteServer (); + remote_server2.type = "rdp"; + remote_server2.name = "MegaCool RDP server"; + remote_server2.url = "http://megacoolrdpserver.com"; + remote_server2.last_used_server = false; + remote_server2.fields = {}; + RemoteServerField field21 = RemoteServerField (); + field21.type = "username"; + RemoteServerField field22 = RemoteServerField (); + field22.type = "password"; + remote_server2.fields = {field21, field22}; + + server_list.resize (2); + server_list[0] = remote_server1; + server_list[1] = remote_server2; + + remote_login_servers_updated (currently_browsing_server_url, currently_browsing_server_email, "", server_list); + } + + private void test_fill_remote_login_servers (out RemoteServer[] server_list) + { + string[] domains = { "SCANNERS", "PRINTERS", "ROUTERS" }; + + server_list = {}; + RemoteServer remote_server1 = RemoteServer (); + remote_server1.type = "rdp"; + remote_server1.name = "Cool RDP server"; + remote_server1.url = "http://coolrdpserver.com"; + remote_server1.last_used_server = false; + remote_server1.fields = {}; + RemoteServerField field1 = RemoteServerField (); + field1.type = "username"; + RemoteServerField field2 = RemoteServerField (); + field2.type = "password"; + RemoteServerField field3 = RemoteServerField (); + field3.type = "domain"; + remote_server1.fields = {field1, field2, field3}; + + RemoteServer remote_server2 = RemoteServer (); + remote_server2.type = "rdp"; + remote_server2.name = "RDP server with default username, and editable domain"; + remote_server2.url = "http://rdpdefaultusername.com"; + remote_server2.last_used_server = false; + remote_server2.fields = {}; + RemoteServerField field21 = RemoteServerField (); + field21.type = "username"; + field21.default_value = new Variant.string ("alowl"); + RemoteServerField field22 = RemoteServerField (); + field22.type = "password"; + RemoteServerField field23 = RemoteServerField (); + field23.type = "domain"; + field23.default_value = new Variant.string ("PRINTERS"); + field23.properties = new HashTable<string, Variant> (str_hash, str_equal); + field23.properties["domains"] = domains; + remote_server2.fields = {field21, field22, field23}; + + RemoteServer remote_server3 = RemoteServer (); + remote_server3.type = "rdp"; + remote_server3.name = "RDP server with default username, and non editable domain"; + remote_server3.url = "http://rdpdefaultusername2.com"; + remote_server3.last_used_server = true; + remote_server3.fields = {}; + RemoteServerField field31 = RemoteServerField (); + field31.type = "username"; + field31.default_value = new Variant.string ("lwola"); + RemoteServerField field32 = RemoteServerField (); + field32.type = "password"; + RemoteServerField field33 = RemoteServerField (); + field33.type = "domain"; + field33.default_value = new Variant.string ("PRINTERS"); + field33.properties = new HashTable<string, Variant> (str_hash, str_equal); + field33.properties["domains"] = domains; + field33.properties["read-only"] = true; + + remote_server3.fields = {field31, field32, field33}; + + RemoteServer remote_server4 = RemoteServer (); + remote_server4.type = "notsupported"; + remote_server4.name = "Not supported server"; + remote_server4.url = "http://notsupportedserver.com"; + remote_server4.fields = {}; + RemoteServerField field41 = RemoteServerField (); + field41.type = "username"; + RemoteServerField field42 = RemoteServerField (); + field42.type = "password"; + RemoteServerField field43 = RemoteServerField (); + field43.type = "domain"; + + remote_server4.fields = {field41, field42, field43}; + + server_list.resize (4); + server_list[0] = remote_server1; + server_list[1] = remote_server2; + server_list[2] = remote_server3; + server_list[3] = remote_server4; + } + + private void test_fill_remote_login_servers_duplicate_entries (out RemoteServer[] server_list) + { + /* Create two remote servers with same url but different username and domain. */ + server_list = {}; + + RemoteServer remote_server2 = RemoteServer (); + remote_server2.type = "rdp"; + remote_server2.name = "RDP server with default username, and editable domain"; + remote_server2.url = "http://rdpdefaultusername.com"; + remote_server2.last_used_server = false; + remote_server2.fields = {}; + RemoteServerField field21 = RemoteServerField (); + field21.type = "username"; + field21.default_value = new Variant.string ("alowl1"); + RemoteServerField field22 = RemoteServerField (); + field22.type = "password"; + field22.default_value = new Variant.string ("duplicate1"); + RemoteServerField field23 = RemoteServerField (); + field23.type = "domain"; + field23.default_value = new Variant.string ("SCANNERS"); + remote_server2.fields = {field21, field22, field23}; + + RemoteServer remote_server5 = RemoteServer (); + remote_server5.type = "rdp"; + remote_server5.name = "RDP server with default username, and editable domain"; + remote_server5.url = "http://rdpdefaultusername.com"; + remote_server5.last_used_server = false; + remote_server5.fields = {}; + RemoteServerField field51 = RemoteServerField (); + field51.type = "username"; + field51.default_value = new Variant.string ("alowl2"); + RemoteServerField field52 = RemoteServerField (); + field52.type = "password"; + field52.default_value = new Variant.string ("duplicate2"); + RemoteServerField field53 = RemoteServerField (); + field53.type = "domain"; + field53.default_value = new Variant.string ("PRINTERS"); + remote_server5.fields = {field51, field52, field53}; + + server_list.resize (2); + server_list[0] = remote_server2; + server_list[1] = remote_server5; + } + + private bool test_key_press_cb (Gdk.EventKey event) + { + if ((event.state & Gdk.ModifierType.CONTROL_MASK) == 0) + return false; + + switch (event.keyval) + { + case Gdk.Key.plus: + add_test_entry (); + break; + case Gdk.Key.minus: + remove_test_entry (); + break; + case Gdk.Key.@0: + while (remove_test_entry ()); + offer_guest = false; + break; + case Gdk.Key.equal: + while (add_test_entry ()); + offer_guest = true; + break; + case Gdk.Key.g: + offer_guest = false; + break; + case Gdk.Key.G: + offer_guest = true; + break; + case Gdk.Key.m: + always_show_manual = false; + break; + case Gdk.Key.M: + always_show_manual = true; + break; + } + + return false; + } + + private bool add_test_entry () + { + var e = test_entries[n_test_entries]; + if (e.username == "") + return false; + + var background = e.background; + if (background == "*") + { + var background_index = 0; + for (var i = 0; i < n_test_entries; i++) + { + if (test_entries[i].background == "*") + background_index++; + } + if (test_backgrounds.length () > 0) + background = test_backgrounds.nth_data (background_index % test_backgrounds.length ()); + } + add_user (e.username, e.real_name, background, e.is_active, e.has_messages, e.session); + n_test_entries++; + + return true; + } + + private bool remove_test_entry () + { + if (n_test_entries == 0) + return false; + + remove_entry (test_entries[n_test_entries - 1].username); + n_test_entries--; + + return true; + } + + private void test_respond (string text) + { + debug ("response %s", text); + switch (get_selected_id ()) + { + case "*other": + if (test_username == null) + { + debug ("username=%s", text); + test_username = text; + show_prompt_cb ("Password:", LightDM.PromptType.SECRET); + } + else + { + test_is_authenticated = text == "password"; + authentication_complete_cb (); + } + break; + case "two-factor": + if (!test_prompted_sso) + { + if (text == "password") + { + debug ("prompt otp"); + test_prompted_sso = true; + show_prompt_cb ("OTP:", LightDM.PromptType.QUESTION); + } + else + { + test_is_authenticated = false; + authentication_complete_cb (); + } + } + else + { + test_is_authenticated = text == "otp"; + authentication_complete_cb (); + } + break; + case "two-prompts": + if (test_two_prompts_first == null) + test_two_prompts_first = text; + else + { + test_is_authenticated = test_two_prompts_first == "blue" && text == "password"; + authentication_complete_cb (); + } + break; + case "change-password": + if (test_new_password != null) + { + test_is_authenticated = text == test_new_password; + authentication_complete_cb (); + } + else if (test_request_new_password) + { + test_new_password = text; + show_prompt_cb ("Retype new UNIX password: ", LightDM.PromptType.SECRET); + } + else + { + if (text != "password") + { + test_is_authenticated = false; + authentication_complete_cb (); + } + else + { + test_request_new_password = true; + show_message_cb ("You are required to change your password immediately (root enforced)", LightDM.MessageType.ERROR); + show_prompt_cb ("Enter new UNIX password: ", LightDM.PromptType.SECRET); + } + } + break; + case "no-response": + break; + case "locked": + test_is_authenticated = false; + show_message_cb ("Account is locked", LightDM.MessageType.ERROR); + authentication_complete_cb (); + break; + case "messages-after-login": + test_is_authenticated = text == "password"; + if (test_is_authenticated) + show_message_cb ("Congratulations on logging in!", LightDM.MessageType.INFO); + authentication_complete_cb (); + break; + default: + test_is_authenticated = text == "password"; + authentication_complete_cb (); + break; + } + } + + protected override void test_start_authentication () + { + test_username = null; + test_is_authenticated = false; + test_prompted_sso = false; + test_two_prompts_first = null; + test_request_new_password = false; + test_new_password = null; + + switch (get_selected_id ()) + { + case "*other": + if (authenticate_user != null) + { + test_username = authenticate_user; + authenticate_user = null; + show_prompt_cb ("Password:", LightDM.PromptType.SECRET); + } + else + show_prompt_cb ("Username:", LightDM.PromptType.QUESTION); + break; + case "*guest": + test_is_authenticated = true; + authentication_complete_cb (); + break; + case "different-prompt": + show_prompt_cb ("Secret word", LightDM.PromptType.SECRET); + break; + case "no-password": + test_is_authenticated = true; + authentication_complete_cb (); + break; + case "auth-error": + show_message_cb ("Authentication Error", LightDM.MessageType.ERROR); + test_is_authenticated = false; + authentication_complete_cb (); + break; + case "info-prompt": + show_message_cb ("Welcome to Unity Greeter", LightDM.MessageType.INFO); + show_prompt_cb ("Password:", LightDM.PromptType.SECRET); + break; + case "long-info-prompt": + show_message_cb ("Welcome to Unity Greeter\n\nWe like to annoy you with long messages.\nLike this one\n\nThis is the last line of a multiple line message.", LightDM.MessageType.INFO); + show_prompt_cb ("Password:", LightDM.PromptType.SECRET); + break; + case "wide-info-prompt": + show_message_cb ("Welcome to Unity Greeter, the greeteriest greeter that ever did appear in these fine lands", LightDM.MessageType.INFO); + show_prompt_cb ("Password:", LightDM.PromptType.SECRET); + break; + case "multi-info-prompt": + show_message_cb ("Welcome to Unity Greeter", LightDM.MessageType.INFO); + show_message_cb ("This is an error", LightDM.MessageType.ERROR); + show_message_cb ("You should have seen three messages", LightDM.MessageType.INFO); + show_prompt_cb ("Password:", LightDM.PromptType.SECRET); + break; + case "two-prompts": + show_prompt_cb ("Favorite Color (blue):", LightDM.PromptType.QUESTION); + show_prompt_cb ("Password:", LightDM.PromptType.SECRET); + break; + default: + show_prompt_cb ("Password:", LightDM.PromptType.SECRET); + break; + } + } +} diff --git a/src/user-prompt-box.vala b/src/user-prompt-box.vala new file mode 100644 index 0000000..9b285da --- /dev/null +++ b/src/user-prompt-box.vala @@ -0,0 +1,36 @@ +/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- + * + * Copyright (C) 2011,2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Robert Ancell <robert.ancell@canonical.com> + * Michael Terry <michael.terry@canonical.com> + */ + +public class UserPromptBox : PromptBox +{ + /* Background for this user */ + public string background; + + /* Default session for this user */ + public string session; + + /* True if should be marked as active */ + public bool is_active; + + public UserPromptBox (string name) + { + Object (id: name); + } +} |