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