[gnome-contacts/nielsdg/gtk4: 10/10] WIP




commit 57d0fabebeb201449dd393299bb72715c1983033
Author: Niels De Graef <nielsdegraef gmail com>
Date:   Wed Jan 5 02:07:23 2022 +0100

    WIP

 data/ui/contacts-main-window.ui    |   8 +-
 data/ui/style.css                  |  19 ++--
 src/contacts-avatar.vala           |   7 ++
 src/contacts-contact-pane.vala     |  12 +--
 src/contacts-contact-sheet.vala    |  21 +----
 src/contacts-delete-operation.vala |  55 +++++++++++
 src/contacts-editor-property.vala  |  26 ++++--
 src/contacts-link-operation.vala   |  79 ++++++++++++++++
 src/contacts-linking.vala          | 103 ---------------------
 src/contacts-main-window.vala      | 183 +++++++++++++++++--------------------
 src/contacts-operation.vala        |  63 +++++++++++++
 src/contacts-unlink-operation.vala |  56 ++++++++++++
 src/contacts-utils.vala            |   7 +-
 src/meson.build                    |   5 +-
 todo-gtk4.md                       |   7 ++
 15 files changed, 391 insertions(+), 260 deletions(-)
---
diff --git a/data/ui/contacts-main-window.ui b/data/ui/contacts-main-window.ui
index 7ef0e5f0..a1bb42ee 100644
--- a/data/ui/contacts-main-window.ui
+++ b/data/ui/contacts-main-window.ui
@@ -68,7 +68,7 @@
           <object class="AdwLeaflet" id="header">
             <property name="visible-child-name" bind-source="content_box" bind-property="visible-child-name" 
bind-flags="bidirectional|sync-create"/>
             <property name="mode-transition-duration" bind-source="content_box" 
bind-property="mode-transition-duration" bind-flags="bidirectional|sync-create"/>
-            <property name="child-transition-duration" bind-source="content_box" 
bind-property="child-transition-duration" bind-flags="bidirectional|sync-create"/>
+            <property name="child-transition-params" bind-source="content_box" 
bind-property="child-transition-params" bind-flags="bidirectional|sync-create"/>
             <property name="transition-type" bind-source="content_box" bind-property="transition-type" 
bind-flags="bidirectional|sync-create"/>
             <child>
               <object class="AdwLeafletPage">
@@ -148,7 +148,6 @@
                     <child>
                       <object class="GtkRevealer" id="back_revealer">
                         <property name="transition-type">slide-right</property>
-                        <property name="transition-duration" bind-source="content_box" 
bind-property="mode-transition-duration" bind-flags="bidirectional|sync-create"/>
                         <child>
                           <object class="GtkButton" id="back">
                             <property name="valign">center</property>
@@ -214,10 +213,11 @@
                     <child type="end">
                       <object class="GtkButton" id="done_button">
                         <property name="visible">False</property>
+                        <property name="use_underline">True</property>
                         <property name="label" translatable="yes">Done</property>
                         <property name="valign">center</property>
                         <style>
-                          <class name="text-button"/>
+                          <class name="suggested-action"/>
                         </style>
                       </object>
                     </child>
@@ -228,7 +228,7 @@
           </object>
         </child>
         <child>!
-          <object class="GtkOverlay" id="notification_overlay">
+          <object class="AdwToastOverlay" id="toast_overlay">
             <child>
               <object class="AdwLeaflet" id="content_box">
                 <property name="can-navigate-back">True</property>
diff --git a/data/ui/style.css b/data/ui/style.css
index 1027be8e..fc8b9f0a 100644
--- a/data/ui/style.css
+++ b/data/ui/style.css
@@ -52,27 +52,20 @@ flowboxchild.circular {
   padding: 0 0;
 }
 
-.contacts-flatten:not(:hover) {
-  background-color: transparent;
-  background-image: none;
-  border-color: transparent;
-  box-shadow: inset 0 1px rgba(255, 255, 255, 0), 0 1px rgba(255, 255, 255, 0);
-  text-shadow: none; -gtk-icon-shadow: none;
-  border: 1px solid rgba(205, 199, 194, 0.5);
-}
-
 /* Contact Editor-related CSS classes */
 
-/* Common class all widgets editing a property  */
+/* Common class for all widgets editing a property  */
 .contacts-editor-property {
+  margin: 6px 0;
 }
 
-  .contacts-editor-property .contacts-property-icon {
-    margin: 9px;
+  .contacts-editor-property .contacts-property-icon,
+  .contacts-editor-property .contacts-editor-main-entry image {
+    margin: 10px;
   }
 
   .contacts-editor-property .contacts-editor-main-entry {
-    padding: 12px 6px;
+    padding: 6px 6px 6px 0px;
   }
 
 /* Class for editing postal address */
diff --git a/src/contacts-avatar.vala b/src/contacts-avatar.vala
index 70cd43e0..791179d4 100644
--- a/src/contacts-avatar.vala
+++ b/src/contacts-avatar.vala
@@ -42,6 +42,13 @@ public class Contacts.Avatar : Adw.Bin {
     }
 
     this.child = new Adw.Avatar (size, name, show_initials);
+
+    if (individual != null && individual.avatar != null) {
+      this.map.connect (() => {
+        var stream = this.individual.avatar.load (size, null);
+        this.set_pixbuf (new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true));
+      });
+    }
   }
 
   /**
diff --git a/src/contacts-contact-pane.vala b/src/contacts-contact-pane.vala
index a031c2e7..5329d36e 100644
--- a/src/contacts-contact-pane.vala
+++ b/src/contacts-contact-pane.vala
@@ -36,19 +36,11 @@ public class Contacts.ContactPane : Adw.Bin {
 
   [GtkChild]
   private unowned Gtk.Stack stack;
-  [GtkChild]
-  private unowned Gtk.StackPage none_selected_page;
-
-  [GtkChild]
-  private unowned Gtk.ScrolledWindow contact_sheet_view;
 
   [GtkChild]
   private unowned Adw.Clamp contact_sheet_clamp;
   private unowned ContactSheet? sheet = null;
 
-  [GtkChild]
-  private unowned Gtk.ScrolledWindow contact_editor_view;
-
   [GtkChild]
   private unowned Gtk.Box contact_editor_box;
   private unowned ContactEditor? editor = null;
@@ -79,11 +71,11 @@ public class Contacts.ContactPane : Adw.Bin {
 
     this.suggestion_grid.suggestion_accepted.connect ( () => {
         var linked_contact = this.individual.display_name;
-        var operation = new LinkOperation (this.store);
         var to_link = new Gee.LinkedList<Individual> ();
         to_link.add (this.individual);
         to_link.add (i);
-        operation.execute.begin (to_link);
+        var operation = new LinkOperation (this.store, to_link);
+        operation.execute.begin ();
         this.contacts_linked (null, linked_contact, operation);
         remove_suggestion_grid ();
       });
diff --git a/src/contacts-contact-sheet.vala b/src/contacts-contact-sheet.vala
index 0fd94c3e..dc0a9830 100644
--- a/src/contacts-contact-sheet.vala
+++ b/src/contacts-contact-sheet.vala
@@ -17,7 +17,7 @@
 
 using Folks;
 
-// XXX accesibility? tooltips?
+// XXX accesibility?
 public class Contacts.ContactSheetRow : Adw.ActionRow {
 
   public ContactSheetRow (string property_name, string title, string? subtitle = null) {
@@ -308,21 +308,10 @@ public class Contacts.ContactSheet : Gtk.Grid {
         if (window == null)
           return;
 
-        try {
-          Gtk.show_uri (window,
-                        fallback_to_https (url.value),
-                        Gdk.CURRENT_TIME);
-        } catch (Error e) {
-          var message = "Failed to open url '%s'".printf(url.value);
-
-          // Notify the user
-          var notification = new InAppNotification (message);
-          notification.show ();
-          window.add_notification (notification);
-
-          // Print details on stdout
-          debug (message + ": " + e.message);
-        }
+        // FIXME: use show_uri_full so we can show errors
+        Gtk.show_uri (window,
+                      fallback_to_https (url.value),
+                      Gdk.CURRENT_TIME);
       });
 
       this.attach_row (row);
diff --git a/src/contacts-delete-operation.vala b/src/contacts-delete-operation.vala
new file mode 100644
index 00000000..5c379dc1
--- /dev/null
+++ b/src/contacts-delete-operation.vala
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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/>.
+ */
+
+using Folks;
+
+public class Contacts.DeleteOperation : Object, Operation {
+
+  private Gee.List<Individual> individuals;
+
+  // We don't support reversing a removal. What we do instead, is put a timeout
+  // before actually executing this operation so the user has time to change
+  // their mind.
+  public bool reversable { get { return false; } }
+
+  private string _description;
+  public string description { owned get { return this._description; } }
+
+  public DeleteOperation (Gee.List<Individual> individuals) {
+    this.individuals = individuals;
+    this._description = ngettext ("Deleting %d contact",
+                                  "Deleting %d contacts", individuals.size)
+                        .printf (individuals.size);
+  }
+
+  /**
+   * Link individuals
+   */
+  public async void execute () throws GLib.Error {
+    foreach (var indiv in this.individuals) {
+      foreach (var persona in indiv.personas) {
+        // TODO: make sure it is actually removed
+        yield persona.store.remove_persona (persona);
+      }
+    }
+  }
+
+  // See comments near the reversable property
+  protected async void _undo () throws GLib.Error {
+    throw new GLib.IOError.NOT_SUPPORTED("Undoing not supported");
+  }
+}
diff --git a/src/contacts-editor-property.vala b/src/contacts-editor-property.vala
index f486358d..88a8ded9 100644
--- a/src/contacts-editor-property.vala
+++ b/src/contacts-editor-property.vala
@@ -212,7 +212,7 @@ public class Contacts.EditorPropertyRow : Adw.Bin {
     this.listbox = list_box;
     this.listbox.selection_mode = Gtk.SelectionMode.NONE;
     this.listbox.activate_on_single_click = true;
-    this.listbox.add_css_class ("content");
+    this.listbox.add_css_class ("boxed-list");
     this.listbox.add_css_class ("contacts-editor-property");
     this.revealer.set_child (listbox);
   }
@@ -250,7 +250,7 @@ public class Contacts.EditorPropertyRow : Adw.Bin {
   /**
    * Setter for the main widget, which can be used to actually edit the property
    */
-  public void set_main_widget (Gtk.Widget widget) {
+  public void set_main_widget (Gtk.Widget widget, bool add_icon = true) {
     var row = new Gtk.ListBoxRow ();
     row.focusable = false;
 
@@ -259,12 +259,14 @@ public class Contacts.EditorPropertyRow : Adw.Bin {
     row.set_child (box);
 
     // Start with the icon (if known)
-    unowned var icon_name = Utils.get_icon_name_for_property (this.ptype);
-    if (icon_name != null) {
-      var icon = new Gtk.Image.from_icon_name (icon_name);
-      icon.add_css_class ("contacts-property-icon");
-      icon.tooltip_text = Utils.get_display_name_for_property (this.ptype);
-      box.prepend (icon);
+    if (add_icon) {
+      unowned var icon_name = Utils.get_icon_name_for_property (this.ptype);
+      if (icon_name != null) {
+        var icon = new Gtk.Image.from_icon_name (icon_name);
+        icon.add_css_class ("contacts-property-icon");
+        icon.tooltip_text = Utils.get_display_name_for_property (this.ptype);
+        box.prepend (icon);
+      }
     }
 
     // Set the actual widget
@@ -299,7 +301,13 @@ public class Contacts.EditorPropertyRow : Adw.Bin {
     entry.placeholder_text = placeholder;
     entry.add_css_class ("flat");
     entry.add_css_class ("contacts-editor-main-entry");
-    this.set_main_widget (entry);
+    // Set the icon as part of the GtkEntry, to avoid it being outside of the margin
+    unowned var icon_name = Utils.get_icon_name_for_property (this.ptype);
+    if (icon_name != null) {
+      entry.primary_icon_name = icon_name;
+      entry.primary_icon_tooltip_text = Utils.get_display_name_for_property (this.ptype);
+    }
+    this.set_main_widget (entry, false);
 
     this.is_empty = (text == "");
     entry.changed.connect (() => {
diff --git a/src/contacts-link-operation.vala b/src/contacts-link-operation.vala
new file mode 100644
index 00000000..9e75841d
--- /dev/null
+++ b/src/contacts-link-operation.vala
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2011 Alexander Larsson <alexl redhat com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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/>.
+ */
+
+using Folks;
+
+public class Contacts.LinkOperation : Object, Operation {
+
+  private weak Store store;
+
+  private Gee.LinkedList<Individual> individuals;
+  private Gee.HashSet<Gee.HashSet<Persona>> personas_to_link
+      = new Gee.HashSet<Gee.HashSet<Persona>> ();
+
+  private bool finished { get; set; default = false; }
+
+  private bool _reversable = false;
+  public bool reversable { get { return this._reversable; } }
+
+  private string _description;
+  public string description { owned get { return this._description; } }
+
+  public LinkOperation (Store store, Gee.LinkedList<Individual> individuals) {
+    this.store = store;
+    this.individuals = individuals;
+
+    this._description = ngettext ("Linked %d contact",
+                                  "Linked %d contacts", individuals.size)
+                        .printf (individuals.size);
+  }
+
+  /**
+   * Link individuals
+   */
+  public async void execute () throws GLib.Error {
+    var personas_to_link = new Gee.HashSet<Persona> ();
+    foreach (var i in individuals) {
+      var saved_personas = new Gee.HashSet<Persona> ();
+      foreach (var persona in i.personas) {
+        personas_to_link.add (persona);
+        saved_personas.add (persona);
+      }
+      this.personas_to_link.add (saved_personas);
+    }
+
+    yield this.store.aggregator.link_personas (personas_to_link);
+    this._reversable = true;
+    notify_property ("reversable");
+  }
+
+  /**
+   * Undoing means unlinking
+   */
+  public async void _undo () throws GLib.Error {
+    var individual = this.personas_to_link.first_match(() => {return true;})
+      .first_match(() => {return true;}).individual;
+
+    yield store.aggregator.unlink_individual (individual);
+
+    foreach (var personas in personas_to_link) {
+      yield this.store.aggregator.link_personas (personas);
+    }
+    this._reversable = false;
+    notify_property ("reversable");
+  }
+}
diff --git a/src/contacts-main-window.vala b/src/contacts-main-window.vala
index 62152a90..81e4a551 100644
--- a/src/contacts-main-window.vala
+++ b/src/contacts-main-window.vala
@@ -27,6 +27,7 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
     { "unlink-contact", unlink_contact },
     { "delete-contact", delete_contact },
     { "sort-on", null, "s", "'surname'", sort_on_changed },
+    { "undo-operation", undo_operation_action },
   };
 
   [GtkChild]
@@ -42,11 +43,9 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
   [GtkChild]
   private unowned Adw.HeaderBar left_header;
   [GtkChild]
-  private unowned Gtk.Separator header_separator;
-  [GtkChild]
   private unowned Adw.HeaderBar right_header;
   [GtkChild]
-  private unowned Gtk.Overlay notification_overlay;
+  private unowned Adw.ToastOverlay toast_overlay;
   [GtkChild]
   private unowned Gtk.Button select_cancel_button;
   [GtkChild]
@@ -84,6 +83,9 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
     get; construct set;
   }
 
+  // If an unduable operation was recently performed, this will be set
+  public Operation? last_operation = null;
+
   construct {
     this.actions.add_action_entries (ACTION_ENTRIES, this);
     this.insert_action_group ("window", this.actions);
@@ -189,7 +191,6 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
         = this.state.editing ();
     this.right_header.show_end_title_buttons = !this.state.editing ();
     if (this.state.editing ()) {
-      this.done_button.use_underline = true;
       this.done_button.label = (this.state == UiState.CREATING)? _("_Add") : _("Done");
       // Cast is required because Gtk.Button.set_focus_on_click is deprecated and
       // we have to use Gtk.Widget.set_focus_on_click instead
@@ -238,27 +239,27 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
   }
 
   private void unlink_contact () {
-    var individual = this.contact_pane.individual;
+    unowned var individual = this.contact_pane.individual;
     if (individual == null)
       return;
 
     set_shown_contact (null);
     this.state = UiState.NORMAL;
 
-    var operation = new UnLinkOperation (this.store);
-    operation.execute.begin (individual);
-
-    var b = new Gtk.Button.with_mnemonic (_("_Undo"));
-    var notification = new InAppNotification (_("Contacts unlinked"), b);
+    this.last_operation = new UnlinkOperation (this.store, individual);
+    this.last_operation.execute.begin ((obj, res) => {
+      try {
+        this.last_operation.execute.end (res);
+      } catch (GLib.Error e) {
+        warning ("Error unlinking individuals: %s", e.message);
+      }
+    });
 
-    /* signal handlers */
-    b.clicked.connect ( () => {
-        /* here, we will link the thing in question */
-        operation.undo.begin ();
-        notification.dismiss ();
-      });
+    var toast = new Adw.Toast (this.last_operation.description);
+    toast.set_button_label (_("_Undo"));
+    toast.action_name = "win.undo-operation";
 
-    add_notification (notification);
+    this.toast_overlay.add_toast (toast);
   }
 
   private void delete_contact () {
@@ -276,6 +277,23 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
     action.set_state (new_state);
   }
 
+  private void undo_operation_action (SimpleAction action, GLib.Variant? parameter) {
+    if (this.last_operation == null) {
+      warning ("Undo action was called without anything that can be undone?");
+      return;
+    }
+
+    debug ("Undoing operation '%s'", this.last_operation.description);
+    this.last_operation.undo.begin ((obj, res) => {
+      try {
+        this.last_operation.undo.end (res);
+      } catch (GLib.Error e) {
+        warning ("Couldn't undo operation '%s': %s", this.last_operation.description, e.message);
+      }
+      debug ("Finished undoing operation '%s'", this.last_operation.description);
+    });
+  }
+
   private void stop_editing (bool cancel = false) {
     if (this.state == UiState.CREATING) {
       if (cancel) {
@@ -292,11 +310,6 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
     update_header_titles (null, "");
   }
 
-  public void add_notification (InAppNotification notification) {
-    this.notification_overlay.add_overlay (notification);
-    notification.reveal ();
-  }
-
   public void set_shown_contact (Individual? i) {
     /* FIXME: ask the user to leave edit-mode and act accordingly */
     if (this.contact_pane.on_edit_mode)
@@ -448,7 +461,7 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
     return Gdk.EVENT_PROPAGATE;
   }
 
-  void list_pane_selection_changed_cb (Individual? new_selection) {
+  private void list_pane_selection_changed_cb (Individual? new_selection) {
     set_shown_contact (new_selection);
     if (this.state != UiState.SELECTING)
       this.state = UiState.SHOWING;
@@ -457,95 +470,67 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
       show_contact_pane ();
   }
 
-  void list_pane_link_contacts_cb (Gee.LinkedList<Individual> contact_list) {
+  private void list_pane_link_contacts_cb (Gee.LinkedList<Individual> contact_list) {
     set_shown_contact (null);
     this.state = UiState.NORMAL;
 
-    var operation = new LinkOperation (this.store);
-    operation.execute.begin (contact_list);
-
-    string msg = ngettext ("%d contacts linked",
-                           "%d contacts linked",
-                           contact_list.size).printf (contact_list.size);
-
-    var b = new Gtk.Button.with_mnemonic (_("_Undo"));
-    var notification = new InAppNotification (msg, b);
-
-    /* signal handlers */
-    b.clicked.connect ( () => {
-        /* here, we will unlink the thing in question */
-        operation.undo.begin ();
-        notification.dismiss ();
-      });
+    this.last_operation = new LinkOperation (this.store, contact_list);
+    this.last_operation.execute.begin ((obj, res) => {
+      try {
+        this.last_operation.execute.end (res);
+      } catch (GLib.Error e) {
+        warning ("Error linking individuals: %s", e.message);
+      }
+    });
 
-    add_notification (notification);
+    var toast = new Adw.Toast (this.last_operation.description);
+    toast.set_button_label (_("_Undo"));
+    toast.action_name = "win.undo-operation";
+    this.toast_overlay.add_toast (toast);
   }
 
   private void delete_contacts (Gee.List<Individual> individuals) {
     set_shown_contact (null);
     this.state = UiState.NORMAL;
 
-    string msg;
-    if (individuals.size == 1)
-      msg = _("Deleted contact %s").printf (individuals[0].display_name);
-    else
-      msg = ngettext ("%d contact deleted", "%d contacts deleted", individuals.size)
-              .printf (individuals.size);
+    this.last_operation = new DeleteOperation (individuals);
+    var toast = new Adw.Toast (this.last_operation.description);
+    toast.set_button_label (_("_Undo"));
+    toast.action_name = "win.undo-operation";
 
-    var b = new Gtk.Button.with_mnemonic (_("_Undo"));
+    // signal handlers
+    bool really_delete = true;
+      // XXX
+    // b.clicked.connect (() => {
+        // really_delete = false;
+        // toast.dismiss ();
+    // });
+    toast.dismissed.connect (() => {
+        if (really_delete) {
+          this.last_operation.execute.begin ((obj, res) => {
+              try {
+                this.last_operation.execute.end (res);
+              } catch (Error e) {
+                debug ("Coudln't remove persona: %s", e.message);
+              }
+          });
+        } else {
+          /* Reset the contact list */
+          this.list_pane.undo_deletion ();
 
-    var notification = new InAppNotification (msg, b);
+          set_shown_contact (individuals[0]);
+          this.state = UiState.SHOWING;
+        }
+    });
 
-    // Don't wrap (default), but ellipsize
-    notification.message_label.wrap = false;
-    notification.message_label.max_width_chars = 45;
-    notification.message_label.ellipsize = Pango.EllipsizeMode.END;
+    this.toast_overlay.add_toast (toast);
+  }
 
-    // signal handlers
-    bool really_delete = true;
-    b.clicked.connect ( () => {
-        really_delete = false;
-        notification.dismiss ();
-
-        /* Reset the contact list */
-        list_pane.undo_deletion ();
-
-        set_shown_contact (individuals[0]);
-        this.state = UiState.SHOWING;
-      });
-    notification.dismissed.connect ( () => {
-        if (really_delete)
-          foreach (var i in individuals)
-            foreach (var p in i.personas) {
-              // TODO: make sure it is acctally removed
-              p.store.remove_persona.begin (p, (obj, res) => {
-                try {
-                  p.store.remove_persona.end (res);
-                } catch (Error e) {
-                  debug ("Coudln't remove persona: %s", e.message);
-                }
-              });
-            }
-      });
-
-    add_notification (notification);
-  }
-
-  void contact_pane_contacts_linked_cb (string? main_contact, string linked_contact, LinkOperation 
operation) {
-    string msg;
-    if (main_contact != null)
-      msg = _("%s linked to %s").printf (main_contact, linked_contact);
-    else
-      msg = _("%s linked to the contact").printf (linked_contact);
-
-    var b = new Gtk.Button.with_mnemonic (_("_Undo"));
-    var notification = new InAppNotification (msg, b);
-
-    b.clicked.connect ( () => {
-        notification.dismiss ();
-        operation.undo.begin ();
-      });
-
-    add_notification (notification);
+  private void contact_pane_contacts_linked_cb (string? main_contact, string linked_contact, LinkOperation 
operation) {
+    this.last_operation = operation;
+    var toast = new Adw.Toast (this.last_operation.description);
+    toast.set_button_label (_("_Undo"));
+    toast.action_name = "win.undo-operation";
+    this.toast_overlay.add_toast (toast);
   }
 }
diff --git a/src/contacts-operation.vala b/src/contacts-operation.vala
new file mode 100644
index 00000000..f77944b4
--- /dev/null
+++ b/src/contacts-operation.vala
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef redhat com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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/>.
+ */
+
+/**
+ * Contacts.Operation is a simple interface to describe actions that can be
+ * executed and possibly undone later on (for example, using a button on an
+ * in-app notification).
+ *
+ * Since some operations might not be able undoable later onwards, there is a
+ * property `reversable` that you should check first before calling undo().
+ */
+public interface Contacts.Operation : Object {
+
+  /**
+   * Whether undo() can be called on this object
+   */
+  public abstract bool reversable { get; }
+
+  /**
+   * A user-facing string that tells us what the operation does
+   */
+  public abstract string description { owned get; }
+
+  /**
+   * This the actual implementation of the operation that a subclass needs to
+   * implement.
+   */
+  public abstract async void execute () throws GLib.Error;
+
+  /**
+   * The is the public API undo. If you want, you can override it still, e.g.
+   * to provide better warnings.
+   */
+  public virtual async void undo () throws GLib.Error {
+    // FIXME: should throw an error instead so we can show something to the user
+    if (!this.reversable) {
+      warning ("Can't undo '%s'", this.description);
+      return;
+    }
+
+    yield this._undo ();
+  }
+
+  /**
+   * This the actual implementation of the undo that a subclass needs to
+   * implement.
+   */
+  protected abstract async void _undo () throws GLib.Error;
+}
diff --git a/src/contacts-unlink-operation.vala b/src/contacts-unlink-operation.vala
new file mode 100644
index 00000000..7b67679b
--- /dev/null
+++ b/src/contacts-unlink-operation.vala
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 Alexander Larsson <alexl redhat com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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/>.
+ */
+
+using Folks;
+
+public class Contacts.UnlinkOperation : Object, Operation {
+
+  private weak Store store;
+
+  private Individual individual;
+
+  private Gee.HashSet<Persona> personas = new Gee.HashSet<Persona> ();
+
+  private bool _reversable = false;
+  public bool reversable { get { return this._reversable; } }
+
+  private string _description;
+  public string description { owned get { return this._description; } }
+
+  public UnlinkOperation (Store store, Individual main) {
+    this.store = store;
+    this.individual = main;
+    this._description = _("Unlinking contacts");
+  }
+
+  /* Remove a personas from individual */
+  public async void execute () throws GLib.Error {
+    foreach (var persona in this.individual.personas)
+      this.personas.add (persona);
+
+    yield store.aggregator.unlink_individual (this.individual);
+    this._reversable = true;
+    notify_property ("reversable");
+  }
+
+  /* Undo the unlinking */
+  public async void _undo () throws GLib.Error {
+    yield this.store.aggregator.link_personas (personas);
+    this._reversable = false;
+    notify_property ("reversable");
+  }
+}
diff --git a/src/contacts-utils.vala b/src/contacts-utils.vala
index 4627acee..fd7d72e7 100644
--- a/src/contacts-utils.vala
+++ b/src/contacts-utils.vala
@@ -65,13 +65,10 @@ namespace Contacts {
 }
 
 namespace Contacts.Utils {
+
   public void compose_mail (string email) {
     var mailto_uri = "mailto:"; + Uri.escape_string (email, "@" , false);
-    try {
-      Gtk.show_uri (null, mailto_uri, 0);
-    } catch (Error e) {
-      debug ("Couldn't launch URI \"%s\": %s", mailto_uri, e.message);
-    }
+    Gtk.show_uri (null, mailto_uri, 0);
   }
 
 #if HAVE_TELEPATHY
diff --git a/src/meson.build b/src/meson.build
index 733c9822..f801c70f 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -6,12 +6,16 @@ install_data('org.gnome.Contacts.gschema.xml',
 
 # Common library
 libcontacts_sources = files(
+  'contacts-delete-operation.vala',
   'contacts-esd-setup.vala',
   'contacts-fake-persona-store.vala',
   'contacts-im-service.vala',
+  'contacts-link-operation.vala',
+  'contacts-operation.vala',
   'contacts-store.vala',
   'contacts-typeset.vala',
   'contacts-type-descriptor.vala',
+  'contacts-unlink-operation.vala',
   'contacts-utils.vala',
   'contacts-vcard-type-mapping.vala',
   'xdg-portal-camera.vala',
@@ -81,7 +85,6 @@ contacts_vala_sources = files(
   'contacts-in-app-notification.vala',
   'contacts-link-suggestion-grid.vala',
   'contacts-linked-personas-dialog.vala',
-  'contacts-linking.vala',
   'contacts-list-pane.vala',
   'contacts-main-window.vala',
   'contacts-settings.vala',
diff --git a/todo-gtk4.md b/todo-gtk4.md
new file mode 100644
index 00000000..71937a39
--- /dev/null
+++ b/todo-gtk4.md
@@ -0,0 +1,7 @@
+* go through all XML files and make sure `<requires lib="gtk" version="4.0"></requires>`
+* go through all XML files and try to remove `visible=true`
+* go through all code files and remove unnecessary gtk_widget_show()
+* All XXX remarks which you haven't solved yet
+* Check if some things can be replaced by libadwaita stuff (`InAppNotification`
+    → Toast)
+* Finally: maybe we can use this to just go for uncrustify?


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]