Merging upstream version 0.8.0 (Closes: #639729, #761971).
[debian/lxpanel.git] / src / gtk-run.c
CommitLineData
6cc5e1a6
DB
1/*
2 * gtk-run.c: Little application launcher
1ea75322 3 * Copyright (C) 2006 Hong Jen Yee (PCMan) pcman.tw(AT)gmail.com
6cc5e1a6
DB
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
7 * published by the Free Software Foundation; either version 2 of the
8 * 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
64ea8d44
AG
20#ifdef HAVE_CONFIG_H
21#include "config.h"
22#endif
23
6cc5e1a6 24#include <gtk/gtk.h>
f8c25730 25#include <gdk/gdkx.h>
6cc5e1a6
DB
26#include <glib/gi18n.h>
27#include <string.h>
1ea75322
DB
28#include <unistd.h>
29
6cc5e1a6 30#include "misc.h"
00916e98 31#ifndef DISABLE_MENU
10862fa6 32#include <menu-cache.h>
00916e98
AG
33#endif
34#include <libfm/fm-gtk.h>
6cc5e1a6 35
89173f95
AG
36#include "gtk-compat.h"
37
1ea75322 38static GtkWidget* win = NULL; /* the run dialog */
00916e98 39#ifndef DISABLE_MENU
934ecce5 40static MenuCache* menu_cache = NULL;
1ea75322 41static GSList* app_list = NULL; /* all known apps in menu cache */
934ecce5 42static gpointer reload_notify_id = NULL;
00916e98 43#endif
1ea75322
DB
44
45typedef struct _ThreadData
46{
47 gboolean cancel; /* is the loading cancelled */
48 GSList* files; /* all executable files found */
49 GtkEntry* entry;
50}ThreadData;
6cc5e1a6 51
1ea75322 52static ThreadData* thread_data = NULL; /* thread data used to load availble programs in PATH */
10862fa6 53
00916e98 54#ifndef DISABLE_MENU
10862fa6
DB
55static MenuCacheApp* match_app_by_exec(const char* exec)
56{
57 GSList* l;
58 MenuCacheApp* ret = NULL;
59 char* exec_path = g_find_program_in_path(exec);
60 const char* pexec;
61 int path_len, exec_len, len;
62
63 if( ! exec_path )
64 return NULL;
65
66 path_len = strlen(exec_path);
67 exec_len = strlen(exec);
68
69 for( l = app_list; l; l = l->next )
70 {
71 MenuCacheApp* app = MENU_CACHE_APP(l->data);
72 const char* app_exec = menu_cache_app_get_exec(app);
934ecce5
DB
73 if ( ! app_exec)
74 continue;
10862fa6
DB
75#if 0 /* This is useless and incorrect. */
76 /* Dirty hacks to skip sudo programs. This can be a little bit buggy */
77 if( g_str_has_prefix(app_exec, "gksu") )
78 {
79 app_exec += 4;
80 if( app_exec[0] == '\0' ) /* "gksu" itself */
81 app_exec -= 4;
82 else if( app_exec[0] == ' ' ) /* "gksu something..." */
83 ++app_exec;
84 else if( g_str_has_prefix(app_exec, "do ") ) /* "gksudo something" */
85 app_exec += 3;
86 }
87 else if( g_str_has_prefix(app_exec, "kdesu ") ) /* kdesu */
88 app_exec += 6;
89#endif
90
91 if( g_path_is_absolute(app_exec) )
92 {
93 pexec = exec_path;
94 len = path_len;
95 }
96 else
97 {
98 pexec = exec;
99 len = exec_len;
100 }
101
102 if( strncmp(app_exec, pexec, len) == 0 )
103 {
104 /* exact match has the highest priority */
105 if( app_exec[len] == '\0' )
106 {
107 ret = app;
108 break;
109 }
110 /* those matches the pattern: exe_name %F|%f|%U|%u have higher priority */
111 if( app_exec[len] == ' ' )
112 {
113 if( app_exec[len + 1] == '%' )
114 {
115 if( strchr( "FfUu", app_exec[len + 2] ) )
116 {
117 ret = app;
118 break;
119 }
120 }
121 ret = app;
122 }
123 }
124 }
125
126 /* if this is a symlink */
127 if( ! ret && g_file_test(exec_path, G_FILE_TEST_IS_SYMLINK) )
128 {
129 char target[512]; /* FIXME: is this enough? */
130 len = readlink( exec_path, target, sizeof(target) - 1);
131 if( len > 0 )
132 {
133 target[len] = '\0';
134 ret = match_app_by_exec(target);
135 if( ! ret )
136 {
137 /* FIXME: Actually, target could be relative paths.
138 * So, actually path resolution is needed here. */
139 char* basename = g_path_get_basename(target);
140 char* locate = g_find_program_in_path(basename);
141 if( locate && strcmp(locate, target) == 0 )
142 {
143 ret = match_app_by_exec(basename);
144 g_free(locate);
145 }
146 g_free(basename);
147 }
148 }
149 }
1ea75322 150
10862fa6
DB
151 g_free(exec_path);
152 return ret;
153}
00916e98 154#endif
10862fa6 155
1ea75322 156static void setup_auto_complete_with_data(ThreadData* data)
6cc5e1a6
DB
157{
158 GtkListStore* store;
1ea75322 159 GSList *l;
6cc5e1a6
DB
160 GtkEntryCompletion* comp = gtk_entry_completion_new();
161 gtk_entry_completion_set_minimum_key_length( comp, 2 );
162 gtk_entry_completion_set_inline_completion( comp, TRUE );
6cc5e1a6
DB
163 gtk_entry_completion_set_popup_set_width( comp, TRUE );
164 gtk_entry_completion_set_popup_single_match( comp, FALSE );
1ea75322
DB
165 store = gtk_list_store_new( 1, G_TYPE_STRING );
166
167 for( l = data->files; l; l = l->next )
168 {
169 const char *name = (const char*)l->data;
170 GtkTreeIter it;
171 gtk_list_store_append( store, &it );
172 gtk_list_store_set( store, &it, 0, name, -1 );
173 }
174
175 gtk_entry_completion_set_model( comp, (GtkTreeModel*)store );
176 g_object_unref( store );
177 gtk_entry_completion_set_text_column( comp, 0 );
178 gtk_entry_set_completion( (GtkEntry*)data->entry, comp );
179
180 /* trigger entry completion */
181 gtk_entry_completion_complete(comp);
182 g_object_unref( comp );
183}
184
00916e98 185static void thread_data_free(ThreadData* data)
1ea75322
DB
186{
187 g_slist_foreach(data->files, (GFunc)g_free, NULL);
188 g_slist_free(data->files);
189 g_slice_free(ThreadData, data);
190}
191
192static gboolean on_thread_finished(ThreadData* data)
193{
194 /* don't setup entry completion if the thread is already cancelled. */
195 if( !data->cancel )
196 setup_auto_complete_with_data(thread_data);
197 thread_data_free(data);
198 thread_data = NULL; /* global thread_data pointer */
199 return FALSE;
200}
6cc5e1a6 201
1ea75322
DB
202static gpointer thread_func(ThreadData* data)
203{
5d26221e 204 GSList *list = NULL;
1ea75322
DB
205 gchar **dirname;
206 gchar **dirnames = g_strsplit( g_getenv("PATH"), ":", 0 );
207
208 for( dirname = dirnames; !thread_data->cancel && *dirname; ++dirname )
6cc5e1a6
DB
209 {
210 GDir *dir = g_dir_open( *dirname, 0, NULL );
211 const char *name;
212 if( ! dir )
213 continue;
1ea75322 214 while( !thread_data->cancel && (name = g_dir_read_name(dir)) )
6cc5e1a6
DB
215 {
216 char* filename = g_build_filename( *dirname, name, NULL );
217 if( g_file_test( filename, G_FILE_TEST_IS_EXECUTABLE ) )
218 {
1ea75322
DB
219 if(thread_data->cancel)
220 break;
221 if( !g_slist_find_custom( list, name, (GCompareFunc)strcmp ) )
222 list = g_slist_prepend( list, g_strdup( name ) );
6cc5e1a6
DB
223 }
224 g_free( filename );
225 }
226 g_dir_close( dir );
227 }
228 g_strfreev( dirnames );
229
1ea75322
DB
230 data->files = list;
231 /* install an idle handler to free associated data */
232 g_idle_add((GSourceFunc)on_thread_finished, data);
89173f95
AG
233#if GLIB_CHECK_VERSION(2, 32, 0)
234 g_thread_unref(g_thread_self());
235#endif
6cc5e1a6 236
1ea75322 237 return NULL;
6cc5e1a6
DB
238}
239
1ea75322 240static void setup_auto_complete( GtkEntry* entry )
6cc5e1a6 241{
1ea75322
DB
242 gboolean cache_is_available = FALSE;
243 /* FIXME: consider saving the list of commands as on-disk cache. */
244 if( cache_is_available )
245 {
246 /* load cached program list */
247 }
248 else
249 {
250 /* load in another working thread */
251 thread_data = g_slice_new0(ThreadData); /* the data will be freed in idle handler later. */
252 thread_data->entry = entry;
89173f95
AG
253#if GLIB_CHECK_VERSION(2, 32, 0)
254 g_thread_new("gtk-run-autocomplete", (GThreadFunc)thread_func, thread_data);
255 /* we don't use loader_thread_id but Glib 2.32 crashes if we unref
256 GThread while it's in creation progress. It is a bug of GLib
257 certainly but as workaround we'll unref it in the thread itself */
258#else
1ea75322 259 g_thread_create((GThreadFunc)thread_func, thread_data, FALSE, NULL);
89173f95 260#endif
1ea75322 261 }
6cc5e1a6 262}
6cc5e1a6 263
00916e98 264#ifndef DISABLE_MENU
934ecce5
DB
265static void reload_apps(MenuCache* cache, gpointer user_data)
266{
267 g_debug("reload apps!");
268 if(app_list)
269 {
270 g_slist_foreach(app_list, (GFunc)menu_cache_item_unref, NULL);
271 g_slist_free(app_list);
272 }
dcd17043 273 app_list = menu_cache_list_all_apps(cache);
934ecce5 274}
00916e98 275#endif
934ecce5 276
6cc5e1a6
DB
277static void on_response( GtkDialog* dlg, gint response, gpointer user_data )
278{
279 GtkEntry* entry = (GtkEntry*)user_data;
280 if( G_LIKELY(response == GTK_RESPONSE_OK) )
281 {
00916e98 282 if (!fm_launch_command_simple(GTK_WINDOW(dlg), NULL, 0, gtk_entry_get_text(entry), NULL))
6cc5e1a6 283 {
6cc5e1a6
DB
284 g_signal_stop_emission_by_name( dlg, "response" );
285 return;
286 }
287 }
1ea75322
DB
288
289 /* cancel running thread if needed */
290 if( thread_data ) /* the thread is still running */
291 thread_data->cancel = TRUE; /* cancel the thread */
292
6cc5e1a6 293 gtk_widget_destroy( (GtkWidget*)dlg );
10862fa6
DB
294 win = NULL;
295
00916e98 296#ifndef DISABLE_MENU
10862fa6
DB
297 /* free app list */
298 g_slist_foreach(app_list, (GFunc)menu_cache_item_unref, NULL);
299 g_slist_free(app_list);
300 app_list = NULL;
934ecce5
DB
301
302 /* free menu cache */
303 menu_cache_remove_reload_notify(menu_cache, reload_notify_id);
304 reload_notify_id = NULL;
305 menu_cache_unref(menu_cache);
306 menu_cache = NULL;
00916e98 307#endif
10862fa6
DB
308}
309
00916e98 310#ifndef DISABLE_MENU
10862fa6
DB
311static void on_entry_changed( GtkEntry* entry, GtkImage* img )
312{
313 const char* str = gtk_entry_get_text(entry);
314 MenuCacheApp* app = NULL;
315 if( str && *str )
316 app = match_app_by_exec(str);
317
318 if( app )
319 {
320 int w, h;
00916e98
AG
321 const char *name = menu_cache_item_get_icon(MENU_CACHE_ITEM(app));
322 FmIcon * fm_icon;
10862fa6 323 GdkPixbuf* pix;
00916e98 324
10862fa6 325 gtk_icon_size_lookup(GTK_ICON_SIZE_DIALOG, &w, &h);
00916e98
AG
326 fm_icon = fm_icon_from_name(name ? name : "application-x-executable");
327 pix = fm_pixbuf_from_icon_with_fallback(fm_icon, h, "application-x-executable");
328 g_object_unref(fm_icon);
10862fa6
DB
329 gtk_image_set_from_pixbuf(img, pix);
330 g_object_unref(pix);
331 }
332 else
333 {
334 gtk_image_set_from_stock(img, GTK_STOCK_EXECUTE, GTK_ICON_SIZE_DIALOG);
335 }
6cc5e1a6 336}
00916e98 337#endif
6cc5e1a6 338
f8c25730
DB
339static void activate_window(GtkWindow* toplevel_window)
340{
00916e98
AG
341 /* Calling gtk_window_present() cannot support
342 * source indication. Use our own implementation.
343 * Without this, the window activated might be
344 * put in background by WM to avoid stealing
345 * of focus.
346 * See EWMH spec, source indication part:
347 * http://standards.freedesktop.org/wm-spec/wm-spec-latest.html#sourceindication
348 */
349 GtkWidget* widget = GTK_WIDGET(toplevel_window);
350 GdkScreen* screen = gtk_widget_get_screen(widget);
351 if (gdk_x11_screen_supports_net_wm_hint (screen,
f8c25730
DB
352 gdk_atom_intern_static_string ("_NET_ACTIVE_WINDOW")))
353 {
00916e98
AG
354 GdkWindow* window = gtk_widget_get_window(widget);
355 GdkDisplay* display = gtk_widget_get_display(widget);
356 GdkWindow* root = gdk_screen_get_root_window(screen);
357
358 /* show the window first */
359 gtk_widget_show(widget);
360
361 /* then, activate it */
362 XClientMessageEvent xclient;
363 memset(&xclient, 0, sizeof (xclient));
364 xclient.type = ClientMessage;
365 xclient.window = GDK_WINDOW_XID(window);
366 xclient.message_type = gdk_x11_get_xatom_by_name_for_display(display,
367 "_NET_ACTIVE_WINDOW");
368 xclient.format = 32;
369 xclient.data.l[0] = 2; /* source indication: 2 represents direct user actions. */
370 xclient.data.l[1] = gtk_get_current_event_time();
371 xclient.data.l[2] = None; /* currently active window */
372 xclient.data.l[3] = 0;
373 xclient.data.l[4] = 0;
374
375 XSendEvent(GDK_DISPLAY_XDISPLAY(display), GDK_WINDOW_XID(root), False,
376 SubstructureRedirectMask|SubstructureNotifyMask,
377 (XEvent *)&xclient);
f8c25730 378 } /* if _NET_ACTIVE_WINDOW command is not supported */
00916e98
AG
379 else
380 gtk_window_present(toplevel_window);
f8c25730
DB
381}
382
6cc5e1a6
DB
383void gtk_run()
384{
00916e98 385 GtkWidget *entry, *hbox, *img, *dlg_vbox;
10862fa6 386
f8c25730 387 if(!win)
10862fa6 388 {
00916e98
AG
389 win = gtk_dialog_new_with_buttons( _("Run"),
390 NULL,
391 GTK_DIALOG_NO_SEPARATOR,
392 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
393 GTK_STOCK_OK, GTK_RESPONSE_OK,
394 NULL );
395 gtk_dialog_set_alternative_button_order((GtkDialog*)win,
396 GTK_RESPONSE_OK, GTK_RESPONSE_CANCEL, -1);
397 gtk_dialog_set_default_response( (GtkDialog*)win, GTK_RESPONSE_OK );
398 entry = gtk_entry_new();
399
400 gtk_entry_set_activates_default( (GtkEntry*)entry, TRUE );
401 dlg_vbox = gtk_dialog_get_content_area((GtkDialog*)win);
402 gtk_box_pack_start( (GtkBox*)dlg_vbox,
403 gtk_label_new(_("Enter the command you want to execute:")),
404 FALSE, FALSE, 8 );
405 hbox = gtk_hbox_new( FALSE, 2 );
406 img = gtk_image_new_from_stock( GTK_STOCK_EXECUTE, GTK_ICON_SIZE_DIALOG );
407 gtk_box_pack_start( (GtkBox*)hbox, img,
408 FALSE, FALSE, 4 );
409 gtk_box_pack_start( (GtkBox*)hbox, entry, TRUE, TRUE, 4 );
410 gtk_box_pack_start( (GtkBox*)dlg_vbox,
411 hbox, FALSE, FALSE, 8 );
412 g_signal_connect( win, "response", G_CALLBACK(on_response), entry );
413 gtk_window_set_position( (GtkWindow*)win, GTK_WIN_POS_CENTER );
414 gtk_window_set_default_size( (GtkWindow*)win, 360, -1 );
415 gtk_widget_show_all( win );
416
417 setup_auto_complete( (GtkEntry*)entry );
418 gtk_widget_show(win);
419
420#ifndef DISABLE_MENU
421 g_signal_connect(entry ,"changed", G_CALLBACK(on_entry_changed), img);
422
423 /* get all apps */
89173f95
AG
424#ifndef MENU_CACHE_CHECK_VERSION
425#define MENU_CACHE_CHECK_VERSION(a,b,c) 0
426#endif
427#if MENU_CACHE_CHECK_VERSION(0, 6, 1)
dcd17043 428 menu_cache = menu_cache_lookup_sync(g_getenv("XDG_MENU_PREFIX") ? "applications.menu" : "lxde-applications.menu" );
00916e98
AG
429 if( menu_cache )
430 {
89173f95
AG
431#else
432 /* SF bug #689: menu_cache_lookup_sync() was fail-prone before 0.6.1 */
433 menu_cache = menu_cache_lookup(g_getenv("XDG_MENU_PREFIX") ? "applications.menu" : "lxde-applications.menu" );
434 if( menu_cache )
435 {
436 menu_cache_reload(menu_cache);
437#endif
dcd17043 438 app_list = menu_cache_list_all_apps(menu_cache);
00916e98
AG
439 reload_notify_id = menu_cache_add_reload_notify(menu_cache, reload_apps, NULL);
440 }
441#endif
442 }
443
444 activate_window(GTK_WINDOW(win));
6cc5e1a6
DB
445}
446
00916e98
AG
447
448/* vim: set sw=4 et sts=4 ts=4 : */