Enabling multithreaded compilation.
[debian/lxpanel.git] / src / plugins / dirmenu.c
1 /**
2 * Copyright (c) 2006 LxDE Developers, see the file AUTHORS for details.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software Foundation,
16 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 */
18
19 #include <stdlib.h>
20 #include <unistd.h>
21
22 #include <gdk-pixbuf/gdk-pixbuf.h>
23 #include <glib/gi18n.h>
24 #include <string.h>
25
26 #include "panel.h"
27 #include "misc.h"
28 #include "plugin.h"
29 #include "dbg.h"
30
31 /* Temporary for sort of directory names. */
32 typedef struct _directory_name {
33 struct _directory_name * flink;
34 char * directory_name;
35 char * directory_name_collate_key;
36 } DirectoryName;
37
38 /* Private context for directory menu plugin. */
39 typedef struct {
40 Plugin * plugin; /* Back pointer to plugin */
41 char * image; /* Icon for top level widget */
42 char * path; /* Top level path for widget */
43 char * name; /* User's label for widget */
44 GdkPixbuf * folder_icon; /* Icon for folders */
45 } DirMenuPlugin;
46
47 static void dirmenu_open_in_file_manager(Plugin * p, const char * path);
48 static void dirmenu_open_in_terminal(Plugin * p, const char * path);
49 static void dirmenu_menuitem_open_directory(GtkWidget * item, Plugin * p);
50 static void dirmenu_menuitem_open_in_terminal(GtkWidget * item, Plugin * p);
51 static void dirmenu_menuitem_select(GtkMenuItem * item, Plugin * p);
52 static void dirmenu_menuitem_deselect(GtkMenuItem * item, Plugin * p);
53 void dirmenu_menu_selection_done(GtkWidget * menu, Plugin * p);
54 static void dirmenu_popup_set_position(GtkWidget * menu, gint * px, gint * py, gboolean * push_in, Plugin * p);
55 static GtkWidget * dirmenu_create_menu(Plugin * p, const char * path, gboolean open_at_top);
56 static void dirmenu_show_menu(GtkWidget * widget, Plugin * p, int btn, guint32 time);
57 static gboolean dirmenu_button_press_event(GtkWidget * widget, GdkEventButton * event, Plugin * p);
58 static int dirmenu_constructor(Plugin * p, char ** fp);
59 static void dirmenu_destructor(Plugin * p);
60 static void dirmenu_apply_configuration_to_children(GtkWidget * w, DirMenuPlugin * dm);
61 static void dirmenu_apply_configuration(Plugin * p);
62 static void dirmenu_configure(Plugin * p, GtkWindow * parent);
63 static void dirmenu_save_configuration(Plugin * p, FILE * fp);
64 static void dirmenu_panel_configuration_changed(Plugin * p);
65
66 /* Open a specified path in a file manager. */
67 static void dirmenu_open_in_file_manager(Plugin * p, const char * path)
68 {
69 char * quote = g_shell_quote(path);
70 const char * fm = lxpanel_get_file_manager();
71 char * cmd = ((strstr(fm, "%s") != NULL) ? g_strdup_printf(fm, quote) : g_strdup_printf("%s %s", fm, quote));
72 g_free(quote);
73 g_spawn_command_line_async(cmd, NULL);
74 g_free(cmd);
75 }
76
77 /* Open a specified path in a terminal. */
78 static void dirmenu_open_in_terminal(Plugin * p, const char * path)
79 {
80 const char * term = lxpanel_get_terminal();
81 char * argv[2];
82 char * sp = strchr(term, ' ');
83 argv[0] = ((sp != NULL) ? g_strndup(term, sp - term) : (char *) term);
84 argv[1] = NULL;
85 g_spawn_async(path, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL);
86 if (argv[0] != term)
87 g_free(argv[0]);
88 }
89
90 /* Handler for activate event on popup Open menu item. */
91 static void dirmenu_menuitem_open_directory(GtkWidget * item, Plugin * p)
92 {
93 dirmenu_open_in_file_manager(p, g_object_get_data(G_OBJECT(gtk_widget_get_parent(item)), "path"));
94 }
95
96 /* Handler for activate event on popup Open In Terminal menu item. */
97 static void dirmenu_menuitem_open_in_terminal(GtkWidget * item, Plugin * p)
98 {
99 dirmenu_open_in_terminal(p, g_object_get_data(G_OBJECT(gtk_widget_get_parent(item)), "path"));
100 }
101
102 /* Handler for select event on popup menu item. */
103 static void dirmenu_menuitem_select(GtkMenuItem * item, Plugin * p)
104 {
105 GtkWidget * sub = gtk_menu_item_get_submenu(item);
106 if (sub != NULL)
107 {
108 /* On first reference, populate the submenu using the parent directory and the item directory name. */
109 GtkMenu * parent = GTK_MENU(gtk_widget_get_parent(GTK_WIDGET(item)));
110 char * path = (char *) g_object_get_data(G_OBJECT(sub), "path");
111 if (path == NULL)
112 {
113 path = g_build_filename(
114 (char *) g_object_get_data(G_OBJECT(parent), "path"),
115 (char *) g_object_get_data(G_OBJECT(item), "name"),
116 NULL);
117 sub = dirmenu_create_menu(p, path, TRUE);
118 g_free(path);
119 gtk_menu_item_set_submenu(item, sub);
120 }
121 }
122 }
123
124 /* Handler for deselect event on popup menu item. */
125 static void dirmenu_menuitem_deselect(GtkMenuItem * item, Plugin * p)
126 {
127 /* Delete old menu on deselect to save resource. */
128 gtk_menu_item_set_submenu(item, gtk_menu_new());
129 }
130
131 /* Handler for selection-done event on popup menu. */
132 void dirmenu_menu_selection_done(GtkWidget * menu, Plugin * p)
133 {
134 gtk_widget_destroy(menu);
135 }
136
137 /* Position-calculation callback for popup menu. */
138 static void dirmenu_popup_set_position(GtkWidget * menu, gint * px, gint * py, gboolean * push_in, Plugin * p)
139 {
140 /* Get the allocation of the popup menu. */
141 GtkRequisition popup_req;
142 gtk_widget_size_request(menu, &popup_req);
143
144 /* Determine the coordinates. */
145 plugin_popup_set_position_helper(p, p->pwid, menu, &popup_req, px, py);
146 *push_in = TRUE;
147 }
148
149 /* Create a menu populated with all subdirectories. */
150 static GtkWidget * dirmenu_create_menu(Plugin * p, const char * path, gboolean open_at_top)
151 {
152 DirMenuPlugin * dm = (DirMenuPlugin *) p->priv;
153
154 /* Create a menu. */
155 GtkWidget * menu = gtk_menu_new();
156
157 if (dm->folder_icon == NULL)
158 {
159 int w;
160 int h;
161 gtk_icon_size_lookup_for_settings(gtk_widget_get_settings(menu), GTK_ICON_SIZE_MENU, &w, &h);
162 dm->folder_icon = gtk_icon_theme_load_icon(
163 p->panel->icon_theme,
164 "gnome-fs-directory", MAX(w, h), 0, NULL);
165 if (dm->folder_icon == NULL)
166 dm->folder_icon = gtk_widget_render_icon(menu, GTK_STOCK_DIRECTORY, GTK_ICON_SIZE_MENU, NULL);
167 }
168
169 g_object_set_data_full(G_OBJECT(menu), "path", g_strdup(path), g_free);
170
171 /* Scan the specified directory to populate the menu with its subdirectories. */
172 DirectoryName * dir_list = NULL;
173 GDir * dir = g_dir_open(path, 0, NULL);
174 if (dir != NULL)
175 {
176 const char * name;
177 while ((name = g_dir_read_name(dir)) != NULL) /* Memory owned by glib */
178 {
179 /* Omit hidden files. */
180 if (name[0] != '.')
181 {
182 char * full = g_build_filename(path, name, NULL);
183 if (g_file_test(full, G_FILE_TEST_IS_DIR))
184 {
185 /* Convert name to UTF-8 and to the collation key. */
186 char * directory_name = g_filename_display_name(name);
187 char * directory_name_collate_key = g_utf8_collate_key(directory_name, -1);
188
189 /* Locate insertion point. */
190 DirectoryName * dir_pred = NULL;
191 DirectoryName * dir_cursor;
192 for (dir_cursor = dir_list; dir_cursor != NULL; dir_pred = dir_cursor, dir_cursor = dir_cursor->flink)
193 {
194 if (strcmp(directory_name_collate_key, dir_cursor->directory_name_collate_key) <= 0)
195 break;
196 }
197
198 /* Allocate and initialize sorted directory name entry. */
199 dir_cursor = g_new0(DirectoryName, 1);
200 dir_cursor->directory_name = directory_name;
201 dir_cursor->directory_name_collate_key = directory_name_collate_key;
202 if (dir_pred == NULL)
203 {
204 dir_cursor->flink = dir_list;
205 dir_list = dir_cursor;
206 }
207 else
208 {
209 dir_cursor->flink = dir_pred->flink;
210 dir_pred->flink = dir_cursor;
211 }
212 }
213 g_free(full);
214 }
215 }
216 g_dir_close(dir);
217 }
218
219 /* The sorted directory name list is complete. Loop to create the menu. */
220 DirectoryName * dir_cursor;
221 while ((dir_cursor = dir_list) != NULL)
222 {
223 /* Create and initialize menu item. */
224 GtkWidget * item = gtk_image_menu_item_new_with_label(dir_cursor->directory_name);
225 gtk_image_menu_item_set_image(
226 GTK_IMAGE_MENU_ITEM(item),
227 gtk_image_new_from_stock(GTK_STOCK_DIRECTORY, GTK_ICON_SIZE_MENU));
228 GtkWidget * dummy = gtk_menu_new();
229 gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), dummy);
230 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
231
232 /* Unlink and free sorted directory name element, but reuse the directory name string. */
233 dir_list = dir_cursor->flink;
234 g_object_set_data_full(G_OBJECT(item), "name", dir_cursor->directory_name, g_free);
235 g_free(dir_cursor->directory_name_collate_key);
236 g_free(dir_cursor);
237
238 /* Connect signals. */
239 g_signal_connect(G_OBJECT(item), "select", G_CALLBACK(dirmenu_menuitem_select), p);
240 g_signal_connect(G_OBJECT(item), "deselect", G_CALLBACK(dirmenu_menuitem_deselect), p);
241 }
242
243 /* Create "Open" and "Open in Terminal" items. */
244 GtkWidget * item = gtk_image_menu_item_new_from_stock( GTK_STOCK_OPEN, NULL );
245 g_signal_connect(item, "activate", G_CALLBACK(dirmenu_menuitem_open_directory), p);
246 GtkWidget * term = gtk_menu_item_new_with_mnemonic( _("Open in _Terminal") );
247 g_signal_connect(term, "activate", G_CALLBACK(dirmenu_menuitem_open_in_terminal), p);
248
249 /* Insert or append based on caller's preference. */
250 if (open_at_top)
251 {
252 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new(), 0);
253 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), term, 0);
254 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), item, 0);
255 }
256 else {
257 gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
258 gtk_menu_shell_append(GTK_MENU_SHELL(menu), term);
259 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
260 }
261
262 /* Show the menu and return. */
263 gtk_widget_show_all(menu);
264 return menu;
265 }
266
267 /* Show a menu of subdirectories. */
268 static void dirmenu_show_menu(GtkWidget * widget, Plugin * p, int btn, guint32 time)
269 {
270 DirMenuPlugin * dm = (DirMenuPlugin *) p->priv;
271
272 /* Create a menu populated with all subdirectories. */
273 GtkWidget * menu = dirmenu_create_menu(
274 p,
275 ((dm->path != NULL) ? expand_tilda(dm->path) : g_get_home_dir()),
276 FALSE);
277 g_signal_connect(menu, "selection-done", G_CALLBACK(dirmenu_menu_selection_done), NULL);
278
279 /* Show the menu. Use a positioning function to get it placed next to the top level widget. */
280 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, (GtkMenuPositionFunc) dirmenu_popup_set_position, p, btn, time);
281 }
282
283 /* Handler for button-press-event on top level widget. */
284 static gboolean dirmenu_button_press_event(GtkWidget * widget, GdkEventButton * event, Plugin * p)
285 {
286 DirMenuPlugin * dm = (DirMenuPlugin *) p->priv;
287
288 /* Standard left-click handling. */
289 if (plugin_button_press_event(widget, event, p))
290 return TRUE;
291
292 if (event->button == 1)
293 {
294 dirmenu_show_menu(widget, p, event->button, event->time);
295 }
296 else
297 {
298 dirmenu_open_in_terminal(p, ((dm->path != NULL) ? expand_tilda(dm->path) : g_get_home_dir()));
299 }
300 return TRUE;
301 }
302
303 /* Plugin constructor. */
304 static int dirmenu_constructor(Plugin * p, char ** fp)
305 {
306 /* Allocate and initialize plugin context and set into Plugin private data pointer. */
307 DirMenuPlugin * dm = g_new0(DirMenuPlugin, 1);
308 dm->plugin = p;
309 p->priv = dm;
310
311 /* Load parameters from the configuration file. */
312 line s;
313 s.len = 256;
314 if (fp != NULL)
315 {
316 while (lxpanel_get_line(fp, &s) != LINE_BLOCK_END)
317 {
318 if (s.type == LINE_NONE)
319 {
320 ERR( "dirmenu: illegal token %s\n", s.str);
321 return 0;
322 }
323 if (s.type == LINE_VAR)
324 {
325 if (g_ascii_strcasecmp(s.t[0], "image") == 0)
326 dm->image = g_strdup(s.t[1]);
327 else if (g_ascii_strcasecmp(s.t[0], "path") == 0)
328 dm->path = g_strdup(s.t[1]);
329 else if (g_ascii_strcasecmp(s.t[0], "name") == 0)
330 dm->name = g_strdup( s.t[1] );
331 else
332 ERR( "dirmenu: unknown var %s\n", s.t[0]);
333 }
334 else
335 {
336 ERR( "dirmenu: illegal in this context %s\n", s.str);
337 return 0;
338 }
339 }
340 }
341
342 /* Allocate top level widget and set into Plugin widget pointer.
343 * It is not known why, but the button text will not draw if it is edited from empty to non-empty
344 * unless this strategy of initializing it with a non-empty value first is followed. */
345 p->pwid = fb_button_new_from_file_with_label(
346 ((dm->image != NULL) ? dm->image : "file-manager"),
347 p->panel->icon_size, p->panel->icon_size, PANEL_ICON_HIGHLIGHT, TRUE, p->panel, "Temp");
348 gtk_container_set_border_width(GTK_CONTAINER(p->pwid), 0);
349 g_signal_connect(p->pwid, "button_press_event", G_CALLBACK(dirmenu_button_press_event), p);
350
351 /* Initialize the widget. */
352 dirmenu_apply_configuration(p);
353
354 /* Show the widget and return. */
355 gtk_widget_show(p->pwid);
356 return 1;
357 }
358
359 /* Plugin destructor. */
360 static void dirmenu_destructor(Plugin * p)
361 {
362 DirMenuPlugin * dm = (DirMenuPlugin *) p->priv;
363
364 /* Release a reference on the folder icon if held. */
365 if (dm->folder_icon)
366 g_object_unref(dm->folder_icon);
367
368 /* Deallocate all memory. */
369 g_free(dm->image);
370 g_free(dm->path);
371 g_free(dm->name);
372 g_free(dm);
373 }
374
375 /* Recursively apply a configuration change. */
376 static void dirmenu_apply_configuration_to_children(GtkWidget * w, DirMenuPlugin * dm)
377 {
378 if (GTK_IS_CONTAINER(w))
379 gtk_container_foreach(GTK_CONTAINER(w), (GtkCallback) dirmenu_apply_configuration_to_children, (gpointer) dm);
380 else if (GTK_IS_LABEL(w))
381 {
382 if (dm->name == NULL)
383 gtk_label_set_text(GTK_LABEL(w), NULL);
384 else
385 panel_draw_label_text(dm->plugin->panel, w, dm->name, FALSE, TRUE);
386 }
387 }
388
389 /* Callback when the configuration dialog has recorded a configuration change. */
390 static void dirmenu_apply_configuration(Plugin * p)
391 {
392 DirMenuPlugin * dm = (DirMenuPlugin *) p->priv;
393
394 fb_button_set_from_file(p->pwid,
395 ((dm->image != NULL) ? dm->image : "file-manager"),
396 ((dm->image != NULL) ? -1 : p->panel->icon_size), p->panel->icon_size, TRUE);
397
398 gtk_widget_set_tooltip_text(p->pwid, ((dm->path != NULL) ? expand_tilda(dm->path) : g_get_home_dir()));
399 gtk_container_foreach(GTK_CONTAINER(p->pwid), (GtkCallback) dirmenu_apply_configuration_to_children, (gpointer) dm);
400 }
401
402 /* Callback when the configuration dialog is to be shown. */
403 static void dirmenu_configure(Plugin * p, GtkWindow * parent)
404 {
405
406 DirMenuPlugin * dm = (DirMenuPlugin *) p->priv;
407 GtkWidget * dlg = create_generic_config_dlg(
408 _(p->class->name),
409 GTK_WIDGET(parent),
410 (GSourceFunc) dirmenu_apply_configuration, (gpointer) p,
411 _("Directory"), &dm->path, CONF_TYPE_DIRECTORY_ENTRY,
412 _("Label"), &dm->name, CONF_TYPE_STR,
413 _("Icon"), &dm->image, CONF_TYPE_FILE_ENTRY,
414 NULL);
415 gtk_window_present(GTK_WINDOW(dlg));
416 }
417
418 /* Callback when the configuration is to be saved. */
419 static void dirmenu_save_configuration(Plugin * p, FILE * fp)
420 {
421 DirMenuPlugin * dm = (DirMenuPlugin *) p->priv;
422 lxpanel_put_str(fp, "path", dm->path);
423 lxpanel_put_str(fp, "name", dm->name);
424 lxpanel_put_str(fp, "image", dm->image);
425 }
426
427 /* Callback when panel configuration changes. */
428 static void dirmenu_panel_configuration_changed(Plugin * p)
429 {
430 DirMenuPlugin * dm = (DirMenuPlugin *) p->priv;
431 fb_button_set_from_file(p->pwid,
432 ((dm->image != NULL) ? dm->image : "file-manager"),
433 p->panel->icon_size, p->panel->icon_size, TRUE);
434 dirmenu_apply_configuration(p);
435 }
436
437 /* Plugin descriptor. */
438 PluginClass dirmenu_plugin_class = {
439
440 PLUGINCLASS_VERSIONING,
441
442 type : "dirmenu",
443 name : N_("Directory Menu"),
444 version: "1.0",
445 description : N_("Browse directory tree via menu (Author: PCMan)"),
446
447 constructor : dirmenu_constructor,
448 destructor : dirmenu_destructor,
449 config : dirmenu_configure,
450 save : dirmenu_save_configuration,
451 panel_configuration_changed : dirmenu_panel_configuration_changed
452
453 };