Merge branch 'use-libfm'
[lxde/lxpanel.git] / src / plugins / dirmenu.c
1 /**
2 * Copyright (c) 2006-2014 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 <glib/gi18n.h>
23 #include <libfm/fm-gtk.h>
24 #include <string.h>
25
26 #include "misc.h"
27 #include "plugin.h"
28
29 /* Temporary for sort of directory names. */
30 typedef struct _directory_name {
31 struct _directory_name * flink;
32 char * directory_name;
33 char * directory_name_collate_key;
34 } DirectoryName;
35
36 /* Private context for directory menu plugin. */
37 typedef struct {
38 Panel * panel; /* The panel and settings are required to apply config */
39 config_setting_t * settings;
40 char * image; /* Icon for top level widget */
41 char * path; /* Top level path for widget */
42 char * name; /* User's label for widget */
43 GdkPixbuf * folder_icon; /* Icon for folders */
44 } DirMenuPlugin;
45
46 static GtkWidget * dirmenu_create_menu(DirMenuPlugin * dm, const char * path, gboolean open_at_top);
47 static void dirmenu_destructor(gpointer user_data);
48 static gboolean dirmenu_apply_configuration(gpointer user_data);
49
50
51 /* Handler for activate event on popup Open menu item. */
52 static void dirmenu_menuitem_open_directory(GtkWidget * item, DirMenuPlugin * dm)
53 {
54 FmPath *path = fm_path_new_for_str(g_object_get_data(G_OBJECT(gtk_widget_get_parent(item)), "path"));
55 lxpanel_launch_path(dm->panel, path);
56 fm_path_unref(path);
57 }
58
59 /* Handler for activate event on popup Open In Terminal menu item. */
60 static void dirmenu_menuitem_open_in_terminal(GtkWidget * item, DirMenuPlugin * dm)
61 {
62 fm_terminal_launch(g_object_get_data(G_OBJECT(gtk_widget_get_parent(item)), "path"), NULL);
63 }
64
65 /* Handler for select event on popup menu item. */
66 static void dirmenu_menuitem_select(GtkMenuItem * item, DirMenuPlugin * dm)
67 {
68 GtkWidget * sub = gtk_menu_item_get_submenu(item);
69 if (sub != NULL)
70 {
71 /* On first reference, populate the submenu using the parent directory and the item directory name. */
72 GtkMenu * parent = GTK_MENU(gtk_widget_get_parent(GTK_WIDGET(item)));
73 char * path = (char *) g_object_get_data(G_OBJECT(sub), "path");
74 if (path == NULL)
75 {
76 path = g_build_filename(
77 (char *) g_object_get_data(G_OBJECT(parent), "path"),
78 (char *) g_object_get_data(G_OBJECT(item), "name"),
79 NULL);
80 sub = dirmenu_create_menu(dm, path, TRUE);
81 g_free(path);
82 gtk_menu_item_set_submenu(item, sub);
83 }
84 }
85 }
86
87 /* Handler for deselect event on popup menu item. */
88 static void dirmenu_menuitem_deselect(GtkMenuItem * item, DirMenuPlugin * dm)
89 {
90 /* Delete old menu on deselect to save resource. */
91 gtk_menu_item_set_submenu(item, gtk_menu_new());
92 }
93
94 /* Handler for selection-done event on popup menu. */
95 static void dirmenu_menu_selection_done(GtkWidget * menu, DirMenuPlugin * dm)
96 {
97 gtk_widget_destroy(menu);
98 }
99
100 /* Position-calculation callback for popup menu. */
101 static void dirmenu_popup_set_position(GtkWidget * menu, gint * px, gint * py, gboolean * push_in, GtkWidget * p)
102 {
103 DirMenuPlugin * dm = lxpanel_plugin_get_data(p);
104 /* Get the allocation of the popup menu. */
105 GtkRequisition popup_req;
106 gtk_widget_size_request(menu, &popup_req);
107
108 /* Determine the coordinates. */
109 lxpanel_plugin_popup_set_position_helper(dm->panel, p, menu, &popup_req, px, py);
110 *push_in = TRUE;
111 }
112
113 /* Create a menu populated with all subdirectories. */
114 static GtkWidget * dirmenu_create_menu(DirMenuPlugin * dm, const char * path, gboolean open_at_top)
115 {
116 /* Create a menu. */
117 GtkWidget * menu = gtk_menu_new();
118
119 if (dm->folder_icon == NULL)
120 {
121 int w;
122 int h;
123 gtk_icon_size_lookup_for_settings(gtk_widget_get_settings(menu), GTK_ICON_SIZE_MENU, &w, &h);
124 dm->folder_icon = gtk_icon_theme_load_icon(
125 panel_get_icon_theme(dm->panel),
126 "gnome-fs-directory", MAX(w, h), 0, NULL);
127 if (dm->folder_icon == NULL)
128 dm->folder_icon = gtk_widget_render_icon(menu, GTK_STOCK_DIRECTORY, GTK_ICON_SIZE_MENU, NULL);
129 }
130
131 g_object_set_data_full(G_OBJECT(menu), "path", g_strdup(path), g_free);
132
133 /* Scan the specified directory to populate the menu with its subdirectories. */
134 DirectoryName * dir_list = NULL;
135 GDir * dir = g_dir_open(path, 0, NULL);
136 if (dir != NULL)
137 {
138 const char * name;
139 while ((name = g_dir_read_name(dir)) != NULL) /* Memory owned by glib */
140 {
141 /* Omit hidden files. */
142 if (name[0] != '.')
143 {
144 char * full = g_build_filename(path, name, NULL);
145 if (g_file_test(full, G_FILE_TEST_IS_DIR))
146 {
147 /* Convert name to UTF-8 and to the collation key. */
148 char * directory_name = g_filename_display_name(name);
149 char * directory_name_collate_key = g_utf8_collate_key(directory_name, -1);
150
151 /* Locate insertion point. */
152 DirectoryName * dir_pred = NULL;
153 DirectoryName * dir_cursor;
154 for (dir_cursor = dir_list; dir_cursor != NULL; dir_pred = dir_cursor, dir_cursor = dir_cursor->flink)
155 {
156 if (strcmp(directory_name_collate_key, dir_cursor->directory_name_collate_key) <= 0)
157 break;
158 }
159
160 /* Allocate and initialize sorted directory name entry. */
161 dir_cursor = g_new0(DirectoryName, 1);
162 dir_cursor->directory_name = directory_name;
163 dir_cursor->directory_name_collate_key = directory_name_collate_key;
164 if (dir_pred == NULL)
165 {
166 dir_cursor->flink = dir_list;
167 dir_list = dir_cursor;
168 }
169 else
170 {
171 dir_cursor->flink = dir_pred->flink;
172 dir_pred->flink = dir_cursor;
173 }
174 }
175 g_free(full);
176 }
177 }
178 g_dir_close(dir);
179 }
180
181 /* The sorted directory name list is complete. Loop to create the menu. */
182 DirectoryName * dir_cursor;
183 while ((dir_cursor = dir_list) != NULL)
184 {
185 /* Create and initialize menu item. */
186 GtkWidget * item = gtk_image_menu_item_new_with_label(dir_cursor->directory_name);
187 gtk_image_menu_item_set_image(
188 GTK_IMAGE_MENU_ITEM(item),
189 gtk_image_new_from_stock(GTK_STOCK_DIRECTORY, GTK_ICON_SIZE_MENU));
190 GtkWidget * dummy = gtk_menu_new();
191 gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), dummy);
192 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
193
194 /* Unlink and free sorted directory name element, but reuse the directory name string. */
195 dir_list = dir_cursor->flink;
196 g_object_set_data_full(G_OBJECT(item), "name", dir_cursor->directory_name, g_free);
197 g_free(dir_cursor->directory_name_collate_key);
198 g_free(dir_cursor);
199
200 /* Connect signals. */
201 g_signal_connect(G_OBJECT(item), "select", G_CALLBACK(dirmenu_menuitem_select), dm);
202 g_signal_connect(G_OBJECT(item), "deselect", G_CALLBACK(dirmenu_menuitem_deselect), dm);
203 }
204
205 /* Create "Open" and "Open in Terminal" items. */
206 GtkWidget * item = gtk_image_menu_item_new_from_stock( GTK_STOCK_OPEN, NULL );
207 g_signal_connect(item, "activate", G_CALLBACK(dirmenu_menuitem_open_directory), dm);
208 GtkWidget * term = gtk_menu_item_new_with_mnemonic( _("Open in _Terminal") );
209 g_signal_connect(term, "activate", G_CALLBACK(dirmenu_menuitem_open_in_terminal), dm);
210
211 /* Insert or append based on caller's preference. */
212 if (open_at_top)
213 {
214 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new(), 0);
215 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), term, 0);
216 gtk_menu_shell_insert(GTK_MENU_SHELL(menu), item, 0);
217 }
218 else {
219 gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
220 gtk_menu_shell_append(GTK_MENU_SHELL(menu), term);
221 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
222 }
223
224 /* Show the menu and return. */
225 gtk_widget_show_all(menu);
226 return menu;
227 }
228
229 /* Show a menu of subdirectories. */
230 static void dirmenu_show_menu(GtkWidget * widget, DirMenuPlugin * dm, int btn, guint32 time)
231 {
232 /* Create a menu populated with all subdirectories. */
233 GtkWidget * menu = dirmenu_create_menu(dm, dm->path, FALSE);
234 g_signal_connect(menu, "selection-done", G_CALLBACK(dirmenu_menu_selection_done), dm);
235
236 /* Show the menu. Use a positioning function to get it placed next to the top level widget. */
237 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, (GtkMenuPositionFunc) dirmenu_popup_set_position, widget, btn, time);
238 }
239
240 /* Handler for button-press-event on top level widget. */
241 static gboolean dirmenu_button_press_event(GtkWidget * widget, GdkEventButton * event, Panel * p)
242 {
243 DirMenuPlugin * dm = lxpanel_plugin_get_data(widget);
244
245 /* Standard left-click handling. */
246 if (lxpanel_plugin_button_press_event(widget, event, p))
247 return TRUE;
248
249 if (event->button == 1)
250 {
251 dirmenu_show_menu(widget, dm, event->button, event->time);
252 }
253 else
254 {
255 fm_terminal_launch(dm->path, NULL);
256 }
257 return TRUE;
258 }
259
260 /* Plugin constructor. */
261 static GtkWidget *dirmenu_constructor(Panel *panel, config_setting_t *settings)
262 {
263 /* Allocate and initialize plugin context and set into Plugin private data pointer. */
264 DirMenuPlugin * dm = g_new0(DirMenuPlugin, 1);
265 GtkWidget * p;
266 const char *str;
267
268 /* Load parameters from the configuration file. */
269 if (config_setting_lookup_string(settings, "image", &str))
270 dm->image = g_strdup(str);
271 if (config_setting_lookup_string(settings, "path", &str))
272 dm->path = expand_tilda(str);
273 else
274 dm->path = g_strdup(fm_get_home_dir());
275 if (config_setting_lookup_string(settings, "name", &str))
276 dm->name = g_strdup(str);
277
278 /* Save construction pointers */
279 dm->panel = panel;
280 dm->settings = settings;
281
282 /* Allocate top level widget and set into Plugin widget pointer.
283 * It is not known why, but the button text will not draw if it is edited from empty to non-empty
284 * unless this strategy of initializing it with a non-empty value first is followed. */
285 p = lxpanel_button_new_for_icon(panel,
286 ((dm->image != NULL) ? dm->image : "file-manager"),
287 NULL, "Temp");
288 lxpanel_plugin_set_data(p, dm, dirmenu_destructor);
289 gtk_container_set_border_width(GTK_CONTAINER(p), 0);
290
291 /* Initialize the widget. */
292 dirmenu_apply_configuration(p);
293
294 /* Show the widget and return. */
295 return p;
296 }
297
298 /* Plugin destructor. */
299 static void dirmenu_destructor(gpointer user_data)
300 {
301 DirMenuPlugin * dm = (DirMenuPlugin *)user_data;
302
303 /* Release a reference on the folder icon if held. */
304 if (dm->folder_icon)
305 g_object_unref(dm->folder_icon);
306
307 /* Deallocate all memory. */
308 g_free(dm->image);
309 g_free(dm->path);
310 g_free(dm->name);
311 g_free(dm);
312 }
313
314 /* Recursively apply a configuration change. */
315 static void dirmenu_apply_configuration_to_children(GtkWidget * w, DirMenuPlugin * dm)
316 {
317 if (GTK_IS_CONTAINER(w))
318 gtk_container_foreach(GTK_CONTAINER(w), (GtkCallback) dirmenu_apply_configuration_to_children, (gpointer) dm);
319 else if (GTK_IS_LABEL(w))
320 {
321 if (dm->name == NULL)
322 gtk_label_set_text(GTK_LABEL(w), NULL);
323 else
324 panel_draw_label_text(dm->panel, w, dm->name, FALSE, 1, TRUE);
325 }
326 }
327
328 /* Callback when the configuration dialog has recorded a configuration change. */
329 static gboolean dirmenu_apply_configuration(gpointer user_data)
330 {
331 GtkWidget * p = user_data;
332 DirMenuPlugin * dm = lxpanel_plugin_get_data(p);
333 char * path = dm->path;
334
335 /* Normalize path */
336 if (path == NULL)
337 dm->path = g_strdup(fm_get_home_dir());
338 else if (path[0] == '~')
339 {
340 dm->path = expand_tilda(path);
341 g_free(path);
342 }
343
344 /* Save configuration */
345 config_setting_set_string(config_setting_add(dm->settings, "path",
346 PANEL_CONF_TYPE_STRING),
347 dm->path);
348 config_setting_set_string(config_setting_add(dm->settings, "name",
349 PANEL_CONF_TYPE_STRING),
350 dm->name);
351 config_setting_set_string(config_setting_add(dm->settings, "image",
352 PANEL_CONF_TYPE_STRING),
353 dm->image);
354
355 lxpanel_button_set_icon(p, ((dm->image != NULL) ? dm->image : "file-manager"),
356 panel_get_icon_size(dm->panel));
357
358 gtk_widget_set_tooltip_text(p, dm->path);
359 gtk_container_foreach(GTK_CONTAINER(p), (GtkCallback) dirmenu_apply_configuration_to_children, (gpointer) dm);
360 return FALSE;
361 }
362
363 /* Callback when the configuration dialog is to be shown. */
364 static void dirmenu_configure(Panel *panel, GtkWidget *p, GtkWindow *parent)
365 {
366 DirMenuPlugin * dm = lxpanel_plugin_get_data(p);
367 GtkWidget * dlg = lxpanel_generic_config_dlg(_("Directory Menu"),
368 panel, dirmenu_apply_configuration, p,
369 _("Directory"), &dm->path, CONF_TYPE_DIRECTORY_ENTRY,
370 _("Label"), &dm->name, CONF_TYPE_STR,
371 _("Icon"), &dm->image, CONF_TYPE_FILE_ENTRY,
372 NULL);
373 gtk_window_present(GTK_WINDOW(dlg));
374 }
375
376 /* Callback when panel configuration changes. */
377 static void dirmenu_panel_configuration_changed(Panel *panel, GtkWidget *p)
378 {
379 dirmenu_apply_configuration(p);
380 }
381
382 /* Plugin descriptor. */
383 LXPanelPluginInit lxpanel_static_plugin_dirmenu = {
384 .name = N_("Directory Menu"),
385 .description = N_("Browse directory tree via menu (Author = PCMan)"),
386
387 .new_instance = dirmenu_constructor,
388 .config = dirmenu_configure,
389 .reconfigure = dirmenu_panel_configuration_changed,
390 .button_press_event = dirmenu_button_press_event
391 };