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