Don't force user to save password: Closes #5317, #5291, #5218, #5083.

- Give the user a "remember password" option that they can uncheck if
  they don't want Geary to remember their password. You've seen most of
  this part of the patch before, aside from a few bugfixes.
- Display a nicer dialog when re-prompting the user for their password.
  This only shows (editable) fields for "password" and "remember
  password" by default, with (non-editable) fields for "Service" and
  "Real name" available in an expander.
- Fix a crash that occurred whenever an account is connected when there
  was previously another account (or the same account) connected.
This commit is contained in:
Matthew Pirocchi 2012-06-06 10:39:49 -07:00
parent 8ff56dd947
commit c3340e91fb
14 changed files with 504 additions and 111 deletions

View file

@ -185,6 +185,7 @@ client/ui/message-list-cell-renderer.vala
client/ui/message-list-store.vala
client/ui/message-list-view.vala
client/ui/message-viewer.vala
client/ui/password-dialog.vala
client/ui/preferences-dialog.vala
client/ui/sidebar/sidebar-branch.vala

View file

@ -165,9 +165,24 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
return;
}
private void initialize_account(bool replace_existing_data = false) {
private void set_account(Geary.EngineAccount? account) {
if (this.account == account)
return;
if (this.account != null)
this.account.report_problem.disconnect(on_report_problem);
this.account = account;
if (this.account != null)
this.account.report_problem.connect(on_report_problem);
controller.connect_account(this.account);
}
private void initialize_account() {
string? username = get_username();
if (username == null || replace_existing_data)
if (username == null)
create_account(username);
else
open_account(username);
@ -213,16 +228,14 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
if (success) {
account_information.store_async.begin(cancellable);
account = account_information.get_account();
account.report_problem.connect(on_report_problem);
controller.connect_account(account);
set_account(account_information.get_account());
} else {
Geary.AccountInformation new_account_information =
request_account_information(account_information);
// If the user refused to enter account information.
if (new_account_information == null) {
account = null;
set_account(null);
return;
}
@ -230,30 +243,29 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
}
}
private void open_account(string username) {
string? password = get_password(username);
if (password == null) {
password = request_password(username);
// If the user refused to enter a password.
if (password == null) {
account = null;
return;
}
}
// Now we know password is non-null.
Geary.Credentials credentials = new Geary.Credentials(username, password);
private void open_account(string username, string? old_password = null, Cancellable? cancellable = null) {
Geary.Credentials credentials = new Geary.Credentials(username, null);
Geary.AccountInformation account_information = new Geary.AccountInformation(credentials);
try {
account_information.load_info_from_file();
} catch (Error err) {
error("Problem loading account information: %s", err.message);
// TODO: Handle this more gracefully?
error("Problem loading account information from file: %s", err.message);
}
account = account_information.get_account();
account.report_problem.connect(on_report_problem);
controller.connect_account(account);
bool remember_password = account_information.remember_password;
string? password = get_password(account_information.credentials.user, old_password, ref remember_password);
// If there was no saved password and the user refused to enter a password.
if (password == null) {
set_account(null);
return;
}
account_information.remember_password = remember_password;
account_information.store_async.begin(cancellable);
account_information.credentials.pass = password;
set_account(account_information.get_account());
}
private string? get_username() {
@ -270,34 +282,51 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
return null;
}
private string? get_password(string username) {
// TODO: For now we always get the password from the keyring. This will change when we
// allow users to not save their password.
string? password = keyring_get_password(username);
return Geary.String.is_empty(password) ? null : password;
}
private string? get_password(string username, string? old_password, ref bool remember_password) {
string? password = null;
if (old_password == null && remember_password)
password = keyring_get_password(username);
if (Geary.String.is_null_or_whitespace(password))
password = request_password(username, old_password, out remember_password);
return password;
}
private string get_default_real_name() {
string real_name = Environment.get_real_name();
return real_name == "Unknown" ? "" : real_name;
}
private string? request_password(string username) {
// TODO: For now we use the full LoginDialog. This should be changed to a dialog that only
// allows editting the password.
Geary.Credentials credentials = new Geary.Credentials(username, null);
private string? request_password(string username, string? old_password, out bool remember_password) {
Geary.Credentials credentials = new Geary.Credentials(username, old_password);
Geary.AccountInformation old_account_information = new Geary.AccountInformation(credentials);
try {
old_account_information.load_info_from_file();
} catch (Error err) {
debug("Problem loading account information: %s", err.message);
old_account_information = null;
// TODO: Handle this more gracefully?
error("Error loading account information: %s", err.message);
}
Geary.AccountInformation account_information = request_account_information(old_account_information);
return account_information == null ? null : account_information.credentials.pass;
PasswordDialog password_dialog = new PasswordDialog(old_account_information);
if (!password_dialog.run()) {
exit(1);
remember_password = false;
return null;
}
// password_dialog.password should never be null at this point. It will only be null when
// password_dialog.run() returns false, in which case we have already exited/returned.
string? password = password_dialog.password;
remember_password = password_dialog.remember_password;
if (remember_password)
keyring_save_password(new Geary.Credentials(username, password));
else
keyring_delete_password(username);
return password;
}
// Prompt the user for a service, real name, username, and password, and try to start Geary.
@ -312,8 +341,10 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
return null;
}
// TODO: This should be optional.
keyring_save_password(login_dialog.account_information.credentials);
if (login_dialog.account_information.remember_password)
keyring_save_password(login_dialog.account_information.credentials);
else
keyring_delete_password(login_dialog.account_information.credentials.user);
return login_dialog.account_information;
}
@ -330,10 +361,9 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
case Geary.Account.Problem.LOGIN_FAILED:
debug("Login failed.");
if (controller != null)
controller.stop();
Geary.Credentials old_credentials = account.get_account_information().credentials;
account.report_problem.disconnect(on_report_problem);
initialize_account(true);
open_account(old_credentials.user, old_credentials.pass);
break;
default:

View file

@ -77,10 +77,7 @@ public class GearyController {
private int conversations_added_counter = 0;
private Gee.LinkedList<ComposerWindow> composer_windows = new Gee.LinkedList<ComposerWindow>();
private Geary.EngineAccount? _account = null;
private Geary.EngineAccount? account {
get { return _account; }
}
private Geary.EngineAccount? account { get; private set; }
public GearyController() {
// Setup actions.
@ -245,13 +242,16 @@ public class GearyController {
return entries;
}
public void connect_account(Geary.EngineAccount? account) {
if (_account == account)
public void connect_account(Geary.EngineAccount? new_account) {
if (account == new_account)
return;
// Disconnect the old account, if any.
if (_account != null) {
_account.folders_added_removed.disconnect(on_folders_added_removed);
if (account != null) {
cancel_folder();
cancel_message();
account.folders_added_removed.disconnect(on_folders_added_removed);
Gtk.Action delete_message = GearyApplication.instance.actions.get_action(ACTION_DELETE_MESSAGE);
delete_message.label = DEFAULT_DELETE_MESSAGE_LABEL;
@ -260,30 +260,29 @@ public class GearyController {
main_window.title = GearyApplication.NAME;
main_window.folder_list.set_user_folders_root_name("");
main_window.folder_list.remove_all_branches();
}
_account = account;
account = new_account;
// Connect the new account, if any.
if (_account != null) {
_account.folders_added_removed.connect(on_folders_added_removed);
if (account != null) {
account.folders_added_removed.connect(on_folders_added_removed);
// Personality-specific setup.
if (_account.delete_is_archive()) {
if (account.delete_is_archive()) {
Gtk.Action delete_message = GearyApplication.instance.actions.get_action(ACTION_DELETE_MESSAGE);
delete_message.label = _("Archive Message");
delete_message.tooltip = _("Archive the selected conversation");
delete_message.icon_name = "archive-insert";
}
if (_account.get_account_information().service_provider == Geary.ServiceProvider.YAHOO)
if (account.get_account_information().service_provider == Geary.ServiceProvider.YAHOO)
main_window.title = GearyApplication.NAME + "!";
main_window.folder_list.set_user_folders_root_name(_account.get_user_folders_label());
main_window.folder_list.set_user_folders_root_name(account.get_user_folders_label());
load_folders.begin(cancellable_folder);
}
load_folders.begin(cancellable_folder);
}
private bool is_viewed_conversation(Geary.Conversation? conversation) {
@ -336,12 +335,6 @@ public class GearyController {
}
}
public void stop() {
cancel_folder();
cancel_message();
connect_account(null);
}
private void on_folder_selected(Geary.Folder? folder) {
if (folder == null) {
debug("no folder selected");

View file

@ -114,9 +114,7 @@ public class FolderList : Sidebar.Tree {
base(new Gtk.TargetEntry[0], Gdk.DragAction.ASK, drop_handler);
entry_selected.connect(on_entry_selected);
user_folder_group = new Sidebar.Grouping("", IconFactory.instance.label_folder_icon);
user_folder_branch = new Sidebar.Branch(user_folder_group,
Sidebar.Branch.Options.STARTUP_OPEN_GROUPING, user_folder_comparator);
reset_user_folder_group();
graft(user_folder_branch, int.MAX);
// Set self as a drag destination.
@ -146,6 +144,12 @@ public class FolderList : Sidebar.Tree {
user_folder_group.rename(name);
}
private void reset_user_folder_group() {
user_folder_group = new Sidebar.Grouping("", IconFactory.instance.label_folder_icon);
user_folder_branch = new Sidebar.Branch(user_folder_group,
Sidebar.Branch.Options.STARTUP_OPEN_GROUPING, user_folder_comparator);
}
public void add_folder(Geary.Folder folder) {
FolderEntry folder_entry = new FolderEntry(folder);
@ -174,6 +178,12 @@ public class FolderList : Sidebar.Tree {
entries.set(folder.get_path(), branch.get_root());
}
public void remove_all_branches() {
prune_all();
entries.clear();
reset_user_folder_group();
}
public void select_path(Geary.FolderPath path) {
Sidebar.Entry? entry = get_entry_for_folder_path(path);
if (entry != null)

View file

@ -11,6 +11,7 @@ public class LoginDialog {
private Gtk.Entry entry_password;
private Gtk.Entry entry_real_name;
private Gtk.ComboBoxText combo_service;
private Gtk.CheckButton check_remember_password;
private Gtk.Alignment other_info;
private Gtk.Entry entry_imap_host;
@ -29,17 +30,18 @@ public class LoginDialog {
public LoginDialog.from_account_information(Geary.AccountInformation default_account_information) {
this(default_account_information.real_name, default_account_information.credentials.user,
default_account_information.credentials.pass, default_account_information.service_provider,
default_account_information.imap_server_host, default_account_information.imap_server_port,
default_account_information.imap_server_ssl, default_account_information.smtp_server_host,
default_account_information.smtp_server_port, default_account_information.smtp_server_ssl);
default_account_information.credentials.pass, default_account_information.remember_password,
default_account_information.service_provider, default_account_information.imap_server_host,
default_account_information.imap_server_port, default_account_information.imap_server_ssl,
default_account_information.smtp_server_host, default_account_information.smtp_server_port,
default_account_information.smtp_server_ssl);
}
public LoginDialog(string default_real_name, string? default_username = null,
string? default_password = null, int default_service_provider = -1,string? default_imap_host = null,
uint16 default_imap_port = Geary.Imap.ClientConnection.DEFAULT_PORT_SSL, bool default_imap_ssl = true,
string? default_smtp_host = null, uint16 default_smtp_port = Geary.Smtp.ClientConnection.DEFAULT_PORT_SSL,
bool default_smtp_ssl = true) {
public LoginDialog(string? default_real_name = null, string? default_username = null,
string? default_password = null, bool default_remember_password = true, int default_service_provider = -1,
string? default_imap_host = null, uint16 default_imap_port = Geary.Imap.ClientConnection.DEFAULT_PORT_SSL,
bool default_imap_ssl = true, string? default_smtp_host = null,
uint16 default_smtp_port = Geary.Smtp.ClientConnection.DEFAULT_PORT_SSL, bool default_smtp_ssl = true) {
Gtk.Builder builder = GearyApplication.instance.create_builder("login.glade");
dialog = builder.get_object("LoginDialog") as Gtk.Dialog;
@ -50,6 +52,7 @@ public class LoginDialog {
combo_service = builder.get_object("service") as Gtk.ComboBoxText;
entry_username = builder.get_object("username") as Gtk.Entry;
entry_password = builder.get_object("password") as Gtk.Entry;
check_remember_password = builder.get_object("remember_password") as Gtk.CheckButton;
other_info = builder.get_object("other_info") as Gtk.Alignment;
entry_imap_host = builder.get_object("imap host") as Gtk.Entry;
@ -74,6 +77,7 @@ public class LoginDialog {
entry_real_name.set_text(default_real_name ?? "");
entry_username.set_text(default_username ?? "");
entry_password.set_text(default_password ?? "");
check_remember_password.active = default_remember_password;
entry_imap_host.set_text(default_imap_host ?? "");
entry_imap_port.set_text(default_imap_port.to_string());
check_imap_ssl.active = default_imap_ssl;
@ -89,6 +93,7 @@ public class LoginDialog {
entry_username.changed.connect(on_changed);
entry_password.changed.connect(on_changed);
entry_real_name.changed.connect(on_changed);
check_remember_password.toggled.connect(on_changed);
combo_service.changed.connect(on_changed);
entry_imap_host.changed.connect(on_changed);
entry_imap_port.changed.connect(on_changed);
@ -124,6 +129,7 @@ public class LoginDialog {
account_information = new Geary.AccountInformation(credentials);
account_information.real_name = entry_real_name.text.strip();
account_information.remember_password = check_remember_password.active;
account_information.service_provider = get_service_provider();
account_information.imap_server_host = entry_imap_host.text.strip();
account_information.imap_server_port = (uint16) int.parse(entry_imap_port.text.strip());
@ -132,6 +138,8 @@ public class LoginDialog {
account_information.smtp_server_port = (uint16) int.parse(entry_smtp_port.text.strip());
account_information.smtp_server_ssl = check_smtp_ssl.active;
on_changed();
dialog.destroy();
return true;
}
@ -184,8 +192,8 @@ public class LoginDialog {
}
private bool is_complete() {
if (Geary.String.is_empty(entry_username.text.strip()) ||
Geary.String.is_empty(entry_password.text.strip()))
if (Geary.String.is_null_or_whitespace(entry_username.text.strip()) ||
Geary.String.is_null_or_whitespace(entry_password.text.strip()))
return false;
// For "other" providers, check server settings.

View file

@ -0,0 +1,80 @@
/* Copyright 2011-2012 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Displays a dialog for collecting the user's password, without allowing them to change their
* other data.
*/
public class PasswordDialog {
private Gtk.Dialog dialog;
private Gtk.Entry password_entry;
private Gtk.CheckButton remember_password_checkbutton;
private Gtk.Button ok_button;
public string password { get; private set; default = ""; }
public bool remember_password { get; private set; }
public PasswordDialog(Geary.AccountInformation account_information) {
Gtk.Builder builder = GearyApplication.instance.create_builder("password-dialog.glade");
// Load dialog
dialog = (Gtk.Dialog)builder.get_object("PasswordDialog");
dialog.set_type_hint(Gdk.WindowTypeHint.DIALOG);
dialog.set_default_response(Gtk.ResponseType.OK);
// Load editable widgets
password_entry = (Gtk.Entry)builder.get_object("password_entry");
remember_password_checkbutton = (Gtk.CheckButton)builder.get_object("remember_password_checkbutton");
// Load non-editable widgets
Gtk.Label email_label = (Gtk.Label)builder.get_object("email_label");
Gtk.Label real_name_label = (Gtk.Label)builder.get_object("real_name_label");
Gtk.Label service_label = (Gtk.Label)builder.get_object("service_label");
// Load default values
email_label.set_text(account_information.credentials.user ?? "");
password_entry.set_text(account_information.credentials.pass ?? "");
remember_password_checkbutton.active = account_information.remember_password;
real_name_label.set_text(account_information.real_name ?? "");
service_label.set_text(account_information.service_provider.display_name() ?? "");
// Add action buttons
Gtk.Button cancel_button = new Gtk.Button.from_stock(Gtk.Stock.CANCEL);
ok_button = new Gtk.Button.from_stock(Gtk.Stock.OK);
ok_button.can_default = true;
dialog.add_action_widget(cancel_button, Gtk.ResponseType.CANCEL);
dialog.add_action_widget(ok_button, Gtk.ResponseType.OK);
dialog.set_default_response(Gtk.ResponseType.OK);
// Setup listeners
refresh_ok_button_sensitivity();
password_entry.changed.connect(refresh_ok_button_sensitivity);
}
private void refresh_ok_button_sensitivity() {
ok_button.sensitive = !Geary.String.is_null_or_whitespace(password_entry.get_text());
}
public bool run() {
dialog.show();
dialog.get_action_area().show_all();
Gtk.ResponseType response = (Gtk.ResponseType)dialog.run();
if (response != Gtk.ResponseType.OK) {
dialog.destroy();
return false;
}
password = password_entry.get_text();
remember_password = remember_password_checkbutton.active;
dialog.destroy();
return true;
}
}

View file

@ -237,7 +237,13 @@ public class Sidebar.Tree : Gtk.TreeView {
public bool is_selected(Sidebar.Entry entry) {
EntryWrapper? wrapper = get_wrapper(entry);
return (wrapper != null) ? get_selection().path_is_selected(wrapper.get_path()) : false;
// Even though get_selection() does not report its return type as nullable, it can be null
// if the window has been destroyed.
Gtk.TreeSelection selection = get_selection();
if (selection == null)
return false;
return (wrapper != null) ? selection.path_is_selected(wrapper.get_path()) : false;
}
public bool is_any_selected() {
@ -456,6 +462,16 @@ public class Sidebar.Tree : Gtk.TreeView {
return new_wrapper;
}
protected void prune_all() {
while (branches.keys.size > 0) {
Gee.Iterator<Sidebar.Branch> iterator = branches.keys.iterator();
if (!iterator.next())
break;
prune(iterator.get());
}
}
public void prune(Sidebar.Branch branch) {
assert(branches.has_key(branch));

View file

@ -9,17 +9,22 @@ const string GEARY_USERNAME_PREFIX = "org.yorba.geary username:";
public static bool keyring_save_password(Geary.Credentials credentials) {
string name = GEARY_USERNAME_PREFIX + credentials.user;
GnomeKeyring.Result res = GnomeKeyring.store_password_sync(GnomeKeyring.NETWORK_PASSWORD, null,
name, credentials.pass, "user", name);
GnomeKeyring.Result result = GnomeKeyring.store_password_sync(GnomeKeyring.NETWORK_PASSWORD,
null, name, credentials.pass, "user", name);
return result == GnomeKeyring.Result.OK;
}
return res == GnomeKeyring.Result.OK;
public static void keyring_delete_password(string username) {
GnomeKeyring.delete_password_sync(GnomeKeyring.NETWORK_PASSWORD, "user",
GEARY_USERNAME_PREFIX + username);
}
// Returns the password for the given username, or null if not set.
public static string? keyring_get_password(string username) {
string password;
GnomeKeyring.Result res = GnomeKeyring.find_password_sync(GnomeKeyring.NETWORK_PASSWORD, out password,
"user", GEARY_USERNAME_PREFIX + username);
GnomeKeyring.Result res = GnomeKeyring.find_password_sync(GnomeKeyring.NETWORK_PASSWORD,
out password, "user", GEARY_USERNAME_PREFIX + username);
return res == GnomeKeyring.Result.OK ? password : null;
}

View file

@ -8,6 +8,7 @@ public class Geary.AccountInformation : Object {
private const string GROUP = "AccountInformation";
private const string REAL_NAME_KEY = "real_name";
private const string SERVICE_PROVIDER_KEY = "service_provider";
private const string REMEMBER_PASSWORD_KEY = "remember_password";
private const string IMAP_HOST = "imap_host";
private const string IMAP_PORT = "imap_port";
private const string IMAP_SSL = "imap_ssl";
@ -18,6 +19,7 @@ public class Geary.AccountInformation : Object {
public const string SETTINGS_FILENAME = "geary.ini";
internal File? settings_dir;
internal File? file = null;
public string real_name { get; set; }
public Geary.ServiceProvider service_provider { get; set; }
@ -32,10 +34,13 @@ public class Geary.AccountInformation : Object {
public bool smtp_server_ssl { get; set; default = true; }
public Geary.Credentials credentials { get; private set; }
public bool remember_password { get; set; default = true; }
public AccountInformation(Geary.Credentials credentials) {
this.credentials = credentials;
this.file = get_settings_file();
this.settings_dir = Geary.Engine.user_data_dir.get_child(credentials.user);
this.file = settings_dir.get_child(SETTINGS_FILENAME);
}
public void load_info_from_file() throws Error {
@ -46,6 +51,7 @@ public class Geary.AccountInformation : Object {
// The file didn't exist. No big deal -- just means we give you the defaults.
} finally {
real_name = get_string_value(key_file, GROUP, REAL_NAME_KEY);
remember_password = get_bool_value(key_file, GROUP, REMEMBER_PASSWORD_KEY, true);
service_provider = Geary.ServiceProvider.from_string(get_string_value(key_file, GROUP,
SERVICE_PROVIDER_KEY));
@ -177,6 +183,15 @@ public class Geary.AccountInformation : Object {
public async void store_async(Cancellable? cancellable = null) {
assert(file != null);
if (!settings_dir.query_exists(cancellable)) {
try {
settings_dir.make_directory_with_parents();
} catch (Error err) {
error("Error creating settings directory for user '%s': %s", credentials.user,
err.message);
}
}
if (!file.query_exists(cancellable)) {
try {
yield file.create_async(FileCreateFlags.REPLACE_DESTINATION);
@ -189,6 +204,7 @@ public class Geary.AccountInformation : Object {
key_file.set_value(GROUP, REAL_NAME_KEY, real_name);
key_file.set_value(GROUP, SERVICE_PROVIDER_KEY, service_provider.to_string());
key_file.set_boolean(GROUP, REMEMBER_PASSWORD_KEY, remember_password);
key_file.set_value(GROUP, IMAP_HOST, imap_server_host);
key_file.set_integer(GROUP, IMAP_PORT, imap_server_port);
@ -206,11 +222,7 @@ public class Geary.AccountInformation : Object {
yield file.replace_contents_async(data.data, null, false, FileCreateFlags.NONE,
cancellable, out new_etag);
} catch (Error err) {
debug("Error writign to account info file: %s", err.message);
debug("Error writing to account info file: %s", err.message);
}
}
private File get_settings_file() {
return Geary.Engine.user_data_dir.get_child(credentials.user).get_child(SETTINGS_FILENAME);
}
}

View file

@ -92,7 +92,9 @@ public class Geary.ConversationMonitor : Object {
id_ascending.add(email);
id_descending.add(email);
message_ids.add_all(email.get_ancestors());
Gee.Set<RFC822.MessageID>? ancestors = email.get_ancestors();
if (ancestors != null)
message_ids.add_all(ancestors);
}
public void remove(Email email) {
@ -502,21 +504,23 @@ public class Geary.ConversationMonitor : Object {
// Right now, all threading is done with Message-IDs (no parsing of subject lines, etc.)
// If a message doesn't have a Message-ID, it's treated as its own conversation
Gee.Set<RFC822.MessageID> ancestors = email.get_ancestors();
Gee.Set<RFC822.MessageID>? ancestors = email.get_ancestors();
// see if any of these ancestor IDs maps to an existing conversation
ImplConversation? conversation = null;
foreach (ImplConversation known in conversations) {
foreach (RFC822.MessageID ancestor in ancestors) {
if (known.tracks_message_id(ancestor)) {
conversation = known;
break;
if (ancestors != null) {
foreach (ImplConversation known in conversations) {
foreach (RFC822.MessageID ancestor in ancestors) {
if (known.tracks_message_id(ancestor)) {
conversation = known;
break;
}
}
if (conversation != null)
break;
}
if (conversation != null)
break;
}
// create new conversation if not seen before

View file

@ -6,7 +6,7 @@
public class Geary.Credentials {
public string user { get; private set; }
public string pass { get; private set; }
public string pass { get; set; }
public Credentials(string? user, string? pass) {
this.user = user ?? "";

View file

@ -6,6 +6,10 @@
namespace Geary.String {
public inline bool is_null_or_whitespace(string? str) {
return str == null || str.strip()[0] == 0;
}
public inline bool is_empty(string? str) {
return (str == null || str[0] == 0);
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="2.24"/>
<!-- interface-requires gtk+ 3.0 -->
<object class="GtkDialog" id="LoginDialog">
<property name="can_focus">False</property>
<property name="can_default">True</property>
@ -187,10 +187,24 @@
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
<object class="GtkCheckButton" id="remember_password">
<property name="label" translatable="yes">_Remember password</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="xalign">0</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<placeholder/>

216
ui/password-dialog.glade Normal file
View file

@ -0,0 +1,216 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<!-- interface-requires gtk+ 3.0 -->
<object class="GtkDialog" id="PasswordDialog">
<property name="can_focus">False</property>
<property name="border_width">5</property>
<property name="title" translatable="yes">Login</property>
<property name="type_hint">dialog</property>
<child internal-child="vbox">
<object class="GtkBox" id="dialog-vbox1">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkGrid" id="grid1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">2</property>
<property name="column_spacing">4</property>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Email address:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">_Password:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">password_entry</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="email_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="xalign">0</property>
<property name="xpad">2</property>
<property name="label" translatable="yes">example@example.com</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="password_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="visibility">False</property>
<property name="invisible_char">•</property>
<property name="activates_default">True</property>
<property name="invisible_char_set">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="remember_password_checkbutton">
<property name="label" translatable="yes">_Remember password</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_underline">True</property>
<property name="xalign">0</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child internal-child="action_area">
<object class="GtkButtonBox" id="dialog-action_area1">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkExpander" id="expander1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<child>
<object class="GtkGrid" id="grid2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">2</property>
<property name="column_spacing">4</property>
<child>
<object class="GtkLabel" id="label5">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Service:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label6">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Real name:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="service_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Service</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="real_name_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Real name</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Details</property>
<property name="use_underline">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
</object>
</interface>