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