[gnome-contacts] Window: Implement the UI as an FSM.



commit 279fecd0459f6b7e9112e7b3542c2d5300943d39
Author: Niels De Graef <nielsdegraef gmail com>
Date:   Sat Jan 13 18:28:28 2018 +0100

    Window: Implement the UI as an FSM.
    
    This way it's clear what parts of the UI should be active when
    selecting, editing, ...
    
    It also allows us to get rid of a hard to follow combo of
    edit_mode/selection_mode/new_contact variables.

 src/contacts-ui-state.vala |   60 +++++++++++++++++++++
 src/contacts-window.vala   |  126 +++++++++++++++++++-------------------------
 src/meson.build            |    1 +
 3 files changed, 116 insertions(+), 71 deletions(-)
---
diff --git a/src/contacts-ui-state.vala b/src/contacts-ui-state.vala
new file mode 100644
index 0000000..ad7230b
--- /dev/null
+++ b/src/contacts-ui-state.vala
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2018 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/>.
+ */
+
+/**
+ * Roughly put, the behaviour of the UI of Contacts can be divided in several
+ * categories. We represent this with the UiState enum, which can be shared
+ * (and sync-ed) between the different parts of the app.
+ *
+ * Note that there is one exception to this: the initial setup is handled
+ * completely separately in the {@link SetupWindow}.
+ */
+public enum Contacts.UiState {
+  /**
+   * The start state: no contact is selected/displayed.
+   */
+  NORMAL,
+
+  /**
+   * A contact has been selected and is displayed.
+   */
+  SHOWING,
+
+  /**
+   * Zero or more contacts are selected (but this can be changed).
+   * No contact should be displayed.
+   */
+  SELECTING,
+
+  /**
+   * The selected contact is being edited.
+   */
+  UPDATING,
+
+  /**
+   * A new contact is being created.
+   */
+  CREATING;
+
+  /**
+   * Returns whether we're editing a contact, either by changing an existing
+   * one, or by creating a new one.
+   */
+  public bool editing () {
+    return this == UiState.UPDATING || this == UiState.CREATING;
+  }
+}
diff --git a/src/contacts-window.vala b/src/contacts-window.vala
index 9ce6c43..f20cc8f 100644
--- a/src/contacts-window.vala
+++ b/src/contacts-window.vala
@@ -52,13 +52,17 @@ public class Contacts.Window : Gtk.ApplicationWindow {
   private ListPane list_pane;
   private ContactPane contact_pane;
 
+  // We start in the normal UI state
+  private UiState _state = UiState.NORMAL;
+  public UiState state {
+    get { return this._state; }
+    set { change_ui_state (value); }
+  }
+
   public Store store {
     get; construct set;
   }
 
-  private bool selection_mode = false;
-  private bool editing_new_contact = false;
-
   public Window (App app, Store contacts_store) {
     Object (
       application: app,
@@ -68,7 +72,6 @@ public class Contacts.Window : Gtk.ApplicationWindow {
     debug ("everyone creation: finalized already!!!");
 
     create_contact_pane ();
-
     set_headerbar_layout ();
     connect_button_signals ();
   }
@@ -113,60 +116,49 @@ public class Contacts.Window : Gtk.ApplicationWindow {
     list_pane.show ();
   }
 
-  public void activate_selection_mode (bool active) {
-    this.selection_mode = active;
-
-    // Show some buttons when selecting (and vice versa)
-    this.select_cancel_button.visible = active;
-    // and hide some
-    this.select_button.visible = !active;
-    this.add_button.visible = !active;
-    this.edit_button.visible = !active;
-    this.right_header.show_close_button = !active;
-
-    this.list_pane.activate_selection_mode (active);
-
-    if (active) {
-      left_header.get_style_context ().add_class ("selection-mode");
-      right_header.get_style_context ().add_class ("selection-mode");
-
-      left_header.set_title (_("Select"));
-
+  private void change_ui_state (UiState new_state) {
+    // UI when we're not editing of selecting stuff
+    this.add_button.visible
+        = this.right_header.show_close_button
+        = this.select_button.visible
+        = (new_state == UiState.NORMAL || new_state == UiState.SHOWING);
+
+    // UI when showing a contact
+    this.edit_button.visible = (new_state == UiState.SHOWING);
+
+    // Selecting UI
+    this.select_cancel_button.visible = (new_state == UiState.SELECTING);
+    this.list_pane.activate_selection_mode (new_state == UiState.SELECTING);
+
+    // Editing UI
+    this.cancel_button.visible
+        = this.done_button.visible
+        = new_state.editing ();
+    if (new_state.editing ())
+      this.done_button.label = (new_state == UiState.CREATING)? _("Add") : _("Done");
+
+    // When selecting or editing, we get special headerbars
+    if (new_state == UiState.SELECTING || new_state.editing ()) {
+      this.left_header.get_style_context ().add_class ("selection-mode");
+      this.right_header.get_style_context ().add_class ("selection-mode");
+
+      this.left_header.title = (new_state == UiState.SELECTING)?  _("Select") : "";
     } else {
-      left_header.get_style_context ().remove_class ("selection-mode");
-      right_header.get_style_context ().remove_class ("selection-mode");
-
-      left_header.set_title (_("All Contacts"));
+      this.left_header.get_style_context ().remove_class ("selection-mode");
+      this.right_header.get_style_context ().remove_class ("selection-mode");
 
-      /* could be no contact selected whatsoever */
-      if (this.contact_pane.contact == null)
-        edit_button.hide ();
+      this.left_header.title = _("All Contacts");
     }
-  }
-
-  private void activate_edit_mode (bool active) {
-    this.done_button.visible = active;
-    this.cancel_button.visible = active;
-
-    this.edit_button.visible = !active;
-    this.add_button.visible = !active;
-    this.select_button.visible = !active;
-    this.right_header.show_close_button = !active;
 
-    if (active) {
-      left_header.get_style_context ().add_class ("selection-mode");
-      right_header.get_style_context ().add_class ("selection-mode");
-    } else {
-      left_header.get_style_context ().remove_class ("selection-mode");
-      right_header.get_style_context ().remove_class ("selection-mode");
-    }
+    // Save the result
+    this._state = new_state;
   }
 
   private void edit_contact () {
     if (this.contact_pane.contact == null)
       return;
 
-    activate_edit_mode (true);
+    this.state = UiState.UPDATING;
 
     var name = this.contact_pane.contact.display_name;
     this.right_header.title = _("Editing %s").printf (name);
@@ -174,18 +166,14 @@ public class Contacts.Window : Gtk.ApplicationWindow {
     this.contact_pane.set_edit_mode (true);
   }
 
-  private void leave_edit_mode (bool drop_changes = false) {
-    activate_edit_mode (false);
-
-    if (this.editing_new_contact) {
-      done_button.label = _("Done");
+  private void stop_editing (bool drop_changes = false) {
+    if (this.state == UiState.CREATING) {
 
       if (drop_changes) {
         this.contact_pane.set_edit_mode (false, drop_changes);
       } else {
         this.contact_pane.create_contact.begin ();
       }
-      this.editing_new_contact = false;
     } else {
       this.contact_pane.set_edit_mode (false, drop_changes);
     }
@@ -194,8 +182,9 @@ public class Contacts.Window : Gtk.ApplicationWindow {
       this.right_header.title = this.contact_pane.contact.display_name;
     } else {
       this.right_header.title = "";
-      edit_button.hide ();
     }
+
+    this.state = UiState.SHOWING;
   }
 
   public void add_notification (InAppNotification notification) {
@@ -206,27 +195,24 @@ public class Contacts.Window : Gtk.ApplicationWindow {
   public void set_shown_contact (Contact? c) {
     /* FIXME: ask the user to leave edit-mode and act accordingly */
     if (this.contact_pane.on_edit_mode)
-      leave_edit_mode ();
+      stop_editing ();
+
+    this.state = (c != null)? UiState.SHOWING : UiState.NORMAL;
 
     this.contact_pane.show_contact (c, false);
     if (list_pane != null)
       list_pane.select_contact (c);
 
     // clearing right_header
-    this.right_header.title = (c != null)? c.display_name : "";
-
-    edit_button.visible = (c != null) && !this.selection_mode;
+    if (c != null)
+      this.right_header.title = c.display_name;
   }
 
   [GtkCallback]
   public void new_contact () {
-    /* FIXME: eventually ContactPane will become just a skeleton and
-     * this call will go through to ContactEditor */
-    activate_edit_mode (true);
-    this.editing_new_contact = true;
+    this.state = UiState.CREATING;
 
     this.right_header.title = _("New Contact");
-    this.done_button.label = _("Add");
 
     this.contact_pane.new_contact ();
   }
@@ -247,11 +233,11 @@ public class Contacts.Window : Gtk.ApplicationWindow {
   }
 
   private void connect_button_signals () {
-    this.select_button.clicked.connect (() => activate_selection_mode (true));
-    this.select_cancel_button.clicked.connect (() => activate_selection_mode (false));
+    this.select_button.clicked.connect (() => { this.state = UiState.SELECTING; });
+    this.select_cancel_button.clicked.connect (() => { this.state = UiState.NORMAL; });
     this.edit_button.clicked.connect (() => edit_contact ());
-    this.done_button.clicked.connect (() => leave_edit_mode ());
-    this.cancel_button.clicked.connect (() => leave_edit_mode (true));
+    this.done_button.clicked.connect (() => stop_editing ());
+    this.cancel_button.clicked.connect (() => stop_editing (true));
   }
 
   [GtkCallback]
@@ -293,7 +279,6 @@ public class Contacts.Window : Gtk.ApplicationWindow {
   void list_pane_link_contacts_cb (LinkedList<Contact> contact_list) {
     /* getting out of selection mode */
     set_shown_contact (null);
-    activate_selection_mode (false);
 
     LinkOperation2 operation = null;
     link_contacts_list.begin (contact_list, this.store, (obj, result) => {
@@ -320,7 +305,7 @@ public class Contacts.Window : Gtk.ApplicationWindow {
   void list_pane_delete_contacts_cb (LinkedList<Contact> contact_list) {
     /* getting out of selection mode */
     set_shown_contact (null);
-    activate_selection_mode (false);
+    this.state == UiState.NORMAL;
 
     string msg = ngettext ("%d contact deleted",
                            "%d contacts deleted",
@@ -353,7 +338,6 @@ public class Contacts.Window : Gtk.ApplicationWindow {
   private void contact_pane_delete_contact_cb (Contact contact) {
     /* unsetting edit-mode */
     set_shown_contact (null);
-    activate_selection_mode (false);
 
     var msg = _("Contact deleted: ā€œ%sā€").printf (contact.display_name);
     var b = new Button.with_mnemonic (_("_Undo"));
diff --git a/src/meson.build b/src/meson.build
index 2a387c3..a7bbf95 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -24,6 +24,7 @@ contacts_vala_sources = [
   'contacts-setup-window.vala',
   'contacts-store.vala',
   'contacts-types.vala',
+  'contacts-ui-state.vala',
   'contacts-utils.vala',
   'contacts-window.vala',
   'main.vala',


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