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