diff options
Diffstat (limited to 'src/greeter-list.vala')
-rw-r--r-- | src/greeter-list.vala | 949 |
1 files changed, 949 insertions, 0 deletions
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 () + { + } +} |