diff options
Diffstat (limited to 'src/prompt-box.vala')
-rw-r--r-- | src/prompt-box.vala | 663 |
1 files changed, 663 insertions, 0 deletions
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); + } +} |