/* -*- 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 .
*
* Authors: Robert Ancell
* Michael Terry
*/
private int get_grid_offset (int size)
{
return (int) (size % grid_size) / 2;
}
[DBus (name="com.canonical.UnityGreeter.List")]
public class ListDBusInterface : Object
{
private UserList list;
public ListDBusInterface (UserList list)
{
this.list = list;
}
public void set_active_entry (string entry_name)
{
list.set_active_entry (entry_name);
}
}
public class UserEntry
{
/* Unique name for this entry */
public string name;
/* Label to display */
public Pango.Layout layout;
/* Background for this user */
public string background;
/* True if should be marked as active */
public bool is_active;
/* True if have messages */
public bool has_messages;
/* Keyboard layouts to use for this user by default */
public List keyboard_layouts;
/* Default session for this user */
public string session;
/* Cached cairo surfaces */
public Cairo.Surface label_in_box_surface;
public Cairo.Surface label_out_of_box_surface;
}
private class AuthenticationMessage
{
public Pango.Layout layout;
public bool is_error;
public AuthenticationMessage (Pango.Layout layout, bool is_error)
{
this.layout = layout;
this.is_error = is_error;
}
}
public class UserList : Gtk.EventBox
{
public Background background;
public MenuBar menubar;
public UserEntry? selected_entry {get; private set; default = null;}
private bool _offer_guest = false;
public bool offer_guest
{
get { return _offer_guest; }
set
{
_offer_guest = value;
if (value)
add_entry ("*guest", _("Guest Session"));
else
remove_entry ("*guest");
}
}
private 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_users ())
remove_entry ("*other");
}
}
private List entries = null;
private ListDBusInterface dbus_object;
private Gdk.Pixbuf message_pixbuf;
private double scroll_target_location;
private double scroll_start_location;
private double scroll_location;
private double scroll_direction;
private AnimateTimer scroll_timer;
/* Authentication messages to show */
private List messages = null;
private Gtk.Fixed fixed;
private Gtk.Box login_box;
private DashEntry prompt_entry;
private DashButton login_button;
private Fadable prompt_widget_to_show;
private Gtk.Button session_button;
private CachedImage session_image;
private SessionChooser session_chooser;
private enum Mode
{
LOGIN,
TRANSFORM_TO_LOGIN_HIDE,
TRANSFORM_TO_LOGIN_SHOW,
SCROLLING,
SESSIONS,
TRANSFORM_TO_SESSIONS_HIDE,
}
private Mode mode = Mode.LOGIN;
private bool complete = false;
private int border = 4;
private int box_width = 7;
private uint n_above = 4;
private uint n_below = 4;
/* Box in the middle taking up three rows */
private int box_height = 3;
private int box_x
{
get { return get_grid_offset (get_allocated_width ()) + grid_size; }
}
private int box_y
{
get
{
var row = (int) (get_allocated_height () / grid_size - box_height) / 2;
return get_grid_offset (get_allocated_height ()) + row * grid_size;
}
}
public signal void user_selected (string? username);
public signal void user_displayed_start ();
public signal void user_displayed_done ();
public signal void respond_to_prompt (string text);
public signal void start_session ();
public string? selected
{
get { if (selected_entry == null) return null; return selected_entry.name; }
}
private string? _manual_username = null;
public string? manual_username
{
get { return _manual_username; }
set
{
_manual_username = value;
if (find_entry ("*other") != null)
add_manual_entry ();
}
}
private string _default_session = "ubuntu";
public string default_session
{
get
{
return _default_session;
}
set
{
_default_session = value;
session_image.set_from_pixbuf (get_badge ());
}
}
private string? _session = null;
public string? session
{
get
{
return _session;
}
set
{
_session = value;
session_image.set_from_pixbuf (get_badge ());
}
}
private Gdk.Pixbuf? last_session_badge = null;
public UserList (Background bg, MenuBar mb)
{
background = bg;
menubar = mb;
can_focus = false;
visible_window = false;
fixed = new Gtk.Fixed ();
fixed.show ();
add (fixed);
login_box = new DashBox (background);
login_box.show ();
add_with_class (login_box);
session_chooser = new SessionChooser ();
session_chooser.session_clicked.connect (session_clicked_cb);
session_chooser.fade_done.connect (session_fade_done_cb);
session_chooser.show ();
UnityGreeter.add_style_class (session_chooser);
prompt_entry = new DashEntry ();
prompt_entry.caps_lock_warning = true;
prompt_entry.activate.connect (prompt_entry_activate_cb);
add_with_class (prompt_entry);
login_button = new DashButton ("");
login_button.clicked.connect (login_button_clicked_cb);
add_with_class (login_button);
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);
}
session_button = new Gtk.Button ();
session_button.focus_on_click = false;
session_button.get_accessible ().set_name (_("Session Options"));
session_image = new CachedImage (get_badge ());
session_image.show ();
session_button.relief = Gtk.ReliefStyle.NONE;
session_button.add (session_image);
session_button.clicked.connect (session_button_clicked_cb);
session_button.show ();
add_with_class (session_button);
scroll_timer = new AnimateTimer (AnimateTimer.ease_out_quint, AnimateTimer.FAST);
scroll_timer.animate.connect (scroll_animate_cb);
try
{
Bus.get.begin (BusType.SESSION, null, on_bus_acquired);
}
catch (IOError e)
{
debug ("Error getting session bus: %s", e.message);
}
// Start with manual entry, because no users have been added yet.
// It will be auto-removed when those are added (unless we're told to
// always show a manual entry).
add_manual_entry ();
}
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 void cancel_authentication ()
{
user_selected (selected_entry.name);
}
public void scroll (ScrollTarget target)
{
if (mode != Mode.LOGIN && mode != Mode.SCROLLING)
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;
}
}
private void add_with_class (Gtk.Widget widget)
{
fixed.add (widget);
UnityGreeter.add_style_class (widget);
}
private void redraw_user_list ()
{
var y = box_y - (int) (n_above + 1) * grid_size;
var height = (int) (n_above + 1 + box_height + n_below + 1) * grid_size;
Gtk.Allocation allocation;
get_allocation (out allocation);
queue_draw_area (allocation.x + box_x, allocation.y + y, box_width * grid_size, height);
}
private void redraw_login_box ()
{
/* Resize to fit messages */
var vertical_offset = 0.0;
foreach (var m in messages)
{
int w, h;
m.layout.get_pixel_size (out w, out h);
vertical_offset += h;
}
var new_box_height = 3 + (int) vertical_offset / grid_size;
if (new_box_height != box_height)
{
box_height = new_box_height;
allocate_login_box ();
}
Gtk.Allocation allocation;
login_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)
{
var layout = create_pango_layout (text);
layout.set_font_description (Pango.FontDescription.from_string ("Ubuntu 10"));
messages.append (new AuthenticationMessage (layout, is_error));
redraw_login_box ();
}
public bool have_messages ()
{
return messages != null;
}
public void clear_messages ()
{
messages = new List ();
redraw_login_box ();
}
public void show_prompt (string text, bool secret = false)
{
login_button.hide ();
prompt_entry.hide ();
if (text.contains ("\n"))
{
show_message (text);
prompt_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));
}
prompt_entry.constant_placeholder_text = placeholder;
}
prompt_entry.text = "";
prompt_entry.sensitive = true;
prompt_entry.visibility = !secret;
if (mode == Mode.SCROLLING)
prompt_widget_to_show = prompt_entry;
else
prompt_entry.show ();
var accessible = prompt_entry.get_accessible ();
if (selected_entry != null && selected_entry.name != null)
accessible.set_name (_("Enter password for %s").printf (selected_entry.layout.get_text ()));
else
{
if (prompt_entry.visibility)
accessible.set_name (_("Enter username"));
else
accessible.set_name (_("Enter password"));
}
focus_prompt ();
redraw_login_box ();
}
public void focus_prompt ()
{
prompt_entry.grab_focus ();
}
public void show_authenticated (bool successful = true)
{
prompt_entry.hide ();
login_button.hide ();
var accessible = login_button.get_accessible ();
if (successful)
{
accessible.set_name (_("Login as %s").printf (selected_entry.layout.get_text ()));
/* 'Log In' here is the button for logging in. */
login_button.text = _("Log In");
}
else
{
accessible.set_name (_("Retry").printf (selected_entry.layout.get_text ()));
login_button.text = _("Retry");
}
if (mode == Mode.SCROLLING)
prompt_widget_to_show = login_button;
else
login_button.show ();
login_button.grab_focus ();
redraw_login_box ();
}
public void login_complete ()
{
complete = true;
sensitive = false;
clear_messages ();
show_message (_("Logging in..."));
login_button.hide ();
prompt_entry.hide ();
redraw_login_box ();
}
private UserEntry? find_entry (string name)
{
foreach (var entry in entries)
{
if (entry.name == name)
return entry;
}
return null;
}
public void add_entry (string name, string label, string? background = null, List ? keyboard_layouts = null, bool is_active = false, bool has_messages = false, string? session = null)
{
var e = find_entry (name);
if (e == null)
{
e = new UserEntry ();
e.name = name;
}
else
entries.remove (e);
e.layout = create_pango_layout (label);
e.layout.set_font_description (Pango.FontDescription.from_string ("Ubuntu 16"));
e.background = background;
e.keyboard_layouts = keyboard_layouts.copy ();
e.is_active = is_active;
e.has_messages = has_messages;
e.session = session;
entries.insert_sorted (e, compare_entry);
if (selected_entry == null)
select_entry (e, 1.0);
else
select_entry (selected_entry, 1.0);
/* Remove manual option when have users */
if (have_users () && !always_show_manual)
remove_entry ("*other");
redraw_user_list ();
}
private static int compare_entry (UserEntry a, UserEntry b)
{
if (a.name.has_prefix ("*") || b.name.has_prefix ("*"))
{
/* Special entries go after normal ones */
if (!a.name.has_prefix ("*"))
return -1;
if (!b.name.has_prefix ("*"))
return 1;
/* Manual comes before guest */
if (a.name == "*other")
return -1;
if (a.name == "*guest")
return 1;
}
/* Alphabetical by label */
return a.layout.get_text ().ascii_casecmp (b.layout.get_text ());
}
private bool have_users ()
{
foreach (var e in entries)
{
if (!e.name.has_prefix ("*"))
return true;
}
return false;
}
private void add_manual_entry ()
{
var text = manual_username;
if (text == null)
text = _("Login");
add_entry ("*other", text);
}
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 remove_entry (string? name)
{
var entry = find_entry (name);
if (entry == null)
return;
var index = entries.index (entry);
entries.remove (entry);
/* Show a manual login if no users */
if (!have_users ())
add_manual_entry ();
/* Select previous entry if the selected one was removed */
if (entry == selected_entry)
{
if (index >= entries.length ())
index--;
select_entry (entries.nth_data (index), -1.0);
}
redraw_user_list ();
}
private void prompt_entry_activate_cb ()
{
prompt_entry.sensitive = false;
respond_to_prompt (prompt_entry.text);
}
private void login_button_clicked_cb ()
{
debug ("Start session for %s", selected_entry.name);
start_session ();
}
private void session_button_clicked_cb ()
{
return_if_fail (mode == Mode.LOGIN);
mode = Mode.TRANSFORM_TO_SESSIONS_HIDE;
scroll_timer.reset (AnimateTimer.INSTANT);
}
private void session_clicked_cb (string? session)
{
return_if_fail (mode == Mode.SESSIONS);
mode = Mode.TRANSFORM_TO_LOGIN_HIDE;
session_chooser.fade_out ();
if (session != null)
this.session = session;
}
private void session_fade_done_cb ()
{
/* Either a fade in into SESSIONS mode or a fade out from SESSIONS mode */
if (mode == Mode.TRANSFORM_TO_LOGIN_HIDE)
{
login_box.remove (session_chooser);
mode = Mode.TRANSFORM_TO_LOGIN_SHOW;
scroll_timer.reset (AnimateTimer.INSTANT);
if (prompt_widget_to_show != null)
{
prompt_widget_to_show.show ();
prompt_widget_to_show.grab_focus ();
}
session_button.show ();
}
}
private void scroll_animate_cb (double progress)
{
switch (mode)
{
case Mode.SCROLLING:
animate_scrolling (progress);
break;
case Mode.TRANSFORM_TO_SESSIONS_HIDE:
animate_to_sessions_hide (progress);
break;
case Mode.TRANSFORM_TO_LOGIN_SHOW:
animate_to_login (progress);
break;
}
}
private void animate_to_sessions_hide (double progress)
{
allocate_login_box ();
/* Stop when we get there */
if (progress >= 1.0)
finished_to_sessions_hide ();
redraw_user_list ();
}
private void finished_to_sessions_hide ()
{
login_box.add (session_chooser);
/* Hide the other login_box widgets while sessions is visible, to
avoid confusing tab-navigation chain. This would be easier if these
were in a proper widget hierarchy, but since they are all just
floating around in a GtkFixed, we have to work a little harder. */
if (prompt_entry.visible)
prompt_widget_to_show = prompt_entry;
else if (login_button.visible)
prompt_widget_to_show = login_button;
prompt_entry.hide ();
login_button.hide ();
session_button.hide ();
session_chooser.fade_in ();
session_chooser.child_focus (Gtk.DirectionType.TAB_FORWARD);
mode = Mode.SESSIONS;
}
private void animate_to_login (double progress)
{
allocate_login_box ();
/* Stop when we get there */
if (progress >= 1.0)
finished_to_login ();
redraw_user_list ();
}
private void finished_to_login ()
{
mode = Mode.LOGIN;
}
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;
/* And finally, redraw */
redraw_user_list ();
if (progress >= 0.975 && prompt_widget_to_show != null)
{
prompt_widget_to_show.fade_in ();
prompt_widget_to_show = null;
user_displayed_start ();
}
/* Stop when we get there */
if (progress >= 1.0)
finished_scrolling ();
}
private void finished_scrolling ()
{
session_button.show ();
user_displayed_done ();
mode = Mode.LOGIN;
}
private void select_entry (UserEntry entry, double direction)
{
last_session_badge = get_badge ();
if (!get_realized ())
{
/* Just note it for the future if we haven't been realized yet */
selected_entry = entry;
session = entry.session;
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)
{
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 * (new_distance)), 500));
if (prompt_entry.visible)
prompt_widget_to_show = prompt_entry;
else if (login_button.visible)
prompt_widget_to_show = login_button;
prompt_entry.hide ();
login_button.hide ();
session_button.hide ();
mode = Mode.SCROLLING;
}
scroll_target_location = new_target;
scroll_direction = new_direction;
scroll_start_location = new_start;
}
if (selected_entry != entry)
{
selected_entry = entry;
session = entry.session;
user_selected (selected_entry.name);
if (mode == Mode.LOGIN)
user_displayed_done (); /* didn't need to move, make sure we trigger side effects */
}
}
private Gdk.Pixbuf? get_badge ()
{
if (session == null)
return SessionChooser.get_badge (default_session);
else
return SessionChooser.get_badge (session);
}
public override void realize ()
{
base.realize ();
var saved_entry = selected_entry;
selected_entry = null;
select_entry (saved_entry, 1);
}
private int round_up_to_grid_size (int size)
{
if (size % grid_size == 0)
return size;
else
return (size / grid_size + 1) * grid_size;
}
private void allocate_login_box ()
{
Gtk.Allocation allocation;
get_allocation (out allocation);
var child_allocation = Gtk.Allocation ();
child_allocation.x = allocation.x + box_x + 6;
child_allocation.y = allocation.y + box_y + 6;
child_allocation.width = grid_size * box_width - 12;
child_allocation.height = grid_size * box_height - 6;
Gtk.Requisition session_request;
session_chooser.get_preferred_size (null, out session_request);
var session_height = round_up_to_grid_size (session_request.height + 12);
if ((session_height / grid_size) % 2 == 0)
session_height += grid_size; /* make sure we expand equally top and bottom */
session_height -= 12;
session_height = int.max (session_height, child_allocation.height);
var session_distance = session_height - child_allocation.height;
switch (mode)
{
case Mode.TRANSFORM_TO_SESSIONS_HIDE:
child_allocation.height += (int) (scroll_timer.progress * session_distance);
child_allocation.y -= (int) (scroll_timer.progress * session_distance) / 2;
break;
case Mode.SESSIONS:
case Mode.TRANSFORM_TO_LOGIN_HIDE:
child_allocation.height = session_height;
child_allocation.y -= session_distance / 2;
break;
case Mode.TRANSFORM_TO_LOGIN_SHOW:
child_allocation.height = session_height - (int) (scroll_timer.progress * session_distance);
child_allocation.y -= (int) ((1.0 - scroll_timer.progress) * session_distance) / 2;
break;
}
login_box.size_allocate (child_allocation);
}
public override void size_allocate (Gtk.Allocation allocation)
{
base.size_allocate (allocation);
if (!get_realized ())
return;
allocate_login_box ();
var child_allocation = Gtk.Allocation ();
/* Put prompt entry and login button inside login box */
child_allocation.x = allocation.x + box_x + grid_size / 2;
child_allocation.y = allocation.y + box_y + grid_size * (box_height - 1) - grid_size / 4;
child_allocation.width = grid_size * (box_width - 1);
prompt_entry.get_preferred_height (null, out child_allocation.height);
prompt_entry.size_allocate (child_allocation);
login_button.get_preferred_height (null, out child_allocation.height);
login_button.size_allocate (child_allocation);
child_allocation.x = allocation.x + box_x + box_width * grid_size - grid_size - grid_size / 4;
child_allocation.y = allocation.y + box_y + grid_size / 4;
child_allocation.width = grid_size;
child_allocation.height = grid_size;
session_button.size_allocate (child_allocation);
}
private Cairo.Surface entry_ensure_label_surface (UserEntry entry, Cairo.Context orig_c, bool in_box)
{
if (in_box && entry.label_in_box_surface != null)
return entry.label_in_box_surface;
else if (!in_box && entry.label_out_of_box_surface != null)
return entry.label_out_of_box_surface;
int w, h;
entry.layout.get_pixel_size (out w, out h);
var bw = (box_width - (in_box ? 1.5 : 0.5)) * grid_size;
var surface = new Cairo.Surface.similar (orig_c.get_target (), Cairo.Content.COLOR_ALPHA, (int)(bw+1), h);
var c = new Cairo.Context (surface);
if (w > bw)
{
var mask = new Cairo.Pattern.linear (0, 0, bw, 0);
if (in_box)
{
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);
}
else
mask.add_color_stop_rgba (1.0 - 64.0 / bw, 1.0, 1.0, 1.0, 1.0);
mask.add_color_stop_rgba (1.0, 1.0, 1.0, 1.0, 0.0);
c.set_source (mask);
}
else
c.set_source_rgba (1.0, 1.0, 1.0, 1.0);
Pango.cairo_show_layout (c, entry.layout);
if (in_box)
entry.label_in_box_surface = surface;
else
entry.label_out_of_box_surface = surface;
return surface;
}
private void draw_entry (Cairo.Context c, UserEntry entry, double alpha = 0.5, bool in_box = false, Gdk.Pixbuf? badge = null)
{
c.save ();
if (menubar.high_contrast || in_box)
alpha = 1.0;
if (entry.is_active)
{
c.move_to (8, grid_size / 2 + 0.5 - 4 + border);
c.rel_line_to (5, 4);
c.rel_line_to (-5, 4);
c.close_path ();
c.set_source_rgba (1.0, 1.0, 1.0, alpha);
c.fill ();
}
int w, h;
entry.layout.get_pixel_size (out w, out h);
var label_x = grid_size / 2;
var label_y = grid_size / 4 + border;
var label_surface = entry_ensure_label_surface (entry, c, in_box);
c.set_source_surface (label_surface, label_x, label_y);
c.paint_with_alpha (alpha);
var bw = (int) ((box_width - (in_box ? 1.5 : 0.5)) * grid_size);
if (entry.has_messages && (!in_box || label_x + w + 6 + message_pixbuf.get_width () < bw))
{
c.translate (label_x + w + 6, label_y + (h - message_pixbuf.get_height ()) / 2);
var surface = CachedImage.get_cached_surface (c, message_pixbuf);
c.set_source_surface (surface, 0, 0);
c.paint_with_alpha (alpha);
}
c.restore ();
/* Now draw session button if we're animating in the box */
if (in_box && mode == Mode.SCROLLING && badge != null)
{
c.save ();
var xpadding = (grid_size - badge.width) / 2;
/* FIXME: The 18px offset here is because the visual assets changed size from 40px to 22px. It should be fixed properly somewhere... */
var ypadding = (grid_size - badge.height) / 2 - 18;
c.translate (box_width * grid_size - grid_size - grid_size / 4 + xpadding, grid_size / 4 - ypadding - border);
var surface = CachedImage.get_cached_surface (c, badge);
c.set_source_surface (surface, 0, 0);
c.paint ();
c.restore ();
}
}
private void draw_entry_at_position (Cairo.Context c, UserEntry entry, double position, bool in_box = false, Gdk.Pixbuf? badge = null)
{
c.save ();
c.translate (0, position * grid_size);
var alpha = 1.0;
if (position < 0)
alpha = 1.0 + position / (n_above + 1);
else
alpha = 1.0 - (position - 2) / (n_below + 1);
draw_entry (c, entry, alpha, in_box, badge);
c.restore ();
}
public override bool draw (Cairo.Context c)
{
var max_alpha = 1.0;
if (mode == Mode.TRANSFORM_TO_SESSIONS_HIDE)
max_alpha = 1.0 - scroll_timer.progress;
else if (mode == Mode.TRANSFORM_TO_LOGIN_SHOW)
max_alpha = scroll_timer.progress;
c.save ();
fixed.propagate_draw (login_box, c); /* Always full alpha */
c.restore ();
if (mode == Mode.LOGIN ||
mode == Mode.SCROLLING ||
mode == Mode.TRANSFORM_TO_SESSIONS_HIDE ||
mode == Mode.TRANSFORM_TO_LOGIN_SHOW)
{
c.save ();
c.push_group ();
draw_names (c, NameLocation.OUTSIDE_BOX);
draw_box_contents (c);
draw_names (c, NameLocation.INSIDE_BOX);
c.pop_group_to_source ();
c.paint_with_alpha (max_alpha);
c.restore ();
}
return false;
}
private enum NameLocation
{
INSIDE_BOX,
OUTSIDE_BOX,
}
private void draw_names (Cairo.Context c, NameLocation where)
{
c.save ();
c.translate (box_x, box_y);
var index = 0;
foreach (var entry in entries)
{
var position = index - scroll_location;
/* Draw entries above the box */
if (where == NameLocation.OUTSIDE_BOX && position < 0 && position > -1 * (int)(n_above + 1))
{
var h_above = (double) (n_above + 1) * grid_size;
c.save ();
c.rectangle (0, -h_above, box_width * grid_size, h_above);
c.clip ();
draw_entry_at_position (c, entry, position);
c.restore ();
}
/* Draw entries in the box */
if (where == NameLocation.INSIDE_BOX && position > -1 && position < 1 && mode == Mode.SCROLLING)
{
c.save ();
c.translate (0, border);
c.rectangle (border, border * 2, box_width * grid_size - border * 2, box_height * grid_size - border * 2);
c.clip ();
var badge = last_session_badge;
if (entry == selected_entry)
badge = get_badge ();
if (position <= 0) /* top of box, normal pace */
draw_entry_at_position (c, entry, position, true, badge);
else /* bottom of box; pace must put across bottom halfway through animation */
draw_entry_at_position (c, entry, position * box_height * 2, true, badge);
c.restore ();
}
/* Draw entries below the box */
if (where == NameLocation.OUTSIDE_BOX && position > 0 && position < n_below + 1)
{
var h_below = (double) (n_below + 1) * grid_size;
c.save ();
c.rectangle (0, box_height * grid_size, box_width * grid_size, h_below);
c.clip ();
draw_entry_at_position (c, entry, position + box_height - 1);
c.restore ();
}
index++;
}
c.restore ();
}
private void draw_box_contents (Cairo.Context c)
{
foreach (var child in fixed.get_children ())
{
if (child != login_box)
fixed.propagate_draw (child, c);
}
c.save ();
c.translate (box_x, box_y);
/* Selected item */
if (selected_entry != null && mode != Mode.SCROLLING)
{
c.save ();
c.rectangle (border, border, box_width * grid_size - border * 2, box_height * grid_size - border * 2);
c.clip ();
c.translate (0, border);
draw_entry (c, selected_entry, 1.0, true);
c.restore ();
}
if (mode != Mode.SCROLLING)
{
var vertical_offset = 0.0;
foreach (var m in messages)
{
int w, h;
m.layout.get_pixel_size (out w, out h);
vertical_offset += h;
}
foreach (var m in messages)
{
int w, h;
m.layout.get_pixel_size (out w, out h);
c.move_to (grid_size / 2, grid_size * (box_height - 1.5) - vertical_offset + border);
vertical_offset -= h;
var r = 1.0;
var g = 1.0;
var b = 1.0;
if (m.is_error)
{
r = 1.0;
g = 0.0;
b = 0.0;
}
var bw = (box_width - 0.5) * grid_size;
if (w > bw)
{
var mask = new Cairo.Pattern.linear (0, 0, bw, 0);
mask.add_color_stop_rgba (1.0 - 0.5 * grid_size / bw, r, g, b, 1.0);
mask.add_color_stop_rgba (1.0, r, g, b, 0.0);
c.set_source (mask);
}
else
c.set_source_rgb (r, g, b);
Pango.cairo_show_layout (c, m.layout);
}
}
c.restore ();
}
private bool inside_entry (double x, double y, double entry_y, UserEntry entry)
{
int w, h;
entry.layout.get_pixel_size (out w, out h);
/* Allow space to the left of the entry */
w += grid_size / 2;
/* Round up to whole grid sizes */
h = grid_size;
w = ((int) (w + grid_size) / grid_size) * grid_size;
return x >= 0 && x <= w && y >= entry_y && y <= entry_y + h;
}
public override bool button_release_event (Gdk.EventButton event)
{
if (mode != Mode.LOGIN)
return false;
var x = event.x - box_x;
var y = event.y - box_y;
/* Total height of list */
var h = (double) entries.length () * grid_size;
var offset = 0.0;
foreach (var entry in entries)
{
var entry_y = -scroll_location * grid_size + offset;
/* Check entry above the box */
var h_above = (double) n_above * grid_size;
if (entry_y < 0 && y < 0 && y > -h_above)
{
if (inside_entry (x, y, entry_y, entry) ||
inside_entry (x, y, entry_y - h, entry))
{
select_entry (entry, -1.0);
return true;
}
}
/* Check entry below the box */
var below_y = y - box_height * grid_size;
var h_below = (double) n_below * grid_size;
if (entry_y > 0 && below_y > 0 && below_y < h_below)
{
if (inside_entry (x, below_y, entry_y - grid_size, entry) ||
inside_entry (x, below_y, entry_y - grid_size + h, entry))
{
select_entry (entry, 1.0);
return true;
}
}
offset += grid_size;
}
return false;
}
}