Fix typo in Readme file
[geeqie.git] / src / editors.cc
1 /*
2  * Copyright (C) 2006 John Ellis
3  * Copyright (C) 2008 - 2016 The Geeqie Team
4  *
5  * Author: John Ellis
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21
22 #include "main.h"
23 #include "editors.h"
24
25 #include "filedata.h"
26 #include "filefilter.h"
27 #include "pixbuf-util.h"
28 #include "ui-fileops.h"
29 #include "ui-spinner.h"
30 #include "utilops.h"
31
32 #define EDITOR_WINDOW_WIDTH 500
33 #define EDITOR_WINDOW_HEIGHT 300
34
35
36
37 typedef struct _EditorVerboseData EditorVerboseData;
38 struct _EditorVerboseData {
39         GenericDialog *gd;
40         GtkWidget *button_close;
41         GtkWidget *button_stop;
42         GtkWidget *text;
43         GtkWidget *progress;
44         GtkWidget *spinner;
45 };
46
47 typedef struct _EditorData EditorData;
48 struct _EditorData {
49         EditorFlags flags;
50         GPid pid;
51         GList *list;
52         gint count;
53         gint total;
54         gboolean stopping;
55         EditorVerboseData *vd;
56         EditorCallback callback;
57         gpointer data;
58         const EditorDescription *editor;
59         gchar *working_directory; /* fallback if no files are given (editor_no_param) */
60 };
61
62
63 static void editor_verbose_window_progress(EditorData *ed, const gchar *text);
64 static EditorFlags editor_command_next_start(EditorData *ed);
65 static EditorFlags editor_command_next_finish(EditorData *ed, gint status);
66 static EditorFlags editor_command_done(EditorData *ed);
67
68 /*
69  *-----------------------------------------------------------------------------
70  * external editor routines
71  *-----------------------------------------------------------------------------
72  */
73
74 GHashTable *editors = NULL;
75 GtkListStore *desktop_file_list;
76 gboolean editors_finished = FALSE;
77
78 #ifdef G_KEY_FILE_DESKTOP_GROUP
79 #define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP
80 #else
81 #define DESKTOP_GROUP "Desktop Entry"
82 #endif
83
84 void editor_description_free(EditorDescription *editor)
85 {
86         if (!editor) return;
87
88         g_free(editor->key);
89         g_free(editor->name);
90         g_free(editor->icon);
91         g_free(editor->exec);
92         g_free(editor->menu_path);
93         g_free(editor->hotkey);
94         g_free(editor->comment);
95         string_list_free(editor->ext_list);
96         g_free(editor->file);
97         g_free(editor);
98 }
99
100 static GList *editor_mime_types_to_extensions(gchar **mime_types)
101 {
102         /** @FIXME this should be rewritten to use the shared mime database, as soon as we switch to gio */
103
104         static const gchar *conv_table[][2] = {
105                 {"image/*",             "*"},
106                 {"image/bmp",           ".bmp"},
107                 {"image/gif",           ".gif"},
108                 {"image/heic",          ".heic"},
109                 {"image/jpeg",          ".jpeg;.jpg;.mpo"},
110                 {"image/jpg",           ".jpg;.jpeg"},
111                 {"image/webp",          ".webp"},
112                 {"image/pcx",           ".pcx"},
113                 {"image/png",           ".png"},
114                 {"image/svg",           ".svg"},
115                 {"image/svg+xml",       ".svg"},
116                 {"image/svg+xml-compressed",    ".svg"},
117                 {"image/tiff",          ".tiff;.tif;.mef"},
118                 {"image/vnd-ms.dds",    ".dds"},
119                 {"image/x-adobe-dng",   ".dng"},
120                 {"image/x-bmp",         ".bmp"},
121                 {"image/x-canon-crw",   ".crw"},
122                 {"image/x-canon-cr2",   ".cr2"},
123                 {"image/x-canon-cr3",   ".cr3"},
124                 {"image/x-cr2",         ".cr2"},
125                 {"image/x-dcraw",       "%raw;.mos"},
126                 {"image/x-epson-erf",   "%erf"},
127                 {"image/x-ico",         ".ico"},
128                 {"image/x-kodak-kdc",   ".kdc"},
129                 {"image/x-mrw",         ".mrw"},
130                 {"image/x-minolta-mrw", ".mrw"},
131                 {"image/x-MS-bmp",      ".bmp"},
132                 {"image/x-nef",         ".nef"},
133                 {"image/x-nikon-nef",   ".nef"},
134                 {"image/x-panasonic-raw",       ".raw"},
135                 {"image/x-panasonic-rw2",       ".rw2"},
136                 {"image/x-pentax-pef",  ".pef"},
137                 {"image/x-orf",         ".orf"},
138                 {"image/x-olympus-orf", ".orf"},
139                 {"image/x-pcx",         ".pcx"},
140                 {"image/xpm",           ".xpm"},
141                 {"image/x-png",         ".png"},
142                 {"image/x-portable-anymap",     ".pam"},
143                 {"image/x-portable-bitmap",     ".pbm"},
144                 {"image/x-portable-graymap",    ".pgm"},
145                 {"image/x-portable-pixmap",     ".ppm"},
146                 {"image/x-psd",         ".psd"},
147                 {"image/x-raf",         ".raf"},
148                 {"image/x-fuji-raf",    ".raf"},
149                 {"image/x-sgi",         ".sgi"},
150                 {"image/x-sony-arw",    ".arw"},
151                 {"image/x-sony-sr2",    ".sr2"},
152                 {"image/x-sony-srf",    ".srf"},
153                 {"image/x-tga",         ".tga"},
154                 {"image/x-xbitmap",     ".xbm"},
155                 {"image/x-xcf",         ".xcf"},
156                 {"image/x-xpixmap",     ".xpm"},
157                 {"image/x-x3f",         ".x3f"},
158                 {"application/x-navi-animation",                ".ani"},
159                 {"application/x-ptoptimizer-script",    ".pto"},
160                 {NULL, NULL}};
161
162         gint i, j;
163         GList *list = NULL;
164
165         for (i = 0; mime_types[i]; i++)
166                 for (j = 0; conv_table[j][0]; j++)
167                         if (strcmp(mime_types[i], conv_table[j][0]) == 0)
168                                 list = g_list_concat(list, filter_to_list(conv_table[j][1]));
169
170         return list;
171 }
172
173 gboolean editor_read_desktop_file(const gchar *path)
174 {
175         GKeyFile *key_file;
176         EditorDescription *editor;
177         gchar *extensions;
178         gchar *type;
179         const gchar *key = filename_from_path(path);
180         gchar **categories, **only_show_in, **not_show_in;
181         gchar *try_exec;
182         GtkTreeIter iter;
183         gboolean category_geeqie = FALSE;
184         GList *work;
185         gboolean disabled;
186
187         if (g_hash_table_lookup(editors, key)) return FALSE; /* the file found earlier wins */
188
189         key_file = g_key_file_new();
190         if (!g_key_file_load_from_file(key_file, path, static_cast<GKeyFileFlags>(0), NULL))
191                 {
192                 g_key_file_free(key_file);
193                 return FALSE;
194                 }
195
196         type = g_key_file_get_string(key_file, DESKTOP_GROUP, "Type", NULL);
197         if (!type || strcmp(type, "Application") != 0)
198                 {
199                 /* We only consider desktop entries of Application type */
200                 g_key_file_free(key_file);
201                 g_free(type);
202                 return FALSE;
203                 }
204         g_free(type);
205
206         editor = g_new0(EditorDescription, 1);
207
208         editor->key = g_strdup(key);
209         editor->file = g_strdup(path);
210
211         g_hash_table_insert(editors, editor->key, editor);
212
213         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Hidden", NULL)
214             || g_key_file_get_boolean(key_file, DESKTOP_GROUP, "NoDisplay", NULL))
215                 {
216                 editor->hidden = TRUE;
217                 }
218
219         categories = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "Categories", NULL, NULL);
220         if (categories)
221                 {
222                 gboolean found = FALSE;
223                 gint i;
224                 for (i = 0; categories[i]; i++)
225                         {
226                         /* IMHO "Graphics" is exactly the category that we are interested in, so this does not have to be configurable */
227                         if (strcmp(categories[i], "Graphics") == 0)
228                                 {
229                                 found = TRUE;
230                                 }
231                         if (strcmp(categories[i], "X-Geeqie") == 0)
232                                 {
233                                 found = TRUE;
234                                 category_geeqie = TRUE;
235                                 break;
236                                 }
237                         }
238                 if (!found) editor->ignored = TRUE;
239                 g_strfreev(categories);
240                 }
241         else
242                 {
243                 editor->ignored = TRUE;
244                 }
245
246         only_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "OnlyShowIn", NULL, NULL);
247         if (only_show_in)
248                 {
249                 gboolean found = FALSE;
250                 gint i;
251                 for (i = 0; only_show_in[i]; i++)
252                         if (strcmp(only_show_in[i], "X-Geeqie") == 0)
253                                 {
254                                 found = TRUE;
255                                 break;
256                                 }
257                 if (!found) editor->ignored = TRUE;
258                 g_strfreev(only_show_in);
259                 }
260
261         not_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "NotShowIn", NULL, NULL);
262         if (not_show_in)
263                 {
264                 gboolean found = FALSE;
265                 gint i;
266                 for (i = 0; not_show_in[i]; i++)
267                         if (strcmp(not_show_in[i], "X-Geeqie") == 0)
268                                 {
269                                 found = TRUE;
270                                 break;
271                                 }
272                 if (found) editor->ignored = TRUE;
273                 g_strfreev(not_show_in);
274                 }
275
276
277         try_exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "TryExec", NULL);
278         if (try_exec && !editor->hidden && !editor->ignored)
279                 {
280                 gchar *try_exec_res = g_find_program_in_path(try_exec);
281                 if (!try_exec_res) editor->hidden = TRUE;
282                 g_free(try_exec_res);
283                 g_free(try_exec);
284                 }
285
286         if (editor->ignored)
287                 {
288                 /* ignored editors will be deleted, no need to parse the rest */
289                 g_key_file_free(key_file);
290                 return TRUE;
291                 }
292
293         editor->name = g_key_file_get_locale_string(key_file, DESKTOP_GROUP, "Name", NULL, NULL);
294         editor->icon = g_key_file_get_string(key_file, DESKTOP_GROUP, "Icon", NULL);
295
296         /* Icon key can be either a full path (absolute with file name extension) or an icon name (without extension) */
297         if (editor->icon && !g_path_is_absolute(editor->icon))
298                 {
299                 gchar *ext = strrchr(editor->icon, '.');
300
301                 if (ext && strlen(ext) == 4 &&
302                     (!strcmp(ext, ".png") || !strcmp(ext, ".xpm") || !strcmp(ext, ".svg")))
303                         {
304                         log_printf(_("Desktop file '%s' should not include extension in Icon key: '%s'\n"),
305                                    editor->file, editor->icon);
306
307                         // drop extension
308                         *ext = '\0';
309                         }
310                 }
311         if (editor->icon && !register_theme_icon_as_stock(editor->key, editor->icon))
312                 {
313                 g_free(editor->icon);
314                 editor->icon = NULL;
315                 }
316
317         editor->exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "Exec", NULL);
318
319         editor->menu_path = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Menu-Path", NULL);
320         if (!editor->menu_path) editor->menu_path = g_strdup("PluginsMenu");
321
322         editor->hotkey = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Hotkey", NULL);
323
324         editor->comment = g_key_file_get_string(key_file, DESKTOP_GROUP, "Comment", NULL);
325
326         extensions = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-File-Extensions", NULL);
327         if (extensions)
328                 editor->ext_list = filter_to_list(extensions);
329         else
330                 {
331                 gchar **mime_types = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "MimeType", NULL, NULL);
332                 if (mime_types)
333                         {
334                         editor->ext_list = editor_mime_types_to_extensions(mime_types);
335                         g_strfreev(mime_types);
336                         if (!editor->ext_list) editor->hidden = TRUE;
337                         }
338                 }
339
340         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Keep-Fullscreen", NULL)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_KEEP_FS);
341         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose", NULL)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_VERBOSE);
342         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose-Multi", NULL)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_VERBOSE_MULTI);
343         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Filter", NULL)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_DEST);
344         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Terminal", NULL)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_TERMINAL);
345
346         editor->flags = static_cast<EditorFlags>(editor->flags | editor_command_parse(editor, NULL, FALSE, NULL));
347
348         if ((editor->flags & EDITOR_NO_PARAM) && !category_geeqie) editor->hidden = TRUE;
349
350         g_key_file_free(key_file);
351
352         if (editor->ignored) return TRUE;
353
354         work = options->disabled_plugins;
355
356         disabled = FALSE;
357         while (work)
358                 {
359                 if (g_strcmp0(path, static_cast<const gchar *>(work->data)) == 0)
360                         {
361                         disabled = TRUE;
362                         break;
363                         }
364                 work = work->next;
365                 }
366
367         editor->disabled = disabled;
368
369         gtk_list_store_append(desktop_file_list, &iter);
370         gtk_list_store_set(desktop_file_list, &iter,
371                            DESKTOP_FILE_COLUMN_KEY, key,
372                            DESKTOP_FILE_COLUMN_DISABLED, editor->disabled,
373                            DESKTOP_FILE_COLUMN_NAME, editor->name,
374                            DESKTOP_FILE_COLUMN_HIDDEN, editor->hidden ? _("yes") : _("no"),
375                            DESKTOP_FILE_COLUMN_WRITABLE, access_file(path, W_OK),
376                            DESKTOP_FILE_COLUMN_PATH, path, -1);
377
378         return TRUE;
379 }
380
381 static gboolean editor_remove_desktop_file_cb(gpointer UNUSED(key), gpointer value, gpointer UNUSED(user_data))
382 {
383         EditorDescription *editor = static_cast<EditorDescription *>(value);
384         return editor->hidden || editor->ignored;
385 }
386
387 void editor_table_finish(void)
388 {
389         g_hash_table_foreach_remove(editors, editor_remove_desktop_file_cb, NULL);
390         editors_finished = TRUE;
391 }
392
393 void editor_table_clear(void)
394 {
395         if (desktop_file_list)
396                 {
397                 gtk_list_store_clear(desktop_file_list);
398                 }
399         else
400                 {
401                 desktop_file_list = gtk_list_store_new(DESKTOP_FILE_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING);
402                 }
403         if (editors)
404                 {
405                 g_hash_table_destroy(editors);
406                 }
407         editors = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)editor_description_free);
408         editors_finished = FALSE;
409 }
410
411 static GList *editor_add_desktop_dir(GList *list, const gchar *path)
412 {
413         DIR *dp;
414         struct dirent *dir;
415         gchar *pathl;
416
417         pathl = path_from_utf8(path);
418         dp = opendir(pathl);
419         g_free(pathl);
420         if (!dp)
421                 {
422                 /* dir not found */
423                 return list;
424                 }
425         while ((dir = readdir(dp)) != NULL)
426                 {
427                 gchar *namel = dir->d_name;
428
429                 if (g_str_has_suffix(namel, ".desktop"))
430                         {
431                         gchar *name = path_to_utf8(namel);
432                         gchar *dpath = g_build_filename(path, name, NULL);
433                         list = g_list_prepend(list, dpath);
434                         g_free(name);
435                         }
436                 }
437         closedir(dp);
438         return list;
439 }
440
441 GList *editor_get_desktop_files(void)
442 {
443         gchar *path;
444         gchar *xdg_data_dirs;
445         gchar *all_dirs;
446         gchar **split_dirs;
447         gint i;
448         GList *list = NULL;
449
450         xdg_data_dirs = getenv("XDG_DATA_DIRS");
451         if (xdg_data_dirs && xdg_data_dirs[0])
452                 xdg_data_dirs = path_to_utf8(xdg_data_dirs);
453         else
454                 xdg_data_dirs = g_strdup("/usr/share");
455
456         all_dirs = g_strconcat(get_rc_dir(), ":", gq_appdir, ":", xdg_data_home_get(), ":", xdg_data_dirs, NULL);
457
458         g_free(xdg_data_dirs);
459
460         split_dirs = g_strsplit(all_dirs, ":", 0);
461
462         g_free(all_dirs);
463
464         for (i = 0; split_dirs[i]; i++);
465         for (--i; i >= 0; i--)
466                 {
467                 path = g_build_filename(split_dirs[i], "applications", NULL);
468                 list = editor_add_desktop_dir(list, path);
469                 g_free(path);
470                 }
471
472         g_strfreev(split_dirs);
473         return list;
474 }
475
476 static void editor_list_add_cb(gpointer UNUSED(key), gpointer value, gpointer data)
477 {
478         GList **listp = static_cast<GList **>(data);
479         EditorDescription *editor = static_cast<EditorDescription *>(value);
480
481         /* do not show the special commands in any list, they are called explicitly */
482         if (strcmp(editor->key, CMD_COPY) == 0 ||
483             strcmp(editor->key, CMD_MOVE) == 0 ||
484             strcmp(editor->key, CMD_RENAME) == 0 ||
485             strcmp(editor->key, CMD_DELETE) == 0 ||
486             strcmp(editor->key, CMD_FOLDER) == 0) return;
487
488         if (editor->disabled)
489                 {
490                 return;
491                 }
492
493         *listp = g_list_prepend(*listp, editor);
494 }
495
496 static gint editor_sort(gconstpointer a, gconstpointer b)
497 {
498         const EditorDescription *ea = static_cast<const EditorDescription *>(a);
499         const EditorDescription *eb = static_cast<const EditorDescription *>(b);
500         gchar *caseless_name_ea;
501         gchar *caseless_name_eb;
502         gchar *collate_key_ea;
503         gchar *collate_key_eb;
504         gint ret;
505
506         ret = strcmp(ea->menu_path, eb->menu_path);
507         if (ret != 0) return ret;
508
509         caseless_name_ea = g_utf8_casefold(ea->name, -1);
510         caseless_name_eb = g_utf8_casefold(eb->name, -1);
511         collate_key_ea = g_utf8_collate_key_for_filename(caseless_name_ea, -1);
512         collate_key_eb = g_utf8_collate_key_for_filename(caseless_name_eb, -1);
513         ret = g_strcmp0(collate_key_ea, collate_key_eb);
514
515         g_free(collate_key_ea);
516         g_free(collate_key_eb);
517         g_free(caseless_name_ea);
518         g_free(caseless_name_eb);
519
520         return ret;
521 }
522
523 GList *editor_list_get(void)
524 {
525         GList *editors_list = NULL;
526
527         if (!editors_finished) return NULL;
528
529         g_hash_table_foreach(editors, editor_list_add_cb, &editors_list);
530         editors_list = g_list_sort(editors_list, editor_sort);
531
532         return editors_list;
533 }
534
535 /* ------------------------------ */
536
537
538 static void editor_verbose_data_free(EditorData *ed)
539 {
540         if (!ed->vd) return;
541         g_free(ed->vd);
542         ed->vd = NULL;
543 }
544
545 static void editor_data_free(EditorData *ed)
546 {
547         editor_verbose_data_free(ed);
548         g_free(ed->working_directory);
549         g_free(ed);
550 }
551
552 static void editor_verbose_window_close(GenericDialog *gd, gpointer data)
553 {
554         EditorData *ed = static_cast<EditorData *>(data);
555
556         generic_dialog_close(gd);
557         editor_verbose_data_free(ed);
558         if (ed->pid == -1) editor_data_free(ed); /* the process has already terminated */
559 }
560
561 static void editor_verbose_window_stop(GenericDialog *UNUSED(gd), gpointer data)
562 {
563         EditorData *ed = static_cast<EditorData *>(data);
564         ed->stopping = TRUE;
565         ed->count = 0;
566         editor_verbose_window_progress(ed, _("stopping..."));
567 }
568
569 static void editor_verbose_window_enable_close(EditorVerboseData *vd)
570 {
571         vd->gd->cancel_cb = editor_verbose_window_close;
572
573         spinner_set_interval(vd->spinner, -1);
574         gtk_widget_set_sensitive(vd->button_stop, FALSE);
575         gtk_widget_set_sensitive(vd->button_close, TRUE);
576 }
577
578 static EditorVerboseData *editor_verbose_window(EditorData *ed, const gchar *text)
579 {
580         EditorVerboseData *vd;
581         GtkWidget *scrolled;
582         GtkWidget *hbox;
583         gchar *buf;
584
585         vd = g_new0(EditorVerboseData, 1);
586
587         vd->gd = file_util_gen_dlg(_("Edit command results"), "editor_results",
588                                    NULL, FALSE,
589                                    NULL, ed);
590         buf = g_strdup_printf(_("Output of %s"), text);
591         generic_dialog_add_message(vd->gd, NULL, buf, NULL, FALSE);
592         g_free(buf);
593         //~ vd->button_stop = generic_dialog_add_button(vd->gd, GTK_STOCK_STOP, NULL,
594                                                    //~ editor_verbose_window_stop, FALSE);
595         vd->button_stop = generic_dialog_add_button(vd->gd, "process-stop", NULL,
596                                                    editor_verbose_window_stop, FALSE);
597         gtk_widget_set_sensitive(vd->button_stop, FALSE);
598         vd->button_close = generic_dialog_add_button(vd->gd, GTK_STOCK_CLOSE, NULL,
599                                                     editor_verbose_window_close, TRUE);
600         gtk_widget_set_sensitive(vd->button_close, FALSE);
601
602         scrolled = gtk_scrolled_window_new(NULL, NULL);
603         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
604         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
605                                        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
606         gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
607         gtk_widget_show(scrolled);
608
609         vd->text = gtk_text_view_new();
610         gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
611         gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
612         gtk_container_add(GTK_CONTAINER(scrolled), vd->text);
613         gtk_widget_show(vd->text);
614
615         hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
616         gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
617         gtk_widget_show(hbox);
618
619         vd->progress = gtk_progress_bar_new();
620         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
621         gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
622         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vd->progress), "");
623         gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(vd->progress), TRUE);
624         gtk_widget_show(vd->progress);
625
626         vd->spinner = spinner_new(NULL, SPINNER_SPEED);
627         gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
628         gtk_widget_show(vd->spinner);
629
630         gtk_widget_show(vd->gd->dialog);
631
632         ed->vd = vd;
633         return vd;
634 }
635
636 static void editor_verbose_window_fill(EditorVerboseData *vd, const gchar *text, gint len)
637 {
638         GtkTextBuffer *buffer;
639         GtkTextIter iter;
640
641         buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
642         gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
643         gtk_text_buffer_insert(buffer, &iter, text, len);
644 }
645
646 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
647 {
648         if (!ed->vd) return;
649
650         if (ed->total)
651                 {
652                 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), (gdouble)ed->count / ed->total);
653                 }
654
655         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
656 }
657
658 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
659 {
660         EditorData *ed = static_cast<EditorData *>(data);
661         gchar buf[512];
662         gsize count;
663
664         if (condition & G_IO_IN)
665                 {
666                 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, NULL) == G_IO_STATUS_NORMAL)
667                         {
668                         if (!g_utf8_validate(buf, count, NULL))
669                                 {
670                                 gchar *utf8;
671
672                                 utf8 = g_locale_to_utf8(buf, count, NULL, NULL, NULL);
673                                 if (utf8)
674                                         {
675                                         editor_verbose_window_fill(ed->vd, utf8, -1);
676                                         g_free(utf8);
677                                         }
678                                 else
679                                         {
680                                         editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
681                                         }
682                                 }
683                         else
684                                 {
685                                 editor_verbose_window_fill(ed->vd, buf, count);
686                                 }
687                         }
688                 }
689
690         if (condition & (G_IO_ERR | G_IO_HUP))
691                 {
692                 g_io_channel_shutdown(source, TRUE, NULL);
693                 return FALSE;
694                 }
695
696         return TRUE;
697 }
698
699 typedef enum {
700         PATH_FILE,
701         PATH_FILE_URL,
702         PATH_DEST
703 } PathType;
704
705
706 static gchar *editor_command_path_parse(const FileData *fd, gboolean consider_sidecars, PathType type, const EditorDescription *editor)
707 {
708         GString *string;
709         gchar *pathl;
710         const gchar *p = NULL;
711
712         DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);
713
714         string = g_string_new("");
715
716         if (type == PATH_FILE || type == PATH_FILE_URL)
717                 {
718                 GList *work = editor->ext_list;
719
720                 if (!work)
721                         p = fd->path;
722                 else
723                         {
724                         while (work)
725                                 {
726                                 GList *work2;
727                                 gchar *ext = static_cast<gchar *>(work->data);
728                                 work = work->next;
729
730                                 if (strcmp(ext, "*") == 0 ||
731                                     g_ascii_strcasecmp(ext, fd->extension) == 0)
732                                         {
733                                         p = fd->path;
734                                         break;
735                                         }
736
737                                 work2 = consider_sidecars ? fd->sidecar_files : NULL;
738                                 while (work2)
739                                         {
740                                         FileData *sfd = static_cast<FileData *>(work2->data);
741                                         work2 = work2->next;
742
743                                         if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
744                                                 {
745                                                 p = sfd->path;
746                                                 break;
747                                                 }
748                                         }
749                                 if (p) break;
750                                 }
751                         if (!p) return NULL;
752                         }
753                 }
754         else if (type == PATH_DEST)
755                 {
756                 if (fd->change && fd->change->dest)
757                         p = fd->change->dest;
758                 else
759                         p = "";
760                 }
761
762         g_assert(p);
763         string = g_string_append(string, p);
764
765         if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
766         pathl = path_from_utf8(string->str);
767         g_string_free(string, TRUE);
768
769         if (pathl && !pathl[0]) /* empty string case */
770                 {
771                 g_free(pathl);
772                 pathl = NULL;
773                 }
774
775         DEBUG_2("editor_command_path_parse: return %s", pathl);
776         return pathl;
777 }
778
779 static GString *append_quoted(GString *str, const char *s, gboolean single_quotes, gboolean double_quotes)
780 {
781         const char *p;
782
783         if (!single_quotes)
784                 {
785                 if (!double_quotes)
786                         g_string_append_c(str, '\'');
787                 else
788                         g_string_append(str, "\"'");
789                 }
790
791         for (p = s; *p != '\0'; p++)
792                 {
793                 if (*p == '\'')
794                         g_string_append(str, "'\\''");
795                 else
796                         g_string_append_c(str, *p);
797                 }
798
799         if (!single_quotes)
800                 {
801                 if (!double_quotes)
802                         g_string_append_c(str, '\'');
803                 else
804                         g_string_append(str, "'\"");
805                 }
806
807         return str;
808 }
809
810
811 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
812 {
813         EditorFlags flags = static_cast<EditorFlags>(0);
814         const gchar *p;
815         GString *result = NULL;
816         gboolean escape = FALSE;
817         gboolean single_quotes = FALSE;
818         gboolean double_quotes = FALSE;
819
820         DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);
821
822         if (output)
823                 result = g_string_new("");
824
825         if (editor->exec == NULL || editor->exec[0] == '\0')
826                 {
827                 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_EMPTY);
828                 goto err;
829                 }
830
831         p = editor->exec;
832         /* skip leading whitespaces if any */
833         while (g_ascii_isspace(*p)) p++;
834
835         /* command */
836
837         while (*p)
838                 {
839                 if (escape)
840                         {
841                         escape = FALSE;
842                         if (output) result = g_string_append_c(result, *p);
843                         }
844                 else if (*p == '\\')
845                         {
846                         if (!single_quotes) escape = TRUE;
847                         if (output) result = g_string_append_c(result, *p);
848                         }
849                 else if (*p == '\'')
850                         {
851                         if (output) result = g_string_append_c(result, *p);
852                         if (!single_quotes && !double_quotes)
853                                 single_quotes = TRUE;
854                         else if (single_quotes)
855                                 single_quotes = FALSE;
856                         }
857                 else if (*p == '"')
858                         {
859                         if (output) result = g_string_append_c(result, *p);
860                         if (!single_quotes && !double_quotes)
861                                 double_quotes = TRUE;
862                         else if (double_quotes)
863                                 double_quotes = FALSE;
864                         }
865                 else if (*p == '%' && p[1])
866                         {
867                         gchar *pathl = NULL;
868
869                         p++;
870
871                         switch (*p)
872                                 {
873                                 case 'f': /* single file */
874                                 case 'u': /* single url */
875                                         flags = static_cast<EditorFlags>(flags | EDITOR_FOR_EACH);
876                                         if (flags & EDITOR_SINGLE_COMMAND)
877                                                 {
878                                                 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
879                                                 goto err;
880                                                 }
881                                         if (list)
882                                                 {
883                                                 /* use the first file from the list */
884                                                 if (!list->data)
885                                                         {
886                                                         flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
887                                                         goto err;
888                                                         }
889                                                 pathl = editor_command_path_parse((FileData *)list->data,
890                                                                                   consider_sidecars,
891                                                                                   (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
892                                                                                   editor);
893                                                 if (!output)
894                                                         {
895                                                         /* just testing, check also the rest of the list (like with F and U)
896                                                            any matching file is OK */
897                                                         GList *work = list->next;
898
899                                                         while (!pathl && work)
900                                                                 {
901                                                                 FileData *fd = static_cast<FileData *>(work->data);
902                                                                 pathl = editor_command_path_parse(fd,
903                                                                                                   consider_sidecars,
904                                                                                                   (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
905                                                                                                   editor);
906                                                                 work = work->next;
907                                                                 }
908                                                         }
909
910                                                 if (!pathl)
911                                                         {
912                                                         flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
913                                                         goto err;
914                                                         }
915                                                 if (output)
916                                                         {
917                                                         result = append_quoted(result, pathl, single_quotes, double_quotes);
918                                                         }
919                                                 g_free(pathl);
920                                                 }
921                                         break;
922
923                                 case 'F':
924                                 case 'U':
925                                         flags = static_cast<EditorFlags>(flags | EDITOR_SINGLE_COMMAND);
926                                         if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
927                                                 {
928                                                 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
929                                                 goto err;
930                                                 }
931
932                                         if (list)
933                                                 {
934                                                 /* use whole list */
935                                                 GList *work = list;
936                                                 gboolean ok = FALSE;
937
938                                                 while (work)
939                                                         {
940                                                         FileData *fd = static_cast<FileData *>(work->data);
941                                                         pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
942                                                         if (pathl)
943                                                                 {
944                                                                 ok = TRUE;
945
946                                                                 if (output)
947                                                                         {
948                                                                         ok = TRUE;
949                                                                         if (work != list) g_string_append_c(result, ' ');
950                                                                         result = append_quoted(result, pathl, single_quotes, double_quotes);
951                                                                         }
952                                                                 g_free(pathl);
953                                                                 }
954                                                         work = work->next;
955                                                         }
956                                                 if (!ok)
957                                                         {
958                                                         flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
959                                                         goto err;
960                                                         }
961                                                 }
962                                         break;
963                                 case 'i':
964                                         if (editor->icon && *editor->icon)
965                                                 {
966                                                 if (output)
967                                                         {
968                                                         result = g_string_append(result, "--icon ");
969                                                         result = append_quoted(result, editor->icon, single_quotes, double_quotes);
970                                                         }
971                                                 }
972                                         break;
973                                 case 'c':
974                                         if (output)
975                                                 {
976                                                 result = append_quoted(result, editor->name, single_quotes, double_quotes);
977                                                 }
978                                         break;
979                                 case 'k':
980                                         if (output)
981                                                 {
982                                                 result = append_quoted(result, editor->file, single_quotes, double_quotes);
983                                                 }
984                                         break;
985                                 case '%':
986                                         /* %% = % escaping */
987                                         if (output) result = g_string_append_c(result, *p);
988                                         break;
989                                 case 'd':
990                                 case 'D':
991                                 case 'n':
992                                 case 'N':
993                                 case 'v':
994                                 case 'm':
995                                         /* deprecated according to spec, ignore */
996                                         break;
997                                 default:
998                                         flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_SYNTAX);
999                                         goto err;
1000                                 }
1001                         }
1002                 else
1003                         {
1004                         if (output) result = g_string_append_c(result, *p);
1005                         }
1006                 p++;
1007                 }
1008
1009         if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags = static_cast<EditorFlags>(flags | EDITOR_NO_PARAM);
1010
1011         if (output)
1012                 {
1013                 *output = g_string_free(result, FALSE);
1014                 DEBUG_3("Editor cmd: %s", *output);
1015                 }
1016
1017         return flags;
1018
1019
1020 err:
1021         if (output)
1022                 {
1023                 g_string_free(result, TRUE);
1024                 *output = NULL;
1025                 }
1026         return flags;
1027 }
1028
1029
1030 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
1031 {
1032         EditorData *ed = static_cast<EditorData *>(data);
1033         g_spawn_close_pid(pid);
1034         ed->pid = -1;
1035
1036         editor_command_next_finish(ed, status);
1037 }
1038
1039
1040 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
1041 {
1042         gchar *command;
1043         FileData *fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? NULL : list->data);;
1044         GPid pid;
1045         gint standard_output;
1046         gint standard_error;
1047         gboolean ok;
1048
1049         ed->pid = -1;
1050         ed->flags = editor->flags;
1051         ed->flags = static_cast<EditorFlags>(ed->flags | editor_command_parse(editor, list, TRUE, &command));
1052
1053         ok = !EDITOR_ERRORS(ed->flags);
1054
1055         if (ok)
1056                 {
1057                 ok = (options->shell.path && *options->shell.path);
1058                 if (!ok) log_printf("ERROR: empty shell command\n");
1059
1060                 if (ok)
1061                         {
1062                         ok = (access(options->shell.path, X_OK) == 0);
1063                         if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
1064                         }
1065
1066                 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1067                 }
1068
1069         if (ok)
1070                 {
1071                 gchar *working_directory;
1072                 gchar *args[4];
1073                 guint n = 0;
1074
1075                 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
1076                 args[n++] = options->shell.path;
1077                 if (options->shell.options && *options->shell.options)
1078                         args[n++] = options->shell.options;
1079                 args[n++] = command;
1080                 args[n] = NULL;
1081
1082                 if ((ed->flags & EDITOR_DEST) && fd && fd->change && fd->change->dest) /** @FIXME error handling */
1083                         {
1084                         g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
1085                         }
1086                 else
1087                         {
1088                         g_unsetenv("GEEQIE_DESTINATION");
1089                         }
1090
1091                 ok = g_spawn_async_with_pipes(working_directory, args, NULL,
1092                                       G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1093                                       NULL, NULL,
1094                                       &pid,
1095                                       NULL,
1096                                       ed->vd ? &standard_output : NULL,
1097                                       ed->vd ? &standard_error : NULL,
1098                                       NULL);
1099
1100                 g_free(working_directory);
1101
1102                 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1103                 }
1104
1105         if (ok)
1106                 {
1107                 g_child_watch_add(pid, editor_child_exit_cb, ed);
1108                 ed->pid = pid;
1109                 }
1110
1111         if (ed->vd)
1112                 {
1113                 if (!ok)
1114                         {
1115                         gchar *buf;
1116
1117                         buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1118                         editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1119                         g_free(buf);
1120
1121                         }
1122                 else
1123                         {
1124                         GIOChannel *channel_output;
1125                         GIOChannel *channel_error;
1126
1127                         channel_output = g_io_channel_unix_new(standard_output);
1128                         g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, NULL);
1129                         g_io_channel_set_encoding(channel_output, NULL, NULL);
1130
1131                         g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1132                                             editor_verbose_io_cb, ed, NULL);
1133                         g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1134                                             editor_verbose_io_cb, ed, NULL);
1135                         g_io_channel_unref(channel_output);
1136
1137                         channel_error = g_io_channel_unix_new(standard_error);
1138                         g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, NULL);
1139                         g_io_channel_set_encoding(channel_error, NULL, NULL);
1140
1141                         g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1142                                             editor_verbose_io_cb, ed, NULL);
1143                         g_io_channel_unref(channel_error);
1144                         }
1145                 }
1146
1147         g_free(command);
1148
1149         return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1150 }
1151
1152 static EditorFlags editor_command_next_start(EditorData *ed)
1153 {
1154         if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1155
1156         if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1157                 {
1158                 FileData *fd;
1159                 EditorFlags error;
1160
1161                 fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? NULL : ed->list->data);
1162
1163                 if (ed->vd)
1164                         {
1165                         if ((ed->flags & EDITOR_FOR_EACH) && fd)
1166                                 editor_verbose_window_progress(ed, fd->path);
1167                         else
1168                                 editor_verbose_window_progress(ed, _("running..."));
1169                         }
1170                 ed->count++;
1171
1172                 error = editor_command_one(ed->editor, ed->list, ed);
1173                 if (!error && ed->vd)
1174                         {
1175                         gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != NULL) );
1176                         if ((ed->flags & EDITOR_FOR_EACH) && fd)
1177                                 {
1178                                 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1179                                 editor_verbose_window_fill(ed->vd, "\n", 1);
1180                                 }
1181                         }
1182
1183                 if (!error)
1184                         return static_cast<EditorFlags>(0);
1185
1186                 /* command was not started, call the finish immediately */
1187                 return editor_command_next_finish(ed, 0);
1188                 }
1189
1190         /* everything is done */
1191         return editor_command_done(ed);
1192 }
1193
1194 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1195 {
1196         gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1197
1198         if (status)
1199                 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_STATUS);
1200
1201         if (ed->flags & EDITOR_FOR_EACH)
1202                 {
1203                 /* handle the first element from the list */
1204                 GList *fd_element = ed->list;
1205
1206                 ed->list = g_list_remove_link(ed->list, fd_element);
1207                 if (ed->callback)
1208                         {
1209                         cont = ed->callback(ed->list ? ed : NULL, ed->flags, fd_element, ed->data);
1210                         if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1211                         }
1212                 filelist_free(fd_element);
1213                 }
1214         else
1215                 {
1216                 /* handle whole list */
1217                 if (ed->callback)
1218                         cont = ed->callback(NULL, ed->flags, ed->list, ed->data);
1219                 filelist_free(ed->list);
1220                 ed->list = NULL;
1221                 }
1222
1223         switch (cont)
1224                 {
1225                 case EDITOR_CB_SUSPEND:
1226                         return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1227                 case EDITOR_CB_SKIP:
1228                         return editor_command_done(ed);
1229                 }
1230
1231         return editor_command_next_start(ed);
1232 }
1233
1234 static EditorFlags editor_command_done(EditorData *ed)
1235 {
1236         EditorFlags flags;
1237
1238         if (ed->vd)
1239                 {
1240                 if (ed->count == ed->total)
1241                         {
1242                         editor_verbose_window_progress(ed, _("done"));
1243                         }
1244                 else
1245                         {
1246                         editor_verbose_window_progress(ed, _("stopped by user"));
1247                         }
1248                 editor_verbose_window_enable_close(ed->vd);
1249                 }
1250
1251         /* free the not-handled items */
1252         if (ed->list)
1253                 {
1254                 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_SKIPPED);
1255                 if (ed->callback) ed->callback(NULL, ed->flags, ed->list, ed->data);
1256                 filelist_free(ed->list);
1257                 ed->list = NULL;
1258                 }
1259
1260         ed->count = 0;
1261
1262         flags = static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1263
1264         if (!ed->vd) editor_data_free(ed);
1265
1266         return flags;
1267 }
1268
1269 void editor_resume(gpointer ed)
1270 {
1271         editor_command_next_start(reinterpret_cast<EditorData *>(ed));
1272 }
1273
1274 void editor_skip(gpointer ed)
1275 {
1276         editor_command_done(static_cast<EditorData *>(ed));
1277 }
1278
1279 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1280 {
1281         EditorData *ed;
1282         EditorFlags flags = editor->flags;
1283
1284         if (EDITOR_ERRORS(flags)) return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1285
1286         ed = g_new0(EditorData, 1);
1287         ed->list = filelist_copy(list);
1288         ed->flags = flags;
1289         ed->editor = editor;
1290         ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1291         ed->callback = cb;
1292         ed->data = data;
1293         ed->working_directory = g_strdup(working_directory);
1294
1295         if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1296                 flags = static_cast<EditorFlags>(flags | EDITOR_VERBOSE);
1297
1298         if (flags & EDITOR_VERBOSE)
1299                 editor_verbose_window(ed, text);
1300
1301         editor_command_next_start(ed);
1302         /* errors from editor_command_next_start will be handled via callback */
1303         return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1304 }
1305
1306 gboolean is_valid_editor_command(const gchar *key)
1307 {
1308         if (!key) return FALSE;
1309         return g_hash_table_lookup(editors, key) != NULL;
1310 }
1311
1312 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1313 {
1314         EditorFlags error;
1315         EditorDescription *editor;
1316         if (!key) return EDITOR_ERROR_EMPTY;
1317
1318         editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1319
1320         if (!editor) return EDITOR_ERROR_EMPTY;
1321         if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1322
1323         error = editor_command_parse(editor, list, TRUE, NULL);
1324
1325         if (EDITOR_ERRORS(error)) return error;
1326
1327         error = static_cast<EditorFlags>(error | editor_command_start(editor, editor->name, list, working_directory, cb, data));
1328
1329         if (EDITOR_ERRORS(error))
1330                 {
1331                 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1332
1333                 file_util_warning_dialog(_("Invalid editor command"), text, GTK_STOCK_DIALOG_ERROR, NULL);
1334                 g_free(text);
1335                 }
1336
1337         return static_cast<EditorFlags>(EDITOR_ERRORS(error));
1338 }
1339
1340 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1341 {
1342         return start_editor_from_filelist_full(key, list, NULL, NULL, NULL);
1343 }
1344
1345 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1346 {
1347         GList *list;
1348         EditorFlags error;
1349
1350         if (!fd) return static_cast<EditorFlags>(FALSE);
1351
1352         list = g_list_append(NULL, fd);
1353         error = start_editor_from_filelist_full(key, list, NULL, cb, data);
1354         g_list_free(list);
1355         return error;
1356 }
1357
1358 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1359 {
1360         return start_editor_from_file_full(key, fd, NULL, NULL);
1361 }
1362
1363 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1364 {
1365         return start_editor_from_filelist_full(key, NULL, working_directory, NULL, NULL);
1366 }
1367
1368 gboolean editor_window_flag_set(const gchar *key)
1369 {
1370         EditorDescription *editor;
1371         if (!key) return TRUE;
1372
1373         editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1374         if (!editor) return TRUE;
1375
1376         return !!(editor->flags & EDITOR_KEEP_FS);
1377 }
1378
1379 gboolean editor_is_filter(const gchar *key)
1380 {
1381         EditorDescription *editor;
1382         if (!key) return TRUE;
1383
1384         editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1385         if (!editor) return TRUE;
1386
1387         return !!(editor->flags & EDITOR_DEST);
1388 }
1389
1390 gboolean editor_no_param(const gchar *key)
1391 {
1392         EditorDescription *editor;
1393         if (!key) return FALSE;
1394
1395         editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1396         if (!editor) return FALSE;
1397
1398         return !!(editor->flags & EDITOR_NO_PARAM);
1399 }
1400
1401 gboolean editor_blocks_file(const gchar *key)
1402 {
1403         EditorDescription *editor;
1404         if (!key) return FALSE;
1405
1406         editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1407         if (!editor) return FALSE;
1408
1409         /* Decide if the image file should be blocked during editor execution
1410            Editors like gimp can be used long time after the original file was
1411            saved, for editing unrelated files.
1412            %f vs. %F seems to be a good heuristic to detect this kind of editors.
1413         */
1414
1415         return !(editor->flags & EDITOR_SINGLE_COMMAND);
1416 }
1417
1418 const gchar *editor_get_error_str(EditorFlags flags)
1419 {
1420         if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1421         if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1422         if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1423         if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1424         if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1425         if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1426         if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1427         return _("Unknown error.");
1428 }
1429
1430 //const gchar *editor_get_name(const gchar *key)
1431 //{
1432         //EditorDescription *editor = g_hash_table_lookup(editors, key);
1433
1434         //if (!editor) return NULL;
1435
1436         //return editor->name;
1437 //}
1438 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */