1 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
2 /*
3  * anjuta
4  * Copyright (C) James Liggett 2010 <jrliggett@cox.net>
5  *
6  * anjuta is free software: you can redistribute it and/or modify it
7  * under the terms of the GNU General Public License as published by the
8  * Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * anjuta is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14  * See the GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License along
17  * with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "anjuta-file-list.h"
21 
22 enum
23 {
24 	PROP_0,
25 
26 	PROP_RELATIVE_PATH,
27 	PROP_SHOW_ADD_BUTTON
28 };
29 
30 enum
31 {
32 	COL_PATH,
33 
34 	NUM_COLS
35 };
36 
37 /* DND Targets */
38 static GtkTargetEntry dnd_target_entries[] =
39 {
40 	{
41 		"text/uri-list",
42 		0,
43 		0
44 	}
45 };
46 
47 struct _AnjutaFileListPriv
48 {
49 	gchar *relative_path;
50 	GtkWidget *list_view;
51 	GtkListStore *list_model;
52 	GtkWidget *add_button;
53 	GtkWidget *copy_button;
54 	GtkWidget *remove_button;
55 
56 	/* The placeholder iter tells the user that a file can be entered into the
57 	 * view, or dragged into it. */
58 	GtkTreeIter placeholder;
59 };
60 
61 G_DEFINE_TYPE (AnjutaFileList, anjuta_file_list, GTK_TYPE_BOX);
62 
63 static void
anjuta_file_list_append_placeholder(AnjutaFileList * self)64 anjuta_file_list_append_placeholder (AnjutaFileList *self)
65 {
66 	GtkTreeIter iter;
67 
68 	gtk_list_store_append (self->priv->list_model, &iter);
69 
70 	gtk_list_store_set (self->priv->list_model, &iter, COL_PATH, NULL, -1);
71 
72 	self->priv->placeholder = iter;
73 }
74 
75 
76 static void
path_cell_data_func(GtkTreeViewColumn * column,GtkCellRenderer * renderer,GtkTreeModel * model,GtkTreeIter * iter,GtkTreeView * list_view)77 path_cell_data_func (GtkTreeViewColumn *column, GtkCellRenderer *renderer,
78                      GtkTreeModel *model,  GtkTreeIter *iter,
79                      GtkTreeView *list_view)
80 {
81 	gchar *path;
82 	GtkStyleContext *context;
83 	GdkRGBA fg_color;
84 
85 	gtk_tree_model_get (model, iter, COL_PATH, &path, -1);
86 	context = gtk_widget_get_style_context (GTK_WIDGET (list_view));
87 
88 	/* NULL path means this is the placeholder */
89 	if (path)
90 	{
91 		gtk_style_context_get_color (context, GTK_STATE_FLAG_NORMAL, &fg_color);
92 		g_object_set (G_OBJECT (renderer),
93 		              "foreground-rgba", &fg_color,
94 		              "style", PANGO_STYLE_NORMAL,
95 		              "text", path,
96 		              NULL);
97 	}
98 	else
99 	{
100 		gtk_style_context_get_color (context, GTK_STATE_FLAG_INSENSITIVE, &fg_color);
101 		g_object_set (G_OBJECT (renderer),
102 		              "foreground-rgba", &fg_color,
103 		              "style", PANGO_STYLE_ITALIC,
104 		              "text", _("Drop a file or enter a path here"),
105 		              NULL);
106 	}
107 
108 	g_free (path);
109 }
110 
111 static void
on_path_renderer_editing_started(GtkCellRenderer * renderer,GtkCellEditable * editable,const gchar * tree_path,GtkTreeModel * list_model)112 on_path_renderer_editing_started (GtkCellRenderer *renderer,
113                                   GtkCellEditable *editable,
114                                   const gchar *tree_path,
115                                   GtkTreeModel *list_model)
116 {
117 	GtkTreeIter iter;
118 	gchar *path;
119 
120 	/* Don't show placeholder text in the edit widget */
121 	gtk_tree_model_get_iter_from_string (list_model, &iter, tree_path);
122 
123 	gtk_tree_model_get (list_model, &iter, COL_PATH, &path, -1);
124 
125 	if (!path)
126 	{
127 		if (GTK_IS_ENTRY (editable))
128 			gtk_entry_set_text (GTK_ENTRY (editable), "");
129 	}
130 }
131 
132 static void
on_path_renderer_edited(GtkCellRendererText * renderer,gchar * path,gchar * new_text,AnjutaFileList * self)133 on_path_renderer_edited (GtkCellRendererText *renderer, gchar *path,
134                          gchar *new_text, AnjutaFileList *self)
135 {
136 	GtkTreeIter iter;
137 	gchar *current_path;
138 
139 	gtk_tree_model_get_iter_from_string (GTK_TREE_MODEL (self->priv->list_model),
140 	                                     &iter, path);
141 	gtk_tree_model_get (GTK_TREE_MODEL (self->priv->list_model), &iter,
142 	                    COL_PATH, &current_path, -1);
143 
144 	/* Interpret empty new_text as a cancellation of the edit */
145 	if (g_utf8_strlen (new_text, -1) > 0)
146 	{
147 		/* If the placeholder is being edited, a new one has to be created */
148 		if (!current_path)
149 			anjuta_file_list_append_placeholder (self);
150 
151 		gtk_list_store_set (self->priv->list_model, &iter, COL_PATH, new_text,
152 		                    -1);
153 	}
154 }
155 
156 static void
on_list_view_drag_data_received(GtkWidget * list_view,GdkDragContext * context,gint x,gint y,GtkSelectionData * data,gint target_type,gint time,AnjutaFileList * self)157 on_list_view_drag_data_received (GtkWidget *list_view, GdkDragContext *context,
158                                  gint x, gint y, GtkSelectionData *data,
159                                  gint target_type, gint time,
160                                  AnjutaFileList *self)
161 {
162 	gboolean success;
163 	gchar **uri_list;
164 	gint i;
165 	GFile *parent_file;
166 	GFile *file;
167 	gchar *path;
168 	GtkTreeIter iter;
169 
170 	success = FALSE;
171 
172 	if ((data != NULL) &&
173 	    (gtk_selection_data_get_length (data) >= 0))
174 	{
175 		if (target_type == 0)
176 		{
177 			uri_list = gtk_selection_data_get_uris (data);
178 			parent_file = NULL;
179 
180 			if (self->priv->relative_path)
181 				parent_file = g_file_new_for_path (self->priv->relative_path);
182 
183 			for (i = 0; uri_list[i]; i++)
184 			{
185 				file = g_file_new_for_uri (uri_list[i]);
186 
187 				if (parent_file)
188 				{
189 					path = g_file_get_relative_path (parent_file, file);
190 
191 					g_object_unref (parent_file);
192 				}
193 				else
194 					path = g_file_get_path (file);
195 
196 				if (path)
197 				{
198 					gtk_list_store_insert_before (self->priv->list_model,
199 					                              &iter,
200 					                              &(self->priv->placeholder));
201 					gtk_list_store_set (self->priv->list_model, &iter,
202 					                    COL_PATH, path,
203 					                    -1);
204 
205 					g_free (path);
206 				}
207 
208 				g_object_unref (file);
209 			}
210 
211 			success = TRUE;
212 
213 			g_strfreev (uri_list);
214 		}
215 	}
216 
217 	/* Do not delete source data */
218 	gtk_drag_finish (context, success, FALSE, time);
219 }
220 
221 static gboolean
on_list_view_drag_drop(GtkWidget * widget,GdkDragContext * context,gint x,gint y,guint time,gpointer user_data)222 on_list_view_drag_drop (GtkWidget *widget, GdkDragContext *context,
223                         gint x, gint y, guint time, gpointer user_data)
224 {
225 	GdkAtom target_type;
226 
227 	target_type = gtk_drag_dest_find_target (widget, context, NULL);
228 
229 	if (target_type != GDK_NONE)
230 		gtk_drag_get_data (widget, context, target_type, time);
231 	else
232 		gtk_drag_finish (context, FALSE, FALSE, time);
233 
234 	return TRUE;
235 }
236 
237 static gboolean
on_list_view_item_selected(GtkTreeSelection * selection,GtkTreeModel * model,GtkTreePath * tree_path,gboolean path_currently_selected,AnjutaFileList * self)238 on_list_view_item_selected (GtkTreeSelection *selection, GtkTreeModel *model,
239                             GtkTreePath *tree_path,
240                             gboolean path_currently_selected,
241                             AnjutaFileList *self)
242 {
243 
244 	gboolean sensitive;
245 	GtkTreeIter iter;
246 	gchar *path;
247 
248 	sensitive = FALSE;
249 
250 	if (!path_currently_selected)
251 	{
252 		gtk_tree_model_get_iter (model, &iter, tree_path);
253 		gtk_tree_model_get (model, &iter, COL_PATH, &path, -1);
254 
255 		if (path)
256 		{
257 			sensitive = TRUE;
258 
259 			g_free (path);
260 		}
261 	}
262 
263 	gtk_widget_set_sensitive (self->priv->copy_button, sensitive);
264 	gtk_widget_set_sensitive (self->priv->remove_button, sensitive);
265 
266 	return TRUE;
267 }
268 
269 static void
on_add_button_clicked(GtkButton * button,AnjutaFileList * self)270 on_add_button_clicked (GtkButton *button, AnjutaFileList *self)
271 {
272 	GtkWidget *file_dialog;
273 	GSList *paths;
274 	GSList *current_path;
275 	GtkTreeIter iter;
276 
277 	file_dialog = gtk_file_chooser_dialog_new (_("Select Files"),
278 	                                           GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (button))),
279 	                                           GTK_FILE_CHOOSER_ACTION_OPEN,
280 	                                           GTK_STOCK_CANCEL,
281 	                                           GTK_RESPONSE_CANCEL,
282 	                                           GTK_STOCK_OPEN,
283 	                                           GTK_RESPONSE_ACCEPT,
284 	                                           NULL);
285 
286 	gtk_file_chooser_set_select_multiple (GTK_FILE_CHOOSER (file_dialog),
287 	                                      TRUE);
288 
289 	if (gtk_dialog_run (GTK_DIALOG (file_dialog)) == GTK_RESPONSE_ACCEPT)
290 	{
291 		paths = gtk_file_chooser_get_filenames (GTK_FILE_CHOOSER (file_dialog));
292 		current_path = paths;
293 
294 		while (current_path)
295 		{
296 			gtk_list_store_insert_before (self->priv->list_model, &iter,
297 			                              &(self->priv->placeholder));
298 			gtk_list_store_set (self->priv->list_model, &iter,
299 			                    COL_PATH, current_path->data,
300 			                    -1);
301 
302 			g_free (current_path->data);
303 
304 			current_path = g_slist_next (current_path);
305 		}
306 
307 		g_slist_free (paths);
308 
309 
310 	}
311 
312 	gtk_widget_destroy (file_dialog);
313 }
314 
315 static void
on_copy_button_clicked(GtkButton * button,GtkTreeSelection * selection)316 on_copy_button_clicked (GtkButton *button, GtkTreeSelection *selection)
317 {
318 	GtkTreeModel *list_model;
319 	GtkTreeIter selected_iter;
320 	GtkTreeIter new_iter;
321 	gchar *path;
322 
323 	if (gtk_tree_selection_get_selected (selection, &list_model,
324 	                                     &selected_iter))
325 	{
326 		gtk_tree_model_get (list_model, &selected_iter, COL_PATH, &path, -1);
327 		gtk_list_store_insert_after (GTK_LIST_STORE (list_model), &new_iter,
328 		                             &selected_iter);
329 
330 		gtk_list_store_set (GTK_LIST_STORE (list_model), &new_iter, COL_PATH,
331 		                    path, -1);
332 
333 		g_free (path);
334 	}
335 }
336 
337 static void
on_remove_button_clicked(GtkButton * button,GtkTreeSelection * selection)338 on_remove_button_clicked (GtkButton *button, GtkTreeSelection *selection)
339 {
340 	GtkTreeIter iter;
341 	GtkTreeModel *list_model;
342 
343 	if (gtk_tree_selection_get_selected (selection, &list_model, &iter))
344 		gtk_list_store_remove (GTK_LIST_STORE (list_model), &iter);
345 }
346 
347 static void
anjuta_file_list_init(AnjutaFileList * self)348 anjuta_file_list_init (AnjutaFileList *self)
349 {
350 	GtkWidget *scrolled_window;
351 	GtkWidget *button_box;
352 	GtkWidget *clear_button;
353 	GtkTreeSelection *selection;
354 	GtkTreeViewColumn *column;
355 	GtkCellRenderer *renderer;
356 
357 
358 	/* Set properties */
359 	g_object_set (self, "orientation", GTK_ORIENTATION_VERTICAL, NULL);
360 
361 	self->priv = g_new0 (AnjutaFileListPriv, 1);
362 	self->priv->list_view = gtk_tree_view_new ();
363 	self->priv->list_model = gtk_list_store_new (NUM_COLS, G_TYPE_STRING);
364 	self->priv->add_button = gtk_button_new_from_stock (GTK_STOCK_ADD);
365 	self->priv->copy_button = gtk_button_new_from_stock (GTK_STOCK_COPY);
366 	self->priv->remove_button = gtk_button_new_from_stock (GTK_STOCK_REMOVE);
367 
368 	button_box = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
369 	scrolled_window = gtk_scrolled_window_new (NULL, NULL);
370 	clear_button = gtk_button_new_from_stock (GTK_STOCK_CLEAR);
371 
372 	gtk_widget_set_sensitive (self->priv->copy_button, FALSE);
373 	gtk_widget_set_sensitive (self->priv->remove_button, FALSE);
374 
375 	g_signal_connect_swapped (G_OBJECT (clear_button), "clicked",
376 	                          G_CALLBACK (anjuta_file_list_clear),
377 	                          self);
378 
379 	/* File list view */
380 	gtk_box_set_spacing (GTK_BOX (self), 2);
381 	gtk_box_set_homogeneous (GTK_BOX (self), FALSE);
382 
383 	gtk_tree_view_set_model (GTK_TREE_VIEW (self->priv->list_view),
384 	                         GTK_TREE_MODEL (self->priv->list_model));
385 	gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (self->priv->list_view),
386 	                                   FALSE);
387 
388 	/* Path column */
389 	column = gtk_tree_view_column_new ();
390 	renderer = gtk_cell_renderer_text_new ();
391 
392 	gtk_tree_view_column_pack_start (column, renderer, TRUE);
393 	gtk_tree_view_column_set_cell_data_func (column, renderer,
394 	                                         (GtkTreeCellDataFunc) path_cell_data_func,
395 	                                         self->priv->list_view,
396 	                                         NULL);
397 	g_object_set (G_OBJECT (renderer), "editable", TRUE, NULL);
398 	gtk_tree_view_append_column (GTK_TREE_VIEW (self->priv->list_view),
399 	                             column);
400 
401 	g_signal_connect (G_OBJECT (renderer), "editing-started",
402 	                  G_CALLBACK (on_path_renderer_editing_started),
403 	                  self->priv->list_model);
404 
405 	g_signal_connect (G_OBJECT (renderer), "edited",
406 	                  G_CALLBACK (on_path_renderer_edited),
407 	                  self);
408 
409 	/* DND */
410 	gtk_tree_view_enable_model_drag_dest (GTK_TREE_VIEW (self->priv->list_view),
411 	                                      dnd_target_entries,
412 	                                      G_N_ELEMENTS (dnd_target_entries),
413 	                                      GDK_ACTION_COPY);
414 
415 	g_signal_connect (G_OBJECT (self->priv->list_view), "drag-drop",
416 	                  G_CALLBACK (on_list_view_drag_drop),
417 	                  NULL);
418 
419 	g_signal_connect (G_OBJECT (self->priv->list_view), "drag-data-received",
420 	                  G_CALLBACK (on_list_view_drag_data_received),
421 	                  self);
422 
423 	/* Selection handling */
424 	selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self->priv->list_view));
425 
426 	gtk_tree_selection_set_select_function (selection,
427 	                                        (GtkTreeSelectionFunc) on_list_view_item_selected,
428 	                                        self, NULL);
429 
430 	g_signal_connect (G_OBJECT (self->priv->add_button), "clicked",
431 	                  G_CALLBACK (on_add_button_clicked),
432 	                  self);
433 
434 	g_signal_connect (G_OBJECT (self->priv->copy_button), "clicked",
435 	                  G_CALLBACK (on_copy_button_clicked),
436 	                  selection);
437 
438 	g_signal_connect (G_OBJECT (self->priv->remove_button), "clicked",
439 	                  G_CALLBACK (on_remove_button_clicked),
440 	                  selection);
441 
442 	gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
443 	                                                     GTK_POLICY_AUTOMATIC,
444 	                                                     GTK_POLICY_AUTOMATIC);
445 	gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window),
446 	                                     GTK_SHADOW_IN);
447 	gtk_container_add (GTK_CONTAINER (scrolled_window), self->priv->list_view);
448 	gtk_box_pack_start (GTK_BOX (self), scrolled_window, TRUE, TRUE, 0);
449 
450 	/* Button box */
451 	gtk_box_set_spacing (GTK_BOX (button_box), 5);
452 	gtk_button_box_set_layout (GTK_BUTTON_BOX (button_box),
453 	                           GTK_BUTTONBOX_START);
454 
455 	gtk_container_add (GTK_CONTAINER (button_box), self->priv->add_button);
456 	gtk_container_add (GTK_CONTAINER (button_box), self->priv->copy_button);
457 	gtk_container_add (GTK_CONTAINER (button_box), self->priv->remove_button);
458 	gtk_container_add (GTK_CONTAINER (button_box), clear_button);
459 	gtk_box_pack_start (GTK_BOX (self), button_box, FALSE, FALSE, 0);
460 
461 	anjuta_file_list_append_placeholder (self);
462 
463 	gtk_widget_show_all (GTK_WIDGET (self));
464 
465 	/* Don't show the Add button by default */
466 	gtk_widget_set_visible (self->priv->add_button, FALSE);
467 }
468 
469 static void
anjuta_file_list_finalize(GObject * object)470 anjuta_file_list_finalize (GObject *object)
471 {
472 	AnjutaFileList *self;
473 
474 	self = ANJUTA_FILE_LIST (object);
475 
476 	g_free (self->priv->relative_path);
477 	g_free (self->priv);
478 
479 	G_OBJECT_CLASS (anjuta_file_list_parent_class)->finalize (object);
480 }
481 
482 static void
anjuta_file_list_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)483 anjuta_file_list_set_property (GObject *object, guint prop_id,
484                                const GValue *value, GParamSpec *pspec)
485 {
486 	AnjutaFileList *self;
487 
488 	g_return_if_fail (ANJUTA_IS_FILE_LIST (object));
489 
490 	self = ANJUTA_FILE_LIST (object);
491 
492 	switch (prop_id)
493 	{
494 		case PROP_RELATIVE_PATH:
495 			g_free (self->priv->relative_path);
496 
497 			self->priv->relative_path = g_value_dup_string (value);
498 			break;
499 		case PROP_SHOW_ADD_BUTTON:
500 			gtk_widget_set_visible (self->priv->add_button,
501 			                        g_value_get_boolean (value));
502 			break;
503 		default:
504 			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
505 			break;
506 	}
507 }
508 
509 static void
anjuta_file_list_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)510 anjuta_file_list_get_property (GObject *object, guint prop_id, GValue *value,
511                                GParamSpec *pspec)
512 {
513 	AnjutaFileList *self;
514 
515 	g_return_if_fail (ANJUTA_IS_FILE_LIST (object));
516 
517 	self = ANJUTA_FILE_LIST (object);
518 
519 	switch (prop_id)
520 	{
521 		case PROP_RELATIVE_PATH:
522 			g_value_set_string (value, self->priv->relative_path);
523 			break;
524 		case PROP_SHOW_ADD_BUTTON:
525 			g_value_set_boolean (value,
526 			                     gtk_widget_get_visible (self->priv->add_button));
527 			break;
528 		default:
529 			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
530 			break;
531 	}
532 }
533 
534 static void
anjuta_file_list_class_init(AnjutaFileListClass * klass)535 anjuta_file_list_class_init (AnjutaFileListClass *klass)
536 {
537 	GObjectClass* object_class = G_OBJECT_CLASS (klass);
538 
539 	object_class->finalize = anjuta_file_list_finalize;
540 	object_class->set_property = anjuta_file_list_set_property;
541 	object_class->get_property = anjuta_file_list_get_property;
542 
543 	g_object_class_install_property (object_class,
544 	                                 PROP_RELATIVE_PATH,
545 	                                 g_param_spec_string ("relative-path",
546 	                                                      "relative-path",
547 	                                                      _("Path that all files in the list should be relative to"),
548 	                                                      "",
549 	                                                      G_PARAM_READWRITE));
550 
551 	g_object_class_install_property (object_class,
552 	                                 PROP_SHOW_ADD_BUTTON,
553 	                                 g_param_spec_boolean ("show-add-button",
554 	                                                       _("Show Add button"),
555 	                                                       _("Display an Add button"),
556 	                                                       FALSE,
557 	                                                       G_PARAM_READWRITE));
558 }
559 
560 
561 GtkWidget *
anjuta_file_list_new(void)562 anjuta_file_list_new (void)
563 {
564 	return g_object_new (ANJUTA_TYPE_FILE_LIST, NULL);
565 }
566 
567 static gboolean
list_model_foreach(GtkTreeModel * list_model,GtkTreePath * tree_path,GtkTreeIter * iter,GList ** list)568 list_model_foreach (GtkTreeModel *list_model, GtkTreePath *tree_path,
569                     GtkTreeIter *iter, GList **list)
570 {
571 	gchar *path;
572 
573 	gtk_tree_model_get (list_model, iter, COL_PATH, &path, -1);
574 
575 	/* Make sure not to add the placeholder to the list */
576 	if (path)
577 		*list = g_list_append (*list, path);
578 
579 	return FALSE;
580 }
581 
582 GList *
anjuta_file_list_get_paths(AnjutaFileList * self)583 anjuta_file_list_get_paths (AnjutaFileList *self)
584 {
585 	GList *list;
586 
587 	list = NULL;
588 
589 	gtk_tree_model_foreach (GTK_TREE_MODEL (self->priv->list_model),
590 	                        (GtkTreeModelForeachFunc) list_model_foreach,
591 	                        &list);
592 
593 	return list;
594 }
595 
596 void
anjuta_file_list_set_relative_path(AnjutaFileList * self,const gchar * path)597 anjuta_file_list_set_relative_path (AnjutaFileList *self, const gchar *path)
598 {
599 	g_object_set (G_OBJECT (self), "relative-path", path, NULL);
600 }
601 
602 void
anjuta_file_list_clear(AnjutaFileList * self)603 anjuta_file_list_clear (AnjutaFileList *self)
604 {
605 	gtk_list_store_clear (self->priv->list_model);
606 
607 	anjuta_file_list_append_placeholder (self);
608 }
609