1 /* gbp-todo-panel.c
2  *
3  * Copyright 2017-2019 Christian Hergert <chergert@redhat.com>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * SPDX-License-Identifier: GPL-3.0-or-later
19  */
20 
21 #define G_LOG_DOMAIN "gbp-todo-panel"
22 
23 #include <glib/gi18n.h>
24 #include <libide-code.h>
25 #include <libide-gui.h>
26 
27 #include "gbp-todo-item.h"
28 #include "gbp-todo-panel.h"
29 
30 struct _GbpTodoPanel
31 {
32   DzlDockWidget  parent_instance;
33 
34   GbpTodoModel  *model;
35 
36   GtkTreeView   *tree_view;
37   GtkStack      *stack;
38 };
39 
40 G_DEFINE_FINAL_TYPE (GbpTodoPanel, gbp_todo_panel, DZL_TYPE_DOCK_WIDGET)
41 
42 enum {
43   PROP_0,
44   PROP_MODEL,
45   N_PROPS
46 };
47 
48 static GParamSpec *properties [N_PROPS];
49 
50 static void
gbp_todo_panel_cell_data_func(GtkCellLayout * cell_layout,GtkCellRenderer * cell,GtkTreeModel * tree_model,GtkTreeIter * iter,gpointer data)51 gbp_todo_panel_cell_data_func (GtkCellLayout   *cell_layout,
52                                GtkCellRenderer *cell,
53                                GtkTreeModel    *tree_model,
54                                GtkTreeIter     *iter,
55                                gpointer         data)
56 {
57   g_autoptr(GbpTodoItem) item = NULL;
58   const gchar *message;
59 
60   gtk_tree_model_get (tree_model, iter, 0, &item, -1);
61 
62   message = gbp_todo_item_get_line (item, 0);
63 
64   if (message != NULL)
65     {
66       g_autofree gchar *title = NULL;
67       const gchar *path;
68       guint lineno;
69 
70       /*
71        * We don't trim the whitespace from lines so that we can keep
72        * them in tact when showing tooltips. So we need to truncate
73        * here for display in the pane.
74        */
75       while (g_ascii_isspace (*message))
76         message++;
77 
78       path = gbp_todo_item_get_path (item);
79       lineno = gbp_todo_item_get_lineno (item);
80       title = g_strdup_printf ("%s:%u", path, lineno);
81       ide_cell_renderer_fancy_take_title (IDE_CELL_RENDERER_FANCY (cell),
82                                           g_steal_pointer (&title));
83       ide_cell_renderer_fancy_set_body (IDE_CELL_RENDERER_FANCY (cell), message);
84     }
85   else
86     {
87       ide_cell_renderer_fancy_set_body (IDE_CELL_RENDERER_FANCY (cell), NULL);
88       ide_cell_renderer_fancy_set_title (IDE_CELL_RENDERER_FANCY (cell), NULL);
89     }
90 }
91 
92 static void
gbp_todo_panel_row_activated(GbpTodoPanel * self,GtkTreePath * tree_path,GtkTreeViewColumn * column,GtkTreeView * tree_view)93 gbp_todo_panel_row_activated (GbpTodoPanel      *self,
94                               GtkTreePath       *tree_path,
95                               GtkTreeViewColumn *column,
96                               GtkTreeView       *tree_view)
97 {
98   g_autoptr(GbpTodoItem) item = NULL;
99   g_autoptr(GFile) file = NULL;
100   IdeWorkbench *workbench;
101   GtkTreeModel *model;
102   const gchar *path;
103   GtkTreeIter iter;
104   guint lineno;
105 
106   g_assert (GBP_IS_TODO_PANEL (self));
107   g_assert (tree_path != NULL);
108   g_assert (GTK_IS_TREE_VIEW (tree_view));
109 
110   model = gtk_tree_view_get_model (tree_view);
111   gtk_tree_model_get_iter (model, &iter, tree_path);
112   gtk_tree_model_get (model, &iter, 0, &item, -1);
113   g_assert (GBP_IS_TODO_ITEM (item));
114 
115   workbench = ide_widget_get_workbench (GTK_WIDGET (self));
116   g_assert (IDE_IS_WORKBENCH (workbench));
117 
118   path = gbp_todo_item_get_path (item);
119   g_assert (path != NULL);
120 
121   if (g_path_is_absolute (path))
122     {
123       file = g_file_new_for_path (path);
124     }
125   else
126     {
127       IdeContext *context;
128       IdeVcs *vcs;
129       GFile *workdir;
130 
131       context = ide_workbench_get_context (workbench);
132       vcs = ide_vcs_from_context (context);
133       workdir = ide_vcs_get_workdir (vcs);
134       file = g_file_get_child (workdir, path);
135     }
136 
137   /* Set lineno info so that the editor can jump to the location of the TODO
138    * item. Our line number from the model is 1-based, and we need 0-based for
139    * our API to open files.
140    */
141   lineno = gbp_todo_item_get_lineno (item);
142   if (lineno > 0)
143     lineno--;
144 
145   ide_workbench_open_at_async (workbench,
146                                file,
147                                "editor",
148                                lineno,
149                                -1,
150                                IDE_BUFFER_OPEN_FLAGS_NONE,
151                                NULL, NULL, NULL);
152 }
153 
154 static gboolean
gbp_todo_panel_query_tooltip(GbpTodoPanel * self,gint x,gint y,gboolean keyboard_mode,GtkTooltip * tooltip,GtkTreeView * tree_view)155 gbp_todo_panel_query_tooltip (GbpTodoPanel *self,
156                               gint          x,
157                               gint          y,
158                               gboolean      keyboard_mode,
159                               GtkTooltip   *tooltip,
160                               GtkTreeView  *tree_view)
161 {
162   g_autoptr(GtkTreePath) path = NULL;
163   GtkTreeModel *model;
164 
165   g_assert (GBP_IS_TODO_PANEL (self));
166   g_assert (GTK_IS_TOOLTIP (tooltip));
167   g_assert (GTK_IS_TREE_VIEW (tree_view));
168 
169   if (NULL == (model = gtk_tree_view_get_model (tree_view)))
170     return FALSE;
171 
172   if (gtk_tree_view_get_path_at_pos (tree_view, x, y, &path, NULL, NULL, NULL))
173     {
174       GtkTreeIter iter;
175 
176       if (gtk_tree_model_get_iter (model, &iter, path))
177         {
178           g_autoptr(GbpTodoItem) item = NULL;
179           g_autoptr(GString) str = g_string_new ("<tt>");
180 
181           gtk_tree_model_get (model, &iter, 0, &item, -1);
182           g_assert (GBP_IS_TODO_ITEM (item));
183 
184           /* only 5 lines stashed */
185           for (guint i = 0; i < 5; i++)
186             {
187               const gchar *line = gbp_todo_item_get_line (item, i);
188               g_autofree gchar *escaped = NULL;
189 
190               if (!line)
191                 break;
192 
193               escaped = g_markup_escape_text (line, -1);
194               g_string_append (str, escaped);
195               g_string_append_c (str, '\n');
196             }
197 
198           g_string_append (str, "</tt>");
199           gtk_tree_view_set_tooltip_row (tree_view, tooltip, path);
200           gtk_tooltip_set_markup (tooltip, str->str);
201 
202 	  return TRUE;
203         }
204     }
205 
206   return FALSE;
207 }
208 
209 static void
gbp_todo_panel_destroy(GtkWidget * widget)210 gbp_todo_panel_destroy (GtkWidget *widget)
211 {
212   GbpTodoPanel *self = (GbpTodoPanel *)widget;
213 
214   g_assert (GBP_IS_TODO_PANEL (self));
215 
216   if (self->tree_view != NULL)
217     gtk_tree_view_set_model (self->tree_view, NULL);
218 
219   g_clear_object (&self->model);
220 
221   GTK_WIDGET_CLASS (gbp_todo_panel_parent_class)->destroy (widget);
222 }
223 
224 static void
gbp_todo_panel_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)225 gbp_todo_panel_get_property (GObject    *object,
226                              guint       prop_id,
227                              GValue     *value,
228                              GParamSpec *pspec)
229 {
230   GbpTodoPanel *self = GBP_TODO_PANEL (object);
231 
232   switch (prop_id)
233     {
234     case PROP_MODEL:
235       g_value_set_object (value, gbp_todo_panel_get_model (self));
236       break;
237 
238     default:
239       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
240     }
241 }
242 
243 static void
gbp_todo_panel_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)244 gbp_todo_panel_set_property (GObject      *object,
245                              guint         prop_id,
246                              const GValue *value,
247                              GParamSpec   *pspec)
248 {
249   GbpTodoPanel *self = GBP_TODO_PANEL (object);
250 
251   switch (prop_id)
252     {
253     case PROP_MODEL:
254       gbp_todo_panel_set_model (self, g_value_get_object (value));
255       break;
256 
257     default:
258       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
259     }
260 }
261 
262 static void
gbp_todo_panel_class_init(GbpTodoPanelClass * klass)263 gbp_todo_panel_class_init (GbpTodoPanelClass *klass)
264 {
265   GObjectClass *object_class = G_OBJECT_CLASS (klass);
266   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
267 
268   object_class->get_property = gbp_todo_panel_get_property;
269   object_class->set_property = gbp_todo_panel_set_property;
270 
271   widget_class->destroy = gbp_todo_panel_destroy;
272 
273   properties [PROP_MODEL] =
274     g_param_spec_object ("model",
275                          "Model",
276                          "The model for the TODO list",
277                          GBP_TYPE_TODO_MODEL,
278                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
279 
280   g_object_class_install_properties (object_class, N_PROPS, properties);
281 }
282 
283 static void
gbp_todo_panel_init(GbpTodoPanel * self)284 gbp_todo_panel_init (GbpTodoPanel *self)
285 {
286   GtkWidget *scroller;
287   GtkWidget *empty;
288   GtkTreeSelection *selection;
289 
290   self->stack = g_object_new (GTK_TYPE_STACK,
291                               "transition-duration", 333,
292                               "transition-type", GTK_STACK_TRANSITION_TYPE_CROSSFADE,
293                               "homogeneous", FALSE,
294                               "visible", TRUE,
295                               NULL);
296   gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->stack));
297 
298   empty = g_object_new (DZL_TYPE_EMPTY_STATE,
299                         "title", _("Loading TODOs…"),
300                         "subtitle", _("Please wait while we scan your project"),
301                         "icon-name", "emblem-ok-symbolic",
302                         "valign", GTK_ALIGN_START,
303                         "visible", TRUE,
304                         NULL);
305   gtk_container_add (GTK_CONTAINER (self->stack), empty);
306 
307   scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
308                            "visible", TRUE,
309                            "vexpand", TRUE,
310                            NULL);
311   gtk_container_add_with_properties (GTK_CONTAINER (self->stack), scroller,
312                                      "name", "todos",
313                                      NULL);
314 
315   self->tree_view = g_object_new (IDE_TYPE_FANCY_TREE_VIEW,
316                                   "has-tooltip", TRUE,
317                                   "visible", TRUE,
318                                   NULL);
319   g_signal_connect (self->tree_view,
320                     "destroy",
321                     G_CALLBACK (gtk_widget_destroyed),
322                     &self->tree_view);
323   g_signal_connect_swapped (self->tree_view,
324                             "row-activated",
325                             G_CALLBACK (gbp_todo_panel_row_activated),
326                             self);
327   g_signal_connect_swapped (self->tree_view,
328                             "query-tooltip",
329                             G_CALLBACK (gbp_todo_panel_query_tooltip),
330                             self);
331   dzl_gtk_widget_add_style_class (GTK_WIDGET (self->tree_view), "i-wanna-be-listbox");
332   gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET (self->tree_view));
333 
334   selection = gtk_tree_view_get_selection (self->tree_view);
335   gtk_tree_selection_set_mode (selection, GTK_SELECTION_NONE);
336 
337   ide_fancy_tree_view_set_data_func (IDE_FANCY_TREE_VIEW (self->tree_view),
338                                      gbp_todo_panel_cell_data_func, NULL, NULL);
339 }
340 
341 /**
342  * gbp_todo_panel_get_model:
343  * @self: a #GbpTodoPanel
344  *
345  * Gets the model being displayed by the treeview.
346  *
347  * Returns: (transfer none) (nullable): a #GbpTodoModel.
348  *
349  * Since: 3.32
350  */
351 GbpTodoModel *
gbp_todo_panel_get_model(GbpTodoPanel * self)352 gbp_todo_panel_get_model (GbpTodoPanel *self)
353 {
354   g_return_val_if_fail (GBP_IS_TODO_PANEL (self), NULL);
355 
356   return self->model;
357 }
358 
359 void
gbp_todo_panel_set_model(GbpTodoPanel * self,GbpTodoModel * model)360 gbp_todo_panel_set_model (GbpTodoPanel *self,
361                           GbpTodoModel *model)
362 {
363   g_return_if_fail (GBP_IS_TODO_PANEL (self));
364   g_return_if_fail (!model || GBP_IS_TODO_MODEL (model));
365 
366   if (g_set_object (&self->model, model))
367     {
368       if (self->model != NULL)
369         gtk_tree_view_set_model (self->tree_view, GTK_TREE_MODEL (self->model));
370       else
371         gtk_tree_view_set_model (self->tree_view, NULL);
372 
373       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]);
374     }
375 }
376 
377 void
gbp_todo_panel_make_ready(GbpTodoPanel * self)378 gbp_todo_panel_make_ready (GbpTodoPanel *self)
379 {
380   g_return_if_fail (GBP_IS_TODO_PANEL (self));
381 
382   gtk_stack_set_visible_child_name (self->stack, "todos");
383 }
384