1 /*
2  * * Copyright (C) 2006-2011 Anders Brander <anders@brander.dk>,
3  * * Anders Kvist <akv@lnxbx.dk> and Klaus Post <klauspost@gmail.com>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (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, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18  */
19 
20 #include <gtk/gtk.h>
21 #include "rs-dir-selector.h"
22 
23 enum {
24 	COL_NAME = 0,
25 	COL_PATH,
26 	NUM_COLS
27 };
28 
29 struct _RSDirSelector {
30 	GtkScrolledWindow parent;
31 	GtkWidget *view;
32 	gchar *root;
33 };
34 
35 G_DEFINE_TYPE (RSDirSelector, rs_dir_selector, GTK_TYPE_SCROLLED_WINDOW);
36 
37 static void realize(GtkWidget *widget, gpointer data);
38 static void dir_selector_add_element(GtkTreeStore *treestore, GtkTreeIter *iter, const gchar *name, const gchar *path);
39 static void row_activated(GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *col, gpointer user_data);
40 static void row_expanded(GtkTreeView *view, GtkTreeIter *iter, GtkTreePath *path, gpointer user_data);
41 static void row_collapsed(GtkTreeView *view, GtkTreeIter *iter, GtkTreePath *path, gpointer user_data);
42 static void move_cursor(GtkTreeView *view, GtkMovementStep movement, gint direction, gpointer user_data);
43 static GtkTreeModel *create_and_fill_model (const gchar *root);
44 static gboolean directory_contains_directories(const gchar *filepath);
45 
46 enum {
47 	DIRECTORY_ACTIVATED_SIGNAL,
48 	LAST_SIGNAL
49 };
50 
51 static guint signals[LAST_SIGNAL] = { 0 };
52 
53 /**
54  * Class initializer
55  */
56 static void
rs_dir_selector_class_init(RSDirSelectorClass * klass)57 rs_dir_selector_class_init(RSDirSelectorClass *klass)
58 {
59 	GtkWidgetClass *widget_class;
60 	GtkObjectClass *object_class;
61 	widget_class = GTK_WIDGET_CLASS(klass);
62 	object_class = GTK_OBJECT_CLASS(klass);
63 
64 	signals[DIRECTORY_ACTIVATED_SIGNAL] = g_signal_new ("directory-activated",
65 		G_TYPE_FROM_CLASS (klass),
66 		G_SIGNAL_RUN_FIRST,
67 		0,
68 		NULL,
69 		NULL,
70 		g_cclosure_marshal_VOID__STRING,
71 		G_TYPE_NONE,
72 		1,
73 		G_TYPE_STRING);
74 }
75 
76 /**
77  * Instance initialization
78  */
79 static void
rs_dir_selector_init(RSDirSelector * selector)80 rs_dir_selector_init(RSDirSelector *selector)
81 {
82 	GtkTreeViewColumn *col;
83 	GtkCellRenderer *renderer;
84 	GtkScrolledWindow *scroller = GTK_SCROLLED_WINDOW(selector);
85 
86 	g_object_set (G_OBJECT (selector), "hadjustment", NULL, "vadjustment", NULL, NULL);
87 	gtk_scrolled_window_set_policy (scroller, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
88 
89 	selector->view = gtk_tree_view_new();
90 	col = gtk_tree_view_column_new();
91 	gtk_tree_view_append_column(GTK_TREE_VIEW(selector->view), col);
92 	renderer = gtk_cell_renderer_text_new();
93 	gtk_tree_view_column_pack_start(col, renderer, TRUE);
94 	gtk_tree_view_column_add_attribute (col, renderer, "text",
95 										COL_NAME);
96 
97 	gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(selector->view), FALSE);
98 
99 	g_signal_connect(selector->view, "row-activated", G_CALLBACK(row_activated), selector);
100 	g_signal_connect(selector->view, "row-expanded", G_CALLBACK(row_expanded), NULL);
101 	g_signal_connect(selector->view, "row-collapsed", G_CALLBACK(row_collapsed), NULL);
102 	g_signal_connect(selector->view, "move-cursor", G_CALLBACK(move_cursor), NULL);
103 
104 	gtk_tree_selection_set_mode(gtk_tree_view_get_selection(GTK_TREE_VIEW(selector->view)),
105 								GTK_SELECTION_SINGLE);
106 
107 	rs_dir_selector_set_root(selector, "/");
108 
109 	gtk_signal_connect(GTK_OBJECT(selector), "realize", G_CALLBACK(realize), NULL);
110 
111 	gtk_container_add (GTK_CONTAINER (scroller), selector->view);
112 	return;
113 }
114 
115 static void
realize(GtkWidget * widget,gpointer data)116 realize(GtkWidget *widget, gpointer data)
117 {
118 	RSDirSelector *selector = RS_DIR_SELECTOR(widget);
119 	GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(selector->view));
120 
121 	if (!selection)
122 		return;
123 
124 	if (gtk_tree_selection_count_selected_rows(selection) == 1)
125 	{
126 		GList *selected = gtk_tree_selection_get_selected_rows(selection, NULL);
127 
128 		gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(selector->view), g_list_nth_data(selected, 0), NULL, TRUE, 0.2, 0.0);
129 
130 		g_list_foreach (selected, (GFunc) gtk_tree_path_free, NULL);
131 		g_list_free (selected);
132 	}
133 }
134 
135 /**
136  * Adds an element to treestore
137  * @param treestore A GtkTreeStore
138  * @param iter A GtkTreeIter
139  * @param name A gchar
140  * @param path A gchar
141  */
142 static void
dir_selector_add_element(GtkTreeStore * treestore,GtkTreeIter * iter,const gchar * name,const gchar * path)143 dir_selector_add_element(GtkTreeStore *treestore, GtkTreeIter *iter, const gchar *name, const gchar *path)
144 {
145 	GtkTreeIter this, child;
146 
147 	gtk_tree_store_append(treestore, &this, iter);
148 	gtk_tree_store_set(treestore, &this,
149 					   COL_NAME, name,
150 					   COL_PATH, path,
151 					   -1);
152 	if (path && directory_contains_directories(path))
153 	{
154 		gtk_tree_store_append(treestore, &child, &this);
155 		gtk_tree_store_set(treestore, &child,
156 		-1);
157 	}
158 }
159 
160 /**
161  * Callback after activating a row
162  * @param view A GtkTreeView
163  * @param path A GtkTreePath
164  * @param col A GtkTreeViewColumn
165  * @param user_data A gpointer
166  */
167 static void
row_activated(GtkTreeView * view,GtkTreePath * path,GtkTreeViewColumn * col,gpointer user_data)168 row_activated(GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *col, gpointer user_data)
169 {
170 	GtkTreeModel *model;
171 	GtkTreeIter iter;
172 	gchar *filepath;
173 
174 	model = gtk_tree_view_get_model(view);
175 	gtk_tree_model_get_iter(model, &iter, path);
176 	gtk_tree_model_get(GTK_TREE_MODEL(model), &iter,
177 					   COL_PATH, &filepath,
178 					   -1);
179 	g_signal_emit (G_OBJECT (user_data), signals[DIRECTORY_ACTIVATED_SIGNAL], 0, filepath);
180 }
181 
182 /**
183  * Callback after expanding a row
184  * @param view A GtkTreeView
185  * @param iter A GtkTreeIter
186  * @param path A GtkTreePath
187  * @param user_data A gpointer
188  */
189 static void
row_expanded(GtkTreeView * view,GtkTreeIter * iter,GtkTreePath * path,gpointer user_data)190 row_expanded(GtkTreeView *view, GtkTreeIter *iter, GtkTreePath *path, gpointer user_data)
191 {
192 	GtkTreeModel *model;
193 	GtkTreeIter empty;
194 	gchar *filepath;
195 	gchar *file;
196 	GDir *dir;
197 	GString *gs = NULL;
198 
199 	/* Set busy cursor */
200 	GdkCursor* cursor = gdk_cursor_new(GDK_WATCH);
201 	gdk_window_set_cursor(gtk_widget_get_toplevel(GTK_WIDGET(view))->window, cursor);
202 	gdk_cursor_unref(cursor);
203 	gdk_flush();
204 
205 	model = gtk_tree_view_get_model(view);
206 	gtk_tree_model_iter_children(GTK_TREE_MODEL(model),
207 								 &empty, iter);
208 	gtk_tree_model_get(GTK_TREE_MODEL(model), iter,
209 					   COL_PATH, &filepath,
210 					   -1);
211 
212 	dir = g_dir_open(filepath, 0, NULL);
213 
214 	if (dir)
215 	{
216 		while ((file = (gchar *) g_dir_read_name(dir)))
217 		{
218 			gs = g_string_new(filepath);
219 			g_string_append(gs, file);
220 			g_string_append(gs, "/");
221 			if (g_file_test(gs->str, G_FILE_TEST_IS_DIR))
222 			{
223 				if (file[0] == '.')
224 				{
225 					/* Fixme: If hidden files should be shown too */
226 				}
227 				else
228 				{
229 					dir_selector_add_element(GTK_TREE_STORE(model), iter, file, gs->str);
230 				}
231 			}
232 			g_string_free(gs, TRUE);
233 		}
234 		g_dir_close(dir);
235 		g_free(filepath);
236 	}
237 	gdk_window_set_cursor(gtk_widget_get_toplevel(GTK_WIDGET(view))->window, NULL);
238 	gtk_tree_store_remove(GTK_TREE_STORE(model), &empty);
239 }
240 
241 /**
242  * Callback after collapsing a row
243  * @param view A GtkTreeView
244  * @param iter A GtkTreeIter
245  * @param path A GtkTreePath
246  * @param user_data A gpointer
247  */
248 static void
row_collapsed(GtkTreeView * view,GtkTreeIter * iter,GtkTreePath * path,gpointer user_data)249 row_collapsed(GtkTreeView *view, GtkTreeIter *iter, GtkTreePath *path, gpointer user_data)
250 {
251 	GtkTreeModel *model;
252 	GtkTreeIter child;
253 
254 	model = gtk_tree_view_get_model(view);
255 	while (gtk_tree_model_iter_children(GTK_TREE_MODEL(model), &child, iter))
256 	{
257 		gtk_tree_store_remove(GTK_TREE_STORE(model), &child);
258 	}
259 	dir_selector_add_element(GTK_TREE_STORE(model), iter, NULL, NULL);
260 }
261 
262 /**
263  * Callback after moving cursor
264  * @param view A GtkTreeView
265  * @param iter A GtkMovementStep
266  * @param path A gint
267  * @param user_data A gpointer
268  */
269 static void
move_cursor(GtkTreeView * view,GtkMovementStep movement,gint direction,gpointer user_data)270 move_cursor(GtkTreeView *view, GtkMovementStep movement, gint direction, gpointer user_data)
271 {
272 	if (movement == GTK_MOVEMENT_VISUAL_POSITIONS)
273 	{
274 		GtkTreePath *path;
275 		gtk_tree_view_get_cursor(view, &path, NULL);
276 		if (direction == 1) /* RIGHT */
277 		{
278 			gtk_tree_view_expand_row(view, path, FALSE);
279 		}
280 		else if (direction == -1) /* LEFT */
281 		{
282 			gtk_tree_view_collapse_row(view, path);
283 		}
284 		gtk_tree_path_free(path);
285 	}
286 }
287 
288 /**
289  * Creates a GtkTreeStore and fills it with data
290  * @param root A gchar
291  * @return A GtkTreeModel
292  */
293 static GtkTreeModel *
create_and_fill_model(const gchar * root)294 create_and_fill_model (const gchar *root)
295 {
296 	GtkTreeStore *treestore;
297 
298 	treestore = gtk_tree_store_new(NUM_COLS, G_TYPE_STRING, G_TYPE_STRING);
299 	dir_selector_add_element(treestore, NULL, root, root);
300 	return GTK_TREE_MODEL(treestore);
301 }
302 
303 /**
304  * Creates a GtkWidget
305  * @return A GtkWidget
306  */
307 GtkWidget *
rs_dir_selector_new()308 rs_dir_selector_new()
309 {
310 	return g_object_new (RS_DIR_SELECTOR_TYPE_WIDGET, NULL);
311 }
312 
313 /**
314  * Sets root
315  * @param root A gchar
316  */
317 void
rs_dir_selector_set_root(RSDirSelector * selector,const gchar * root)318 rs_dir_selector_set_root(RSDirSelector *selector, const gchar *root)
319 {
320 	GtkTreeModel *model;
321 	GtkTreeSortable *sortable;
322 
323 	model = create_and_fill_model(root);
324 
325 	sortable = GTK_TREE_SORTABLE(model);
326 	gtk_tree_sortable_set_sort_column_id(sortable,
327 										 COL_NAME,
328 										 GTK_SORT_ASCENDING);
329 
330 	gtk_tree_view_set_model(GTK_TREE_VIEW(selector->view), model);
331 
332 	g_object_unref(model); /* destroy model automatically with view */
333 }
334 
335 /**
336  * Expands to path
337  * @param path to expand
338  */
339 void
rs_dir_selector_expand_path(RSDirSelector * selector,const gchar * expand)340 rs_dir_selector_expand_path(RSDirSelector *selector, const gchar *expand)
341 {
342 	GtkTreeView *view = GTK_TREE_VIEW(selector->view);
343 	GtkTreeModel *model = gtk_tree_view_get_model(view);
344 	GtkTreePath *path = gtk_tree_path_new_first();
345 	GtkTreeIter iter;
346 	gchar *filepath = NULL;
347 	GString *gs;
348 
349 	if (g_path_is_absolute(expand))
350 	{
351 		gs = g_string_new(expand);
352 	}
353 	else
354 	{
355 		gs = g_string_new(g_get_current_dir());
356 		g_string_append(gs, G_DIR_SEPARATOR_S);
357 		g_string_append(gs, expand);
358 	}
359 
360 	g_string_append(gs, G_DIR_SEPARATOR_S);
361 
362 	while (gtk_tree_model_get_iter(model, &iter, path))
363 	{
364 		gtk_tree_model_get(model, &iter, COL_PATH, &filepath, -1);
365 
366 		if (filepath && g_str_has_prefix(gs->str, filepath))
367 		{
368 			gtk_tree_view_expand_row(GTK_TREE_VIEW(view), path, FALSE);
369 			gtk_tree_path_down(path);
370 		}
371 		else
372 		{
373 			gtk_tree_path_next(path);
374 		}
375 	}
376 
377 	g_string_free(gs, TRUE);
378 
379 	if (GTK_WIDGET_REALIZED(GTK_WIDGET(selector)))
380 		gtk_tree_view_scroll_to_cell(view, path, NULL, TRUE, 0.2, 0.0);
381 	else
382 	{
383 		/* Save this, realize() will catch it later */
384 		GtkTreeSelection *selection = gtk_tree_view_get_selection(view);
385 		if (gtk_tree_model_get_iter(model, &iter, path))
386 			gtk_tree_selection_select_iter(selection, &iter);
387 	}
388 
389 	gtk_tree_path_free(path);
390 }
391 
392 static gboolean
directory_contains_directories(const gchar * filepath)393 directory_contains_directories(const gchar *filepath)
394 {
395 	GDir *dir;
396 	GString *gs = NULL;
397 	gchar *file;
398 
399 	dir = g_dir_open(filepath, 0, NULL);
400 
401 	if (dir)
402 	{
403 		while ((file = (gchar *) g_dir_read_name(dir)))
404 		{
405 			gs = g_string_new(filepath);
406 			g_string_append(gs, file);
407 			g_string_append(gs, "/");
408 
409 			if (file[0] == '.')
410 			{
411 				/* Fixme: If hidden files should be shown too */
412 			}
413 			else
414 			{
415 				if (g_file_test(gs->str, G_FILE_TEST_IS_DIR))
416 				{
417 					g_dir_close(dir);
418 					g_string_free(gs, TRUE);
419 					return TRUE;
420 				}
421 			}
422 			g_string_free(gs, TRUE);
423 		}
424 		g_dir_close(dir);
425 	}
426 
427 	return FALSE;
428 }
429