Closes #6286 Delete accounts

Squashed commit of the following:

commit 1b045edd6c28e3f837107577726b61c839816bc2
Author: Eric Gregory <eric@yorba.org>
Date:   Mon Feb 4 19:25:36 2013 -0800

    Changes from code review

commit bcd52b9f571de316eda39bd181df121bc4753c40
Author: Eric Gregory <eric@yorba.org>
Date:   Mon Feb 4 18:21:53 2013 -0800

    Removed cancellable check

commit 1cc528ca39555233cd5e851229a242fc118c11ae
Author: Eric Gregory <eric@yorba.org>
Date:   Mon Feb 4 18:17:42 2013 -0800

    Delete account (2nd branch)
This commit is contained in:
Eric Gregory 2013-02-04 19:26:48 -08:00
parent 5e9703a1e5
commit 0350a8fe0f
17 changed files with 485 additions and 24 deletions

View file

@ -181,6 +181,7 @@ engine/state/state-mapping.vala
engine/util/util-collection.vala
engine/util/util-converter.vala
engine/util/util-files.vala
engine/util/util-generic-capabilities.vala
engine/util/util-html.vala
engine/util/util-inet.vala
@ -206,6 +207,7 @@ client/main.vala
client/accounts/account-dialog.vala
client/accounts/account-dialog-account-list-pane.vala
client/accounts/account-dialog-add-edit-pane.vala
client/accounts/account-dialog-remove-confirm-pane.vala
client/accounts/account-spinner-page.vala
client/accounts/add-edit-page.vala
client/accounts/login-dialog.vala

View file

@ -14,11 +14,14 @@ public class AccountDialogAccountListPane : Gtk.Box {
private Gtk.TreeView list_view;
private Gtk.ListStore list_model = new Gtk.ListStore(2, typeof (string), typeof (string));
private Gtk.Action edit_action;
private Gtk.Action delete_action;
public signal void add_account();
public signal void edit_account(string email_address);
public signal void delete_account(string email_address);
public signal void close();
public AccountDialogAccountListPane() {
@ -28,6 +31,7 @@ public class AccountDialogAccountListPane : Gtk.Box {
pack_end((Gtk.Box) builder.get_object("container"));
Gtk.ActionGroup actions = (Gtk.ActionGroup) builder.get_object("account list actions");
edit_action = actions.get_action("edit_account");
delete_action = actions.get_action("delete_account");
// Set up list.
list_view = (Gtk.TreeView) builder.get_object("account_list");
@ -43,7 +47,8 @@ public class AccountDialogAccountListPane : Gtk.Box {
actions.get_action("close").activate.connect(() => { close(); });
actions.get_action("add_account").activate.connect(() => { add_account(); });
edit_action.activate.connect(notify_edit_account);
list_view.get_selection().changed.connect(on_selection_changed);
delete_action.activate.connect(notify_delete_account);
list_view.get_selection().changed.connect(update_buttons);
list_view.button_press_event.connect(on_button_press);
// Theme hint: "join" the toolbar to the scrolled window above it.
@ -75,6 +80,12 @@ public class AccountDialogAccountListPane : Gtk.Box {
edit_account(account);
}
private void notify_delete_account() {
string? account = get_selected_account();
if (account != null)
delete_account(account);
}
private bool on_button_press(Gdk.EventButton event) {
if (event.type != Gdk.EventType.2BUTTON_PRESS)
return false;
@ -108,8 +119,19 @@ public class AccountDialogAccountListPane : Gtk.Box {
return account;
}
private void on_selection_changed() {
private void update_buttons() {
edit_action.sensitive = get_selected_account() != null;
delete_action.sensitive = edit_action.sensitive && get_num_accounts() > 1;
}
private int get_num_accounts() {
try {
return Geary.Engine.instance.get_accounts().size;
} catch (Error e) {
debug("Error getting number of accounts: %s", e.message);
}
return 0; // on error
}
private void on_account_added(Geary.AccountInformation account) {
@ -119,11 +141,13 @@ public class AccountDialogAccountListPane : Gtk.Box {
add_account_to_list(account.nickname, account.email);
account.notify.connect(on_account_changed);
update_buttons();
}
private void on_account_removed(Geary.AccountInformation account) {
remove_account_from_list(account.email);
account.notify.disconnect(on_account_changed);
update_buttons();
}
// Adds an account to the list.

View file

@ -0,0 +1,37 @@
/* Copyright 2013 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.
*/
// Confirmation of the deletion of an account
public class AccountDialogRemoveConfirmPane : Gtk.Box {
private Geary.AccountInformation? account = null;
private Gtk.Label account_nickname_label;
private Gtk.Label email_address_label;
public signal void ok(Geary.AccountInformation? account);
public signal void cancel();
public AccountDialogRemoveConfirmPane() {
Object(orientation: Gtk.Orientation.VERTICAL, spacing: 6);
Gtk.Builder builder = GearyApplication.instance.create_builder("remove_confirm.glade");
pack_end((Gtk.Box) builder.get_object("container"));
Gtk.ActionGroup actions = (Gtk.ActionGroup) builder.get_object("actions");
account_nickname_label = (Gtk.Label) builder.get_object("account_nickname_label");
email_address_label = (Gtk.Label) builder.get_object("email_address_label");
// Hook up signals.
actions.get_action("cancel_action").activate.connect(() => { cancel(); });
actions.get_action("remove_action").activate.connect(() => { ok(account); });
}
public void set_account(Geary.AccountInformation a) {
account = a;
account_nickname_label.label = account.nickname;
email_address_label.label = account.email;
}
}

View file

@ -11,9 +11,11 @@ public class AccountDialog : Gtk.Dialog {
private AccountDialogAccountListPane account_list_pane = new AccountDialogAccountListPane();
private AccountDialogAddEditPane add_edit_pane = new AccountDialogAddEditPane();
private AccountSpinnerPage spinner_pane = new AccountSpinnerPage();
private AccountDialogRemoveConfirmPane remove_confirm_pane = new AccountDialogRemoveConfirmPane();
private int add_edit_page_number;
private int account_list_page_number;
private int spinner_page_number;
private int remove_confirm_page_number;
public AccountDialog() {
set_size_request(450, -1); // Sets min size.
@ -26,14 +28,18 @@ public class AccountDialog : Gtk.Dialog {
account_list_page_number = notebook.append_page(account_list_pane, null);
add_edit_page_number = notebook.append_page(add_edit_pane, null);
spinner_page_number = notebook.append_page(spinner_pane, null);
remove_confirm_page_number = notebook.append_page(remove_confirm_pane, null);
// Connect signals from pages.
account_list_pane.close.connect(on_close);
account_list_pane.add_account.connect(on_add_account);
account_list_pane.edit_account.connect(on_edit_account);
account_list_pane.delete_account.connect(on_delete_account);
add_edit_pane.ok.connect(on_save_add_or_edit);
add_edit_pane.cancel.connect(on_cancel_add_edit);
add_edit_pane.cancel.connect(on_cancel_back_to_list);
add_edit_pane.size_changed.connect(() => { resize(1, 1); });
remove_confirm_pane.ok.connect(on_delete_account_confirmed);
remove_confirm_pane.cancel.connect(on_cancel_back_to_list);
// Set default page.
notebook.set_current_page(account_list_page_number);
@ -55,28 +61,55 @@ public class AccountDialog : Gtk.Dialog {
notebook.set_current_page(add_edit_page_number);
}
private void on_edit_account(string email_address) {
// Grab the account info. While the addresses passed into this method should *always* be
// available in Geary, we double-check to be defensive.
Gee.Map<string, Geary.AccountInformation> accounts;
// Grab the account info. While the addresses passed into this method should *always* be
// available in Geary, we double-check to be defensive.
private Geary.AccountInformation? get_account_info_for_email(string email_address) {
Gee.Map<string, Geary.AccountInformation> accounts;
try {
accounts = Geary.Engine.instance.get_accounts();
} catch (Error e) {
debug("Error getting account info: %s", e.message);
return;
return null;
}
if (!accounts.has_key(email_address)) {
debug("Unable to get account info for: %s", email_address);
return;
return null;
}
return accounts.get(email_address);
}
private void on_edit_account(string email_address) {
Geary.AccountInformation? account = get_account_info_for_email(email_address);
if (account == null)
return;
add_edit_pane.set_mode(AddEditPage.PageMode.EDIT);
add_edit_pane.set_account_information(accounts.get(email_address));
add_edit_pane.set_account_information(account);
notebook.set_current_page(add_edit_page_number);
}
private void on_delete_account(string email_address) {
Geary.AccountInformation? account = get_account_info_for_email(email_address);
if (account == null)
return;
// Send user to confirmation screen.
remove_confirm_pane.set_account(account);
notebook.set_current_page(remove_confirm_page_number);
}
private void on_delete_account_confirmed(Geary.AccountInformation? account) {
assert(account != null); // Should not be able to happen since we checked earlier.
// Remove account, then set the page back to the account list.
GearyApplication.instance.remove_account_async.begin(account, null, () => {
notebook.set_current_page(account_list_page_number); });
}
private void on_save_add_or_edit(Geary.AccountInformation info) {
// Show the busy spinner.
notebook.set_current_page(spinner_page_number);
@ -94,7 +127,7 @@ public class AccountDialog : Gtk.Dialog {
notebook.set_current_page(add_edit_page_number);
}
private void on_cancel_add_edit() {
private void on_cancel_back_to_list() {
notebook.set_current_page(account_list_page_number);
}
}

View file

@ -308,6 +308,17 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
}
}
// Removes an existing account.
public async void remove_account_async(Geary.AccountInformation account,
Cancellable? cancellable = null) {
try {
yield GearyApplication.instance.get_account_instance(account).close_async(cancellable);
yield Geary.Engine.instance.remove_account_async(account, cancellable);
} catch (Error e) {
message("Error removing account: %s", e.message);
}
}
public File get_user_data_directory() {
return File.new_for_path(Environment.get_user_data_dir()).get_child("geary");
}

View file

@ -322,12 +322,16 @@ public class GearyController {
}
private void on_folder_selected(Geary.Folder? folder) {
debug("Folder %s selected", folder != null ? folder.to_string() : "(null)");
// If the folder is being unset, clear the message list and exit here.
if (folder == null) {
debug("no folder selected");
main_window.conversation_list_store.clear();
main_window.conversation_viewer.clear(null, null);
return;
}
debug("Folder %s selected", folder.to_string());
set_busy(true);
do_select_folder.begin(folder, on_select_folder_completed);
}
@ -364,13 +368,6 @@ public class GearyController {
main_window.main_toolbar.move_folder_menu.add_folder(f);
}
// The current folder may be null if the user rapidly switches between folders. If they have
// done that then this folder selection is invalid anyways, so just return.
if (current_folder == null) {
warning("Can not open folder: %s", folder.to_string());
return;
}
update_ui();
if (!inboxes.values.contains(current_folder)) {

View file

@ -222,12 +222,25 @@ public class FolderList : Sidebar.Tree {
assert(account_branch != null);
assert(has_branch(account_branch));
// If this is the current folder, unselect it.
Sidebar.Entry? entry = account_branch.folder_entries.get(folder.get_path());
if (entry != null && is_selected(entry))
folder_selected(null);
account_branch.remove_folder(folder);
}
public void remove_account(Geary.Account account) {
AccountBranch? account_branch = account_branches.get(account);
if (account_branch != null) {
// If a folder on this account is selected, unselect it.
foreach (FolderEntry entry in account_branch.folder_entries.values) {
if (is_selected(entry)) {
folder_selected(null);
break;
}
}
if (has_branch(account_branch))
prune(account_branch);
account_branches.unset(account);

View file

@ -44,6 +44,8 @@ public abstract class Geary.AbstractAccount : Object, Geary.Account {
public abstract async void close_async(Cancellable? cancellable = null) throws Error;
public abstract bool is_open();
public abstract Gee.Collection<Geary.Folder> list_matching_folders(
Geary.FolderPath? parent) throws Error;

View file

@ -375,4 +375,48 @@ public class Geary.AccountInformation : Object {
debug("Error writing to account info file: %s", err.message);
}
}
public async void clear_stored_passwords_async(
CredentialsMediator.ServiceFlag services) throws Error {
Error? return_error = null;
check_mediator_instance();
CredentialsMediator mediator = Geary.Engine.instance.authentication_mediator;
try {
if (services.has_imap()) {
yield mediator.clear_password_async(
CredentialsMediator.Service.IMAP, imap_credentials.user);
}
} catch (Error e) {
return_error = e;
}
try {
if (services.has_smtp()) {
yield mediator.clear_password_async(
CredentialsMediator.Service.SMTP, smtp_credentials.user);
}
} catch (Error e) {
return_error = e;
}
if (return_error != null)
throw return_error;
}
/**
* Deletes an account from disk. This is used by Geary.Engine and should not
* normally be invoked directly.
*/
internal async void remove_async(Cancellable? cancellable = null) {
try {
yield clear_stored_passwords_async(CredentialsMediator.ServiceFlag.IMAP
| CredentialsMediator.ServiceFlag.SMTP);
} catch (Error e) {
debug("Error clearing SMTP password: %s", e.message);
}
// Delete files.
yield Files.recursive_delete_async(settings_dir, cancellable);
}
}

View file

@ -80,6 +80,11 @@ public interface Geary.Account : Object {
*/
public abstract async void close_async(Cancellable? cancellable = null) throws Error;
/**
* Returns true if this account is open, else false.
*/
public abstract bool is_open();
/**
* Lists all the currently-available folders found under the parent path
* unless it's null, in which case it lists all the root folders. If the

View file

@ -14,6 +14,7 @@ public errordomain Geary.EngineError {
BAD_RESPONSE,
INCOMPLETE_MESSAGE,
SERVER_UNAVAILABLE,
ALREADY_CLOSED
ALREADY_CLOSED,
CLOSE_REQUIRED
}

View file

@ -293,13 +293,25 @@ public class Geary.Engine {
public async void remove_account_async(AccountInformation account,
Cancellable? cancellable = null) throws Error {
check_opened();
// Ensure account is closed.
if (account_instances.has_key(account.email) && account_instances.get(account.email).is_open()) {
throw new EngineError.CLOSE_REQUIRED("Account %s must be closed before removal",
account.email);
}
if (accounts.unset(account.email)) {
// Removal *MUST* be done in the following order:
// 1. Send the account-unavailable signal.
account_unavailable(account);
// TODO: delete the account from disk.
// 2. Delete the corresponding files.
yield account.remove_async(cancellable);
// 3. Send the account-removed signal.
account_removed(account);
// 4. Remove the account data from the engine.
account_instances.unset(account.email);
}
}

View file

@ -126,6 +126,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
properties_map.clear();
existing_folders.clear();
local_only.clear();
open = false;
if (local_err != null)
throw local_err;
@ -134,6 +135,10 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
throw remote_err;
}
public override bool is_open() {
return open;
}
// Subclasses should implement this to return their flavor of a GenericFolder with the
// appropriate interfaces attached. The returned folder should have its SpecialFolderType
// set using either the properties from the local folder or its path.

View file

@ -0,0 +1,57 @@
/* Copyright 2013 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.
*/
namespace Geary.Files {
// Number of files to delete in each step.
public const int RECURSIVE_DELETE_BATCH_SIZE = 50;
/**
* Recursively deletes a folder and its children.
* This method is designed to keep chugging along even if an error occurs.
* If this method is called with a file, it will simply be deleted.
*/
public async void recursive_delete_async(File folder, Cancellable? cancellable = null) {
// If this is a folder, recurse children.
if (folder.query_file_type(FileQueryInfoFlags.NONE) == FileType.DIRECTORY) {
FileEnumerator? enumerator = null;
try {
enumerator = yield folder.enumerate_children_async(FileAttribute.STANDARD_NAME,
FileQueryInfoFlags.NOFOLLOW_SYMLINKS, Priority.DEFAULT, cancellable);
} catch (Error e) {
debug("Error enumerating files for deletion: %s", e.message);
}
// Iterate the enumerated files in batches.
try {
while (true) {
List<FileInfo>? info_list = null;
info_list = yield enumerator.next_files_async(RECURSIVE_DELETE_BATCH_SIZE,
Priority.DEFAULT, cancellable);
if (info_list == null)
break; // Stop condition.
// Recursive step.
foreach (FileInfo info in info_list)
yield recursive_delete_async(folder.get_child(info.get_name()), cancellable);
}
} catch (Error e) {
debug("Error enumerating batch of files: %s", e.message);
}
}
// Children have been deleted, it's now safe to delete this file/folder.
try {
yield folder.delete_async(Priority.DEFAULT, cancellable);
} catch (Error e) {
debug("Error removing file: %s", e.message);
}
}
}

View file

@ -9,6 +9,7 @@ install(FILES login.glade DESTINATION ${UI_DEST})
install(FILES message.glade DESTINATION ${UI_DEST})
install(FILES password-dialog.glade DESTINATION ${UI_DEST})
install(FILES preferences.glade DESTINATION ${UI_DEST})
install(FILES remove_confirm.glade DESTINATION ${UI_DEST})
install(FILES toolbar.glade DESTINATION ${UI_DEST})
install(FILES toolbar_mark_menu.ui DESTINATION ${UI_DEST})
install(FILES toolbar_menu.ui DESTINATION ${UI_DEST})

View file

@ -17,6 +17,11 @@
<property name="icon_name">gtk-edit</property>
</object>
</child>
<child>
<object class="GtkAction" id="delete_account">
<property name="icon_name">list-remove-symbolic</property>
</object>
</child>
</object>
<object class="GtkBox" id="container">
<property name="visible">True</property>
@ -88,6 +93,19 @@
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="delete_button">
<property name="related_action">delete_account</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">toolbutton1</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>

199
ui/remove_confirm.glade Normal file
View file

@ -0,0 +1,199 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<!-- interface-requires gtk+ 3.0 -->
<object class="GtkActionGroup" id="actions">
<child>
<object class="GtkAction" id="cancel_action">
<property name="stock_id">gtk-cancel</property>
</object>
</child>
<child>
<object class="GtkAction" id="remove_action">
<property name="stock_id">gtk-remove</property>
</object>
</child>
</object>
<object class="GtkBox" id="container">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<child>
<object class="GtkImage" id="image1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="yalign">0</property>
<property name="stock">gtk-dialog-warning</property>
<property name="icon-size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">6</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_bottom">20</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">&lt;span weight="bold" size="larger"&gt;Are you sure you want to remove this account?&lt;/span&gt; </property>
<property name="use_markup">True</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">All email associated with this account will be removed from your computer. This will not affect email on the server.</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">6</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="grid1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="vexpand">True</property>
<property name="row_spacing">5</property>
<property name="column_spacing">5</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">Nickname:</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">Email address:</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="account_nickname_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="xpad">6</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</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="email_address_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="xpad">6</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</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>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">6</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="buttonbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">12</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="button1">
<property name="label" translatable="yes">_Cancel</property>
<property name="related_action">cancel_action</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button2">
<property name="label" translatable="yes">_Remove</property>
<property name="related_action">remove_action</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>