From 9aab0533827c32b356ae3c11e4ea2e25bb1273f2 Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Sun, 2 Dec 2018 13:51:15 +1100 Subject: [PATCH] Implement initial drag and drop for accounts, sender mailbox ordering Code courtesy @ebassi's tutorial: https://blog.gtk.org/2017/04/23/drag-and-drop-in-lists/ --- .../accounts/accounts-editor-edit-pane.vala | 72 ++++++++- .../accounts/accounts-editor-list-pane.vala | 147 +++++++++++++----- src/client/accounts/accounts-editor-row.vala | 75 +++++++++ ui/geary.css | 21 ++- 4 files changed, 263 insertions(+), 52 deletions(-) diff --git a/src/client/accounts/accounts-editor-edit-pane.vala b/src/client/accounts/accounts-editor-edit-pane.vala index 4e8c1c30..bb793d74 100644 --- a/src/client/accounts/accounts-editor-edit-pane.vala +++ b/src/client/accounts/accounts-editor-edit-pane.vala @@ -62,9 +62,9 @@ internal class Accounts.EditorEditPane : Gtk.Grid, EditorPane, AccountPane { this.details_list.add(new NicknameRow(account)); this.senders_list.set_header_func(Editor.seperator_headers); - foreach (Geary.RFC822.MailboxAddress sender - in account.get_sender_mailboxes()) { - this.senders_list.add(new MailboxRow(account, sender)); + foreach (Geary.RFC822.MailboxAddress sender in + account.sender_mailboxes) { + this.senders_list.add(new_mailbox_row(sender)); } this.senders_list.add(new AddMailboxRow()); @@ -173,6 +173,12 @@ internal class Accounts.EditorEditPane : Gtk.Grid, EditorPane, AccountPane { ); } + internal MailboxRow new_mailbox_row(Geary.RFC822.MailboxAddress sender) { + MailboxRow row = new MailboxRow(this.account, sender); + row.dropped.connect(on_sender_row_dropped); + return row; + } + private void on_account_changed() { update_header(); } @@ -181,6 +187,18 @@ internal class Accounts.EditorEditPane : Gtk.Grid, EditorPane, AccountPane { update_actions(); } + private void on_sender_row_dropped(EditorRow source, EditorRow target) { + this.commands.execute.begin( + new ReorderMailboxCommand( + (MailboxRow) source, + (MailboxRow) target, + this.account, + this.senders_list + ), + null + ); + } + [GtkCallback] private void on_setting_activated(Gtk.ListBoxRow row) { EditorRow? setting = row as EditorRow; @@ -309,8 +327,7 @@ private class Accounts.AddMailboxRow : AddRow { pane.commands.execute.begin( new AppendMailboxCommand( (Gtk.ListBox) get_parent(), - new MailboxRow( - pane.account, + pane.new_mailbox_row( new Geary.RFC822.MailboxAddress( popover.display_name, popover.address @@ -338,6 +355,7 @@ private class Accounts.MailboxRow : AccountRow { Geary.RFC822.MailboxAddress mailbox) { base(account, "", new Gtk.Label("")); this.mailbox = mailbox; + enable_drag(); update(); } @@ -584,6 +602,50 @@ internal class Accounts.UpdateMailboxCommand : Application.Command { } +internal class Accounts.ReorderMailboxCommand : Application.Command { + + + private MailboxRow source; + private int source_index; + private int target_index; + + private Geary.AccountInformation account; + private Gtk.ListBox list; + + + public ReorderMailboxCommand(MailboxRow source, + MailboxRow target, + Geary.AccountInformation account, + Gtk.ListBox list) { + this.source = source; + this.source_index = source.get_index(); + this.target_index = target.get_index(); + + this.account = account; + this.list = list; + } + + public async override void execute(GLib.Cancellable? cancellable) + throws GLib.Error { + move_source(this.target_index); + } + + public async override void undo(GLib.Cancellable? cancellable) + throws GLib.Error { + move_source(this.source_index); + } + + private void move_source(int destination) { + this.account.remove_sender(this.source.mailbox); + this.account.insert_sender(destination, this.source.mailbox); + + this.list.remove(this.source); + this.list.insert(this.source, destination); + } + +} + + internal class Accounts.RemoveMailboxCommand : Application.Command { diff --git a/src/client/accounts/accounts-editor-list-pane.vala b/src/client/accounts/accounts-editor-list-pane.vala index 3165bb83..bae6576a 100644 --- a/src/client/accounts/accounts-editor-list-pane.vala +++ b/src/client/accounts/accounts-editor-list-pane.vala @@ -158,7 +158,9 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane { private void add_account(Geary.AccountInformation account, Manager.Status status) { - this.accounts_list.add(new AccountListRow(account, status)); + AccountListRow row = new AccountListRow(account, status); + row.dropped.connect(on_editor_row_dropped); + this.accounts_list.add(row); } private void add_notification(InAppNotification notification) { @@ -216,6 +218,15 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane { } } + private void on_editor_row_dropped(EditorRow source, EditorRow target) { + this.commands.execute.begin( + new ReorderAccountCommand( + (AccountListRow) source, (AccountListRow) target, this.accounts + ), + null + ); + } + private void on_account_removed(Geary.AccountInformation account) { AccountListRow? row = get_account_row(account); if (row != null) { @@ -225,17 +236,21 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane { } private void on_execute(Application.Command command) { - InAppNotification ian = new InAppNotification(command.executed_label); - ian.set_button(_("Undo"), "win." + GearyController.ACTION_UNDO); - add_notification(ian); + if (command.executed_label != null) { + InAppNotification ian = new InAppNotification(command.executed_label); + ian.set_button(_("Undo"), "win." + GearyController.ACTION_UNDO); + add_notification(ian); + } update_actions(); } private void on_undo(Application.Command command) { - InAppNotification ian = new InAppNotification(command.undone_label); - ian.set_button(_("Redo"), "win." + GearyController.ACTION_REDO); - add_notification(ian); + if (command.undone_label != null) { + InAppNotification ian = new InAppNotification(command.undone_label); + ian.set_button(_("Redo"), "win." + GearyController.ACTION_REDO); + add_notification(ian); + } update_actions(); } @@ -267,34 +282,26 @@ internal class Accounts.EditorListPane : Gtk.Grid, EditorPane { } -private class Accounts.AccountListRow : EditorRow { +private class Accounts.AccountListRow : AccountRow { - internal Geary.AccountInformation account; - + private Gtk.Label service_label = new Gtk.Label(""); private Gtk.Image unavailable_icon = new Gtk.Image.from_icon_name( "dialog-warning-symbolic", Gtk.IconSize.BUTTON ); - private Gtk.Label account_name = new Gtk.Label(""); - private Gtk.Label account_details = new Gtk.Label(""); - public AccountListRow(Geary.AccountInformation account, Manager.Status status) { - this.account = account; + base(account, "", new Gtk.Grid()); + enable_drag(); - this.account_name.show(); - this.account_name.set_hexpand(true); - this.account_name.halign = Gtk.Align.START; + this.value.add(this.unavailable_icon); + this.value.add(this.service_label); - this.account_details.show(); - - this.layout.add(this.unavailable_icon); - this.layout.add(this.account_name); - this.layout.add(this.account_details); + this.service_label.show(); this.account.information_changed.connect(on_account_changed); - update_nickname(); + update(); update_status(status); } @@ -327,24 +334,12 @@ private class Accounts.AccountListRow : EditorRow { } } - public void update_nickname() { + public override void update() { string name = this.account.nickname; if (Geary.String.is_empty(name)) { name = account.primary_mailbox.to_address_display("", ""); } - this.account_name.set_text(name); - } - - public void update_status(Manager.Status status) { - if (status != Manager.Status.UNAVAILABLE) { - this.unavailable_icon.hide(); - this.set_tooltip_text(""); - } else { - this.unavailable_icon.show(); - this.set_tooltip_text( - _("This account has encountered a problem and is unavailable") - ); - } + this.label.set_text(name); string? details = this.account.service_label; switch (account.service_provider) { @@ -360,27 +355,43 @@ private class Accounts.AccountListRow : EditorRow { details = _("Yahoo"); break; } - this.account_details.set_text(details); + this.service_label.set_text(details); + } + + public void update_status(Manager.Status status) { + if (status != Manager.Status.UNAVAILABLE) { + this.unavailable_icon.hide(); + this.set_tooltip_text(""); + } else { + this.unavailable_icon.show(); + this.set_tooltip_text( + _("This account has encountered a problem and is unavailable") + ); + } if (status == Manager.Status.ENABLED) { - this.account_name.get_style_context().remove_class( + this.label.get_style_context().remove_class( Gtk.STYLE_CLASS_DIM_LABEL ); - this.account_details.get_style_context().remove_class( + this.service_label.get_style_context().remove_class( Gtk.STYLE_CLASS_DIM_LABEL ); } else { - this.account_name.get_style_context().add_class( + this.label.get_style_context().add_class( Gtk.STYLE_CLASS_DIM_LABEL ); - this.account_details.get_style_context().add_class( + this.service_label.get_style_context().add_class( Gtk.STYLE_CLASS_DIM_LABEL ); } } private void on_account_changed() { - update_nickname(); + update(); + Gtk.ListBox? parent = get_parent() as Gtk.ListBox; + if (parent != null) { + parent.invalidate_sort(); + } } } @@ -451,6 +462,56 @@ private class Accounts.AddServiceProviderRow : EditorRow { } +internal class Accounts.ReorderAccountCommand : Application.Command { + + + private AccountListRow source; + private int source_index; + private int target_index; + + private Manager manager; + + + public ReorderAccountCommand(AccountListRow source, + AccountListRow target, + Manager manager) { + this.source = source; + this.source_index = source.get_index(); + this.target_index = target.get_index(); + + this.manager = manager; + } + + public async override void execute(GLib.Cancellable? cancellable) + throws GLib.Error { + move_source(this.target_index); + } + + public async override void undo(GLib.Cancellable? cancellable) + throws GLib.Error { + move_source(this.source_index); + } + + private void move_source(int destination) { + Gee.List accounts = + this.manager.iterable().to_linked_list(); + accounts.sort(Geary.AccountInformation.compare_ascending); + accounts.remove(this.source.account); + accounts.insert(destination, this.source.account); + + int ord = 0; + foreach (Geary.AccountInformation account in accounts) { + if (account.ordinal != ord) { + account.ordinal = ord; + account.information_changed(); + } + ord++; + } + } + +} + + internal class Accounts.RemoveAccountCommand : Application.Command { diff --git a/src/client/accounts/accounts-editor-row.vala b/src/client/accounts/accounts-editor-row.vala index 44336775..681faa48 100644 --- a/src/client/accounts/accounts-editor-row.vala +++ b/src/client/accounts/accounts-editor-row.vala @@ -8,9 +8,19 @@ internal class Accounts.EditorRow : Gtk.ListBoxRow { + private const string DND_ATOM = "geary-editor-row"; + private const Gtk.TargetEntry[] DRAG_ENTRIES = { + { DND_ATOM, Gtk.TargetFlags.SAME_APP, 0 } + }; + protected Gtk.Grid layout { get; private set; default = new Gtk.Grid(); } + private Gtk.Container drag_handle; + + + public signal void dropped(EditorRow target); + public EditorRow() { get_style_context().add_class("geary-settings"); @@ -19,6 +29,23 @@ internal class Accounts.EditorRow : Gtk.ListBoxRow { this.layout.show(); add(this.layout); + // We'd like to add the drag handle only when needed, but + // GNOME/gtk#1495 prevents us from doing so. + Gtk.EventBox drag_box = new Gtk.EventBox(); + drag_box.add( + new Gtk.Image.from_icon_name( + "open-menu-symbolic", Gtk.IconSize.BUTTON + ) + ); + this.drag_handle = new Gtk.Grid(); + this.drag_handle.valign = Gtk.Align.CENTER; + this.drag_handle.add(drag_box); + this.drag_handle.show_all(); + this.drag_handle.hide(); + // Translators: Tooltip for dragging list items + this.drag_handle.set_tooltip_text(_("Drag to move this item")); + this.layout.add(drag_handle); + this.show(); } @@ -26,6 +53,54 @@ internal class Accounts.EditorRow : Gtk.ListBoxRow { // No-op by default } + /** Adds a drag handle to the row and enables drag signals. */ + protected void enable_drag() { + Gtk.drag_source_set( + this.drag_handle, + Gdk.ModifierType.BUTTON1_MASK, + DRAG_ENTRIES, + Gdk.DragAction.MOVE + ); + + Gtk.drag_dest_set( + this, + Gtk.DestDefaults.ALL, + DRAG_ENTRIES, + Gdk.DragAction.MOVE + ); + + this.drag_handle.drag_data_get.connect(on_drag_data_get); + this.drag_data_received.connect(on_drag_data_received); + this.drag_handle.get_style_context().add_class("geary-drag-handle"); + this.drag_handle.show(); + + get_style_context().add_class("geary-draggable"); + } + + + private void on_drag_data_get(Gdk.DragContext context, + Gtk.SelectionData selection_data, + uint info, uint time_) { + selection_data.set( + Gdk.Atom.intern_static_string(DND_ATOM), 8, + get_index().to_string().data + ); + } + + private void on_drag_data_received(Gdk.DragContext context, + int x, int y, + Gtk.SelectionData selection_data, + uint info, uint time_) { + int drag_index = int.parse((string) selection_data.get_data()); + Gtk.ListBox? parent = this.get_parent() as Gtk.ListBox; + if (parent != null) { + EditorRow? drag_row = parent.get_row_at_index(drag_index) as EditorRow; + if (drag_row != null && drag_row != this) { + drag_row.dropped(this); + } + } + } + } diff --git a/ui/geary.css b/ui/geary.css index a9530a26..3cbc9afc 100644 --- a/ui/geary.css +++ b/ui/geary.css @@ -208,6 +208,10 @@ row.geary-settings { padding: 0px; } +row.geary-settings image { + padding: 0px 6px; +} + row.geary-settings > grid > * { margin: 18px 6px; } @@ -222,11 +226,20 @@ row.geary-settings > grid > *:first-child:dir(rtl) { margin-right: 18px; } -row.geary-settings > grid > image:dir(ltr) { - margin-right: 6px; +/* dir pseudo-class used here for required additional specificity */ +row.geary-settings > grid > grid.geary-drag-handle:dir(ltr), +row.geary-settings > grid > grid.geary-drag-handle:dir(rtl) { + margin: 0; } -row.geary-settings > grid > image:dir(rtl) { - margin-left: 6px; + +row.geary-settings > grid > grid.geary-drag-handle image:dir(ltr) { + padding: 12px; + padding-right: 6px; +} + +row.geary-settings > grid > grid.geary-drag-handle image:dir(rtl) { + padding: 12px; + padding-left: 6px; } frame.geary-settings.geary-signature {