evolution-3.6.4/plugins/external-editor/external-editor.c

No issues found

  1 /*
  2  *
  3  * This program is free software; you can redistribute it and/or
  4  * modify it under the terms of the GNU Lesser General Public
  5  * License as published by the Free Software Foundation; either
  6  * version 2 of the License, or (at your option) version 3.
  7  *
  8  * This program is distributed in the hope that it will be useful,
  9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 11  * Lesser General Public License for more details.
 12  *
 13  * You should have received a copy of the GNU Lesser General Public
 14  * License along with the program; if not, see <http://www.gnu.org/licenses/>
 15  *
 16  *
 17  * Authors:
 18  *		Holger Macht <hmacht@suse.de>
 19  *		based on work by Sankar P <psankar@novell.com>
 20  *
 21  * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
 22  *
 23  */
 24 
 25 #ifdef HAVE_CONFIG_H
 26 #include <config.h>
 27 #endif
 28 
 29 #include <mail/em-config.h>
 30 #include <mail/em-composer-utils.h>
 31 #include <libevolution-utils/e-alert-dialog.h>
 32 #include <e-msg-composer.h>
 33 
 34 #include <glib/gi18n-lib.h>
 35 #include <glib/gstdio.h>
 36 #include <gdk/gdkkeysyms.h>
 37 
 38 #include <sys/stat.h>
 39 #ifdef HAVE_SYS_WAIT_H
 40 #  include <sys/wait.h>
 41 #endif
 42 
 43 #include <stdlib.h>
 44 #include <string.h>
 45 
 46 #define d(x)
 47 
 48 gboolean	e_plugin_ui_init		(GtkUIManager *manager,
 49 						 EMsgComposer *composer);
 50 GtkWidget *	e_plugin_lib_get_configure_widget
 51 						(EPlugin *epl);
 52 static void	ee_editor_command_changed
 53 						(GtkWidget *textbox);
 54 static void	ee_editor_immediate_launch_changed
 55 						(GtkWidget *checkbox);
 56 static void	async_external_editor		(EMsgComposer *composer);
 57 static gboolean	editor_running			(void);
 58 static gboolean	key_press_cb			(GtkWidget *widget,
 59 						 GdkEventKey *event,
 60 						 EMsgComposer *composer);
 61 
 62 /* used to track when the external editor is active */
 63 static GThread *editor_thread;
 64 
 65 gint e_plugin_lib_enable (EPlugin *ep, gint enable);
 66 
 67 gint
 68 e_plugin_lib_enable (EPlugin *ep,
 69                      gint enable)
 70 {
 71 	return 0;
 72 }
 73 
 74 void
 75 ee_editor_command_changed (GtkWidget *textbox)
 76 {
 77 	const gchar *editor;
 78 	GSettings *settings;
 79 
 80 	editor = gtk_entry_get_text (GTK_ENTRY (textbox));
 81 	d (printf ("\n\aeditor is : [%s] \n\a", editor));
 82 
 83 	/* GSettings access for every key-press. Sucky ? */
 84 	settings = g_settings_new ("org.gnome.evolution.plugin.external-editor");
 85 	g_settings_set_string (settings, "command", editor);
 86 	g_object_unref (settings);
 87 }
 88 
 89 void
 90 ee_editor_immediate_launch_changed (GtkWidget *checkbox)
 91 {
 92 	gboolean immediately;
 93 	GSettings *settings;
 94 
 95 	immediately = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (checkbox));
 96 	d (printf ("\n\aimmediate launch is : [%d] \n\a", immediately));
 97 
 98 	settings = g_settings_new ("org.gnome.evolution.plugin.external-editor");
 99 	g_settings_set_boolean (settings, "launch-on-key-press", immediately);
100 	g_object_unref (settings);
101 }
102 
103 GtkWidget *
104 e_plugin_lib_get_configure_widget (EPlugin *epl)
105 {
106 	GtkWidget *vbox, *textbox, *label, *help;
107 	GtkWidget *checkbox;
108 	GSettings *settings;
109 	gchar *editor;
110 	gboolean checked;
111 
112 	vbox = gtk_vbox_new (FALSE, 10);
113 	textbox = gtk_entry_new ();
114 	label = gtk_label_new (_("Command to be executed to launch the editor: "));
115 	help = gtk_label_new (_("For XEmacs use \"xemacs\"\nFor Vim use \"gvim -f\""));
116 	settings = g_settings_new ("org.gnome.evolution.plugin.external-editor");
117 
118 	editor = g_settings_get_string (settings, "command");
119 	if (editor) {
120 		gtk_entry_set_text (GTK_ENTRY (textbox), editor);
121 		g_free (editor);
122 	}
123 
124 	checkbox = gtk_check_button_new_with_label (
125 		_("Automatically launch when a new mail is edited"));
126 	checked = g_settings_get_boolean (settings, "launch-on-key-press");
127 	if (checked)
128 		gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (checkbox), TRUE);
129 	g_object_unref (settings);
130 
131 	gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0);
132 	gtk_box_pack_start (GTK_BOX (vbox), textbox, FALSE, FALSE, 0);
133 	gtk_box_pack_start (GTK_BOX (vbox), help, FALSE, FALSE, 0);
134 	gtk_box_pack_start (GTK_BOX (vbox), checkbox, FALSE, FALSE, 0);
135 
136 	g_signal_connect (
137 		textbox, "changed",
138 		G_CALLBACK (ee_editor_command_changed), textbox);
139 
140 	g_signal_connect (
141 		checkbox, "toggled",
142 		G_CALLBACK (ee_editor_immediate_launch_changed), checkbox);
143 
144 	gtk_widget_show_all (vbox);
145 
146 	return vbox;
147 }
148 
149 static void
150 enable_disable_composer (EMsgComposer *composer,
151                          gboolean enable)
152 {
153 	GtkhtmlEditor *editor;
154 	GtkAction *action;
155 	GtkActionGroup *action_group;
156 
157 	g_return_if_fail (E_IS_MSG_COMPOSER (composer));
158 
159 	editor = GTKHTML_EDITOR (composer);
160 
161 	if (enable)
162 		gtkhtml_editor_run_command (editor, "editable-on");
163 	else
164 		gtkhtml_editor_run_command (editor, "editable-off");
165 
166 	action = GTKHTML_EDITOR_ACTION_EDIT_MENU (composer);
167 	gtk_action_set_sensitive (action, enable);
168 
169 	action = GTKHTML_EDITOR_ACTION_FORMAT_MENU (composer);
170 	gtk_action_set_sensitive (action, enable);
171 
172 	action = GTKHTML_EDITOR_ACTION_INSERT_MENU (composer);
173 	gtk_action_set_sensitive (action, enable);
174 
175 	action_group = gtkhtml_editor_get_action_group (editor, "composer");
176 	gtk_action_group_set_sensitive (action_group, enable);
177 }
178 
179 static void
180 enable_composer (EMsgComposer *composer)
181 {
182 	enable_disable_composer (composer, TRUE);
183 }
184 
185 static void
186 disable_composer (EMsgComposer *composer)
187 {
188 	enable_disable_composer (composer, FALSE);
189 }
190 
191 /* needed because the new thread needs to call g_idle_add () */
192 static gboolean
193 update_composer_text (GArray *array)
194 {
195 	EMsgComposer *composer;
196 	gchar *text;
197 
198 	composer = g_array_index (array, gpointer, 0);
199 	text = g_array_index (array, gpointer, 1);
200 
201 	e_msg_composer_set_body_text (composer, text, FALSE);
202 
203 	enable_composer (composer);
204 
205 	g_free (text);
206 
207 	return FALSE;
208 }
209 
210 struct run_error_dialog_data
211 {
212 	EMsgComposer *composer;
213 	const gchar *text;
214 };
215 
216 /* needed because the new thread needs to call g_idle_add () */
217 static gboolean
218 run_error_dialog (struct run_error_dialog_data *data)
219 {
220 	g_return_val_if_fail (data != NULL, FALSE);
221 
222 	e_alert_run_dialog_for_args (GTK_WINDOW (data->composer), data->text, NULL);
223 	enable_composer (data->composer);
224 
225 	g_free (data);
226 
227 	return FALSE;
228 }
229 
230 static gint
231 numlines (const gchar *text,
232           gint pos)
233 {
234 	gint ctr = 0;
235 	gint lineno = 0;
236 
237 	while (text && *text && ctr <= pos) {
238 		if (*text == '\n')
239 			lineno++;
240 
241 		text++;
242 		ctr++;
243 	}
244 
245 	if (lineno > 0) {
246 		lineno++;
247 	}
248 
249 	return lineno;
250 }
251 
252 void
253 async_external_editor (EMsgComposer *composer)
254 {
255 	gchar *filename = NULL;
256 	gint status = 0;
257 	GSettings *settings;
258 	gchar *editor_cmd_line = NULL, *editor_cmd = NULL, *content;
259 	gint fd, position = -1, offset = -1;
260 
261 	/* prefix temp files with evo so .*vimrc can be setup to recognize them */
262 	fd = g_file_open_tmp ("evoXXXXXX", &filename, NULL);
263 	if (fd > 0) {
264 		gsize length = 0;
265 
266 		close (fd);
267 		d (printf ("\n\aTemporary-file Name is : [%s] \n\a", filename));
268 
269 		/* Push the text (if there is one) from the composer to the file */
270 		content = gtkhtml_editor_get_text_plain (GTKHTML_EDITOR (composer), &length);
271 		g_file_set_contents (filename, content, length, NULL);
272 	} else {
273 		struct run_error_dialog_data *data;
274 
275 		data = g_new0 (struct run_error_dialog_data, 1);
276 		data->composer = composer;
277 		data->text = "org.gnome.evolution.plugins.external-editor:no-temp-file";
278 
279 		g_warning ("Temporary file fd is null");
280 
281 		/* run_error_dialog also calls enable_composer */
282 		g_idle_add ((GSourceFunc) run_error_dialog, data);
283 		return;
284 	}
285 
286 	settings = g_settings_new ("org.gnome.evolution.plugin.external-editor");
287 	editor_cmd = g_settings_get_string (settings, "command");
288 	if (!editor_cmd) {
289 		if (!(editor_cmd = g_strdup (g_getenv ("EDITOR"))))
290 			/* Make gedit the default external editor,
291 			 * if the default schemas are not installed
292 			 * and no $EDITOR is set. */
293 			editor_cmd = g_strdup ("gedit");
294 	}
295 	g_object_unref (settings);
296 
297 	if (g_strrstr (editor_cmd, "vim") != NULL
298 	    && gtk_html_get_cursor_pos (
299 			gtkhtml_editor_get_html (
300 			GTKHTML_EDITOR (composer)), &position, &offset)
301 				&& position >= 0 && offset >= 0) {
302 		gchar *tmp = editor_cmd;
303 		gint lineno;
304 		gboolean set_nofork;
305 
306 		set_nofork = g_strrstr (editor_cmd, "gvim") != NULL;
307 		/* Increment 1 so that entering vim insert mode places you
308 		 * in the same entry position you were at in the html. */
309 		offset++;
310 
311 		/* calculate the line number that the cursor is in */
312 		lineno = numlines (content, position);
313 
314 		editor_cmd = g_strdup_printf (
315 			"%s \"+call cursor(%d,%d)\"%s%s",
316 			tmp, lineno, offset,
317 			set_nofork ? " " : "",
318 			set_nofork ? "--nofork" : "");
319 
320 		g_free (tmp);
321 	}
322 
323 	g_free (content);
324 
325 	editor_cmd_line = g_strconcat (editor_cmd, " ", filename, NULL);
326 
327 	if (!g_spawn_command_line_sync (editor_cmd_line, NULL, NULL, &status, NULL)) {
328 		struct run_error_dialog_data *data;
329 
330 		g_warning ("Unable to launch %s: ", editor_cmd_line);
331 
332 		data = g_new0 (struct run_error_dialog_data, 1);
333 		data->composer = composer;
334 		data->text = "org.gnome.evolution.plugins.external-editor:editor-not-launchable";
335 
336 		/* run_error_dialog also calls enable_composer */
337 		g_idle_add ((GSourceFunc) run_error_dialog, data);
338 
339 		g_free (filename);
340 		g_free (editor_cmd_line);
341 		g_free (editor_cmd);
342 		return;
343 	}
344 	g_free (editor_cmd_line);
345 	g_free (editor_cmd);
346 
347 #ifdef HAVE_SYS_WAIT_H
348 	if (WEXITSTATUS (status) != 0) {
349 #else
350 	if (status) {
351 #endif
352 		d (printf ("\n\nsome problem here with external editor\n\n"));
353 		g_idle_add ((GSourceFunc) enable_composer, composer);
354 		return;
355 	} else {
356 		gchar *buf;
357 
358 		if (g_file_get_contents (filename, &buf, NULL, NULL)) {
359 			gchar *htmltext;
360 			GArray *array;
361 
362 			htmltext = camel_text_to_html (buf, CAMEL_MIME_FILTER_TOHTML_PRE, 0);
363 
364 			array = g_array_sized_new (
365 				TRUE, TRUE,
366 				sizeof (gpointer), 2 * sizeof (gpointer));
367 			array = g_array_append_val (array, composer);
368 			array = g_array_append_val (array, htmltext);
369 
370 			g_idle_add ((GSourceFunc) update_composer_text, array);
371 
372 			/* We no longer need that temporary file */
373 			g_remove (filename);
374 			g_free (filename);
375 		}
376 	}
377 }
378 
379 static void launch_editor (GtkAction *action, EMsgComposer *composer)
380 {
381 	d (printf ("\n\nexternal_editor plugin is launched \n\n"));
382 
383 	if (editor_running ()) {
384 		d (printf ("not opening editor, because it's still running\n"));
385 		return;
386 	}
387 
388 	disable_composer (composer);
389 
390 	editor_thread = g_thread_create (
391 		(GThreadFunc) async_external_editor, composer, FALSE, NULL);
392 }
393 
394 static GtkActionEntry entries[] = {
395 	{ "ExternalEditor",
396 	  GTK_STOCK_EDIT,
397 	  N_("Compose in External Editor"),
398 	  "<Shift><Control>e",
399 	  N_("Compose in External Editor"),
400 	  G_CALLBACK (launch_editor) }
401 };
402 
403 static gboolean
404 key_press_cb (GtkWidget *widget,
405               GdkEventKey *event,
406               EMsgComposer *composer)
407 {
408 	GSettings *settings;
409 	gboolean immediately;
410 
411 	/* we don't want to start the editor on any modifier keys */
412 	switch (event->keyval) {
413 	case GDK_KEY_Alt_L:
414 	case GDK_KEY_Alt_R:
415 	case GDK_KEY_Super_L:
416 	case GDK_KEY_Super_R:
417 	case GDK_KEY_Control_L:
418 	case GDK_KEY_Control_R:
419 		return FALSE;
420 	default:
421 		break;
422 	}
423 
424 	settings = g_settings_new ("org.gnome.evolution.plugin.external-editor");
425 	immediately = g_settings_get_boolean (settings, "launch-on-key-press");
426 	g_object_unref (settings);
427 	if (!immediately)
428 		return FALSE;
429 
430 	launch_editor (NULL, composer);
431 
432 	return TRUE;
433 }
434 
435 static void
436 editor_running_thread_func (GThread *thread,
437                             gpointer running)
438 {
439 	if (thread == editor_thread)
440 		*(gboolean*)running = TRUE;
441 }
442 
443 /* Racy? */
444 static gboolean
445 editor_running (void)
446 {
447 	gboolean running = FALSE;
448 
449 	g_thread_foreach ((GFunc) editor_running_thread_func, &running);
450 
451 	return running;
452 }
453 
454 static gboolean
455 delete_cb (GtkWidget *widget,
456            EMsgComposer *composer)
457 {
458 	if (editor_running ()) {
459 		e_alert_run_dialog_for_args (
460 			NULL, "org.gnome.evolution.plugins."
461 			"external-editor:editor-still-running", NULL);
462 		return TRUE;
463 	}
464 
465 	return FALSE;
466 }
467 
468 gboolean
469 e_plugin_ui_init (GtkUIManager *manager,
470                   EMsgComposer *composer)
471 {
472 	GtkhtmlEditor *editor;
473 	EWebViewGtkHTML *web_view;
474 
475 	editor = GTKHTML_EDITOR (composer);
476 
477 	/* Add actions to the "composer" action group. */
478 	gtk_action_group_add_actions (
479 		gtkhtml_editor_get_action_group (editor, "composer"),
480 		entries, G_N_ELEMENTS (entries), composer);
481 
482 	web_view = e_msg_composer_get_web_view (composer);
483 
484 	g_signal_connect (
485 		web_view, "key_press_event",
486 		G_CALLBACK (key_press_cb), composer);
487 
488 	g_signal_connect (
489 		web_view, "delete-event",
490 		G_CALLBACK (delete_cb), composer);
491 
492 	return TRUE;
493 }