[gnome-todo] task-list: Add helper to move tasks to other positions



commit 325f80f6c5cc7427bd681f818a254873c715ec41
Author: Georges Basile Stavracas Neto <georges stavracas gmail com>
Date:   Wed Feb 6 19:47:41 2019 -0200

    task-list: Add helper to move tasks to other positions
    
    This will be very relevant in future commits where we want to
    keep the position of tasks tightly synchronized with their
    real positions in the task lists.

 src/gtd-task-list.c    | 154 +++++++++++++++++++++++++++++++++++++++++++++++++
 src/gtd-task-list.h    |   4 ++
 tests/meson.build      |   1 +
 tests/test-task-list.c | 101 ++++++++++++++++++++++++++++++++
 4 files changed, 260 insertions(+)
---
diff --git a/src/gtd-task-list.c b/src/gtd-task-list.c
index 8a5f250..76484de 100644
--- a/src/gtd-task-list.c
+++ b/src/gtd-task-list.c
@@ -51,6 +51,8 @@ typedef struct
   GSequence           *sorted_tasks;
   guint                n_tasks;
 
+  guint                freeze_counter;
+
   gchar               *name;
   gboolean             removable;
 } GtdTaskListPrivate;
@@ -248,6 +250,10 @@ task_changed_cb (GtdTask     *task,
       GTD_RETURN ();
     }
 
+  /* Don't update when the list is frozen */
+  if (priv->freeze_counter > 0)
+    GTD_RETURN ();
+
   iter = g_hash_table_lookup (priv->tasks, gtd_object_get_uid (GTD_OBJECT (task)));
 
   old_position = g_sequence_iter_get_position (iter);
@@ -869,3 +875,151 @@ gtd_task_list_get_task_by_id (GtdTaskList *self,
 
   return g_sequence_get (iter);
 }
+
+/**
+ * gtd_task_list_move_task_to_position:
+ * @self: a #GtdTaskList
+ * @task: a #GtdTask
+ * @new_position: the new position of @task inside @self
+ *
+ * Moves @task and all its subtasks to @new_position, and repositions
+ * the elements in between as well.
+ *
+ * @task must belog to @self.
+ */
+void
+gtd_task_list_move_task_to_position (GtdTaskList *self,
+                                     GtdTask     *task,
+                                     guint        new_position)
+{
+  GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (self);
+  GSequenceIter *block_start_iter;
+  GSequenceIter *block_end_iter;
+  GSequenceIter *new_position_iter;
+  gboolean moving_up;
+  guint64 n_subtasks;
+  guint block1_start;
+  guint block1_length;
+  guint block1_new_start;
+  guint block2_start;
+  guint block2_length;
+  guint block2_new_start;
+  guint i;
+
+  /*
+   * The algorithm divides it in 2 blocks:
+   *
+   *  * Block 1: [ @task position → @task position + n_subtasks ]
+   *  * Block 2: the tasks between Block 1 and @new_position
+   *
+   * And there are 2 cases we need to deal with:
+   *
+   *  * Case 1: moving @task to above (@new_position is above the
+   *            current position)
+   *  * Case 2: moving @task to below (@new_position is below the
+   *            current position)
+   */
+
+  g_return_if_fail (GTD_IS_TASK_LIST (self));
+  g_return_if_fail (GTD_IS_TASK (task));
+  g_return_if_fail (gtd_task_list_contains (self, task));
+  g_return_if_fail (g_list_model_get_n_items (G_LIST_MODEL (self)) >= new_position);
+
+  n_subtasks = gtd_task_get_n_total_subtasks (task);
+  block1_start = gtd_task_get_position (task);
+  block1_length = n_subtasks + 1;
+
+  g_return_if_fail (new_position < block1_start || new_position >= block1_start + block1_length);
+
+  moving_up = block1_start > new_position;
+
+  if (moving_up)
+    {
+      /*
+       * Case 1: Moving up
+       */
+      block2_start = new_position;
+      block2_length = block1_start - new_position;
+
+      block1_new_start = new_position;
+      block2_new_start = new_position + block1_length;
+    }
+  else
+    {
+      /*
+       * Case 2: Moving down
+       */
+      block2_start = block1_start + block1_length;
+      block2_length = new_position - block2_start;
+
+      block1_new_start = new_position - n_subtasks - 1;
+      block2_new_start = block2_start - block1_length;
+    }
+
+  GTD_TRACE_MSG ("Moving task and subtasks [%u, %u] to %u, and adjusting [%u, %u] to %u",
+                 block1_start,
+                 block1_start + block1_length - 1,
+                 block1_new_start,
+                 block2_start,
+                 block2_start + block2_length - 1,
+                 block2_new_start);
+
+  priv->freeze_counter++;
+
+  /* Update Block 1 */
+  for (i = 0; i < block1_length; i++)
+    {
+      g_autoptr (GtdTask) task_at_i = NULL;
+
+      task_at_i = g_list_model_get_item (G_LIST_MODEL (self), block1_start + i);
+
+      g_signal_handlers_block_by_func (task_at_i, task_changed_cb, self);
+      gtd_task_set_position (task_at_i, block1_new_start + i);
+      g_signal_handlers_unblock_by_func (task_at_i, task_changed_cb, self);
+    }
+
+  /* Update Block 2 */
+  for (i = 0; i < block2_length; i++)
+    {
+      g_autoptr (GtdTask) task_at_i = NULL;
+
+      task_at_i = g_list_model_get_item (G_LIST_MODEL (self), block2_start + i);
+
+      g_signal_handlers_block_by_func (task_at_i, task_changed_cb, self);
+      gtd_task_set_position (task_at_i, block2_new_start + i);
+      g_signal_handlers_unblock_by_func (task_at_i, task_changed_cb, self);
+    }
+
+  /*
+   * Update the GSequence and emit the signal using the smallest block, to
+   * avoid recreating as many widgets as possible.
+   */
+  if (block1_length < block2_length)
+    {
+      block_start_iter = g_sequence_get_iter_at_pos (priv->sorted_tasks, block1_start);
+      block_end_iter = g_sequence_get_iter_at_pos (priv->sorted_tasks, block1_start + block1_length);
+      new_position_iter = g_sequence_get_iter_at_pos (priv->sorted_tasks, new_position);
+
+      g_sequence_move_range (new_position_iter, block_start_iter, block_end_iter);
+
+      g_list_model_items_changed (G_LIST_MODEL (self), block1_start, block1_length, 0);
+      g_list_model_items_changed (G_LIST_MODEL (self), block1_new_start, 0, block1_length);
+    }
+  else
+    {
+      block_start_iter = g_sequence_get_iter_at_pos (priv->sorted_tasks, block2_start);
+      block_end_iter = g_sequence_get_iter_at_pos (priv->sorted_tasks, block2_start + block2_length);
+
+      if (moving_up)
+        new_position_iter = g_sequence_get_iter_at_pos (priv->sorted_tasks, block1_start + block1_length);
+      else
+        new_position_iter = g_sequence_get_iter_at_pos (priv->sorted_tasks, block1_start);
+
+      g_sequence_move_range (new_position_iter, block_start_iter, block_end_iter);
+
+      g_list_model_items_changed (G_LIST_MODEL (self), block2_start, block2_length, 0);
+      g_list_model_items_changed (G_LIST_MODEL (self), block2_new_start, 0, block2_length);
+    }
+
+  priv->freeze_counter--;
+}
diff --git a/src/gtd-task-list.h b/src/gtd-task-list.h
index 180ef9f..cbe07ba 100644
--- a/src/gtd-task-list.h
+++ b/src/gtd-task-list.h
@@ -84,6 +84,10 @@ gboolean                gtd_task_list_contains                  (GtdTaskList
 GtdTask*                gtd_task_list_get_task_by_id            (GtdTaskList            *self,
                                                                  const gchar            *id);
 
+void                    gtd_task_list_move_task_to_position     (GtdTaskList            *self,
+                                                                 GtdTask                *task,
+                                                                 guint                   new_position);
+
 G_END_DECLS
 
 #endif /* GTD_TASK_LIST_H */
diff --git a/tests/meson.build b/tests/meson.build
index c60bb27..35e79b9 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -49,6 +49,7 @@ static_test_link_args = ['-fPIC']
 static_tests = [
   'test-model-filter',
   'test-model-sort',
+  'test-task-list',
   'test-task-model',
 ]
 
diff --git a/tests/test-task-list.c b/tests/test-task-list.c
new file mode 100644
index 0000000..faf6963
--- /dev/null
+++ b/tests/test-task-list.c
@@ -0,0 +1,101 @@
+/* test-task-model.c
+ *
+ * Copyright 2018 Georges Basile Stavracas Neto <georges stavracas 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 3 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "gnome-todo.h"
+
+#include "gtd-task-list.h"
+#include "logging/gtd-log.h"
+#include "gtd-manager-protected.h"
+#include "dummy-provider.h"
+
+static void
+test_move (void)
+{
+  g_autoptr (DummyProvider) dummy_provider = NULL;
+  g_autoptr (GtdTask) first_root_task = NULL;
+  g_autoptr (GtdTask) last_root_task = NULL;
+  g_autoptr (GList) lists = NULL;
+  GtdTaskList *list = NULL;
+  GListModel *model;
+  guint n_tasks;
+
+  dummy_provider = dummy_provider_new ();
+  n_tasks = dummy_provider_generate_task_list (dummy_provider);
+  g_assert_cmpuint (n_tasks, ==, 10);
+
+  _gtd_manager_inject_provider (gtd_manager_get_default (), GTD_PROVIDER (dummy_provider));
+
+  lists = gtd_provider_get_task_lists (GTD_PROVIDER (dummy_provider));
+  g_assert_nonnull (lists);
+  g_assert_cmpint (g_list_length (lists), ==, 1);
+
+  list = lists->data;
+  model = G_LIST_MODEL (list);
+  g_assert_nonnull (list);
+  g_assert_cmpstr (gtd_task_list_get_name (list), ==, "List");
+
+  first_root_task = g_list_model_get_item (model, 0);
+  g_assert_nonnull (first_root_task);
+  g_assert_cmpint (gtd_task_get_position (first_root_task), ==, 0);
+  g_assert_true (g_list_model_get_item (model, 0) == first_root_task);
+  g_assert_cmpuint (gtd_task_get_n_total_subtasks (first_root_task), ==, 0);
+
+  last_root_task = g_list_model_get_item (model, 6);
+  g_assert_nonnull (last_root_task);
+  g_assert_cmpint (gtd_task_get_position (last_root_task), ==, 6);
+  g_assert_true (g_list_model_get_item (model, 6) == last_root_task);
+  g_assert_cmpuint (gtd_task_get_n_total_subtasks (last_root_task), ==, 3);
+
+  /* Move the task to 0 */
+  gtd_task_list_move_task_to_position (list, last_root_task, 0);
+
+  g_assert_cmpint (gtd_task_get_position (last_root_task), ==, 0);
+  g_assert_cmpint (gtd_task_get_position (first_root_task), ==, 4);
+  g_assert_true (g_list_model_get_item (model, 0) == last_root_task);
+  g_assert_true (g_list_model_get_item (model, 4) == first_root_task);
+
+  /* Move the task to 1 */
+  gtd_task_list_move_task_to_position (list, last_root_task, 5);
+
+  g_assert_cmpint (gtd_task_get_position (last_root_task), ==, 1);
+  g_assert_cmpint (gtd_task_get_position (first_root_task), ==, 0);
+  g_assert_true (g_list_model_get_item (model, 0) == first_root_task);
+  g_assert_true (g_list_model_get_item (model, 1) == last_root_task);
+
+  /* Move the task to 10 */
+  gtd_task_list_move_task_to_position (list, last_root_task, 10);
+
+  g_assert_cmpint (gtd_task_get_position (last_root_task), ==, 6);
+  g_assert_true (g_list_model_get_item (model, 6) == last_root_task);
+}
+
+gint
+main (gint argc,
+      gchar *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+
+  if (g_getenv ("G_MESSAGES_DEBUG"))
+    gtd_log_init ();
+
+  g_test_add_func ("/task-list/move", test_move);
+
+  return g_test_run ();
+}


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