1 /*
2  * This file is part of Siril, an astronomy image processor.
3  * Copyright (C) 2005-2011 Francois Meyer (dulle at free.fr)
4  * Copyright (C) 2012-2021 team free-astro (see more in AUTHORS file)
5  * Reference site is https://free-astro.org/index.php/Siril
6  *
7  * Siril 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 3 of the License, or
10  * (at your option) any later version.
11  *
12  * Siril 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
18  * along with Siril. If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <ctype.h>
24 
25 #include "core/siril.h"
26 #include "core/proto.h"
27 #include "core/initfile.h"
28 #include "core/OS_utils.h"
29 #include "gui/utils.h"
30 #include "gui/progress_and_log.h"
31 #include "gui/utils.h"
32 #include "gui/callbacks.h"
33 #include "core/processing.h"
34 #include "core/command_list.h"
35 #include "io/sequence.h"
36 
37 #include "command_line_processor.h"
38 
parse_line(char * myline,int len,int * nb)39 static void parse_line(char *myline, int len, int *nb) {
40 	int i = 0, wordnb = 0;
41 	char string_starter = '\0';	// quotes don't split words on spaces
42 	word[0] = NULL;
43 
44 	do {
45 		while (i < len && isblank(myline[i]))
46 			i++;
47 		if (myline[i] == '"' || myline[i] == '\'')
48 			string_starter = myline[i++];
49 		if (myline[i] == '\0' || myline[i] == '\n' || myline[i] == '\r')
50 			break;
51 		word[wordnb++] = myline + i;	// the beginning of the word
52 		word[wordnb] = NULL;		// put next word to NULL
53 		do {
54 			i++;
55 			if (string_starter != '\0' && myline[i] == string_starter) {
56 				string_starter = '\0';
57 				break;
58 			}
59 		} while (i < len && (!isblank(myline[i]) || string_starter != '\0')
60 				&& myline[i] != '\r' && myline[i] != '\n');
61 		if (myline[i] == '\0')	// the end of the word and line (i == len)
62 			break;
63 		myline[i++] = '\0';		// the end of the word
64 	} while (wordnb < MAX_COMMAND_WORDS - 1);
65 	*nb = wordnb;
66 }
67 
remove_trailing_cr(char * str)68 static void remove_trailing_cr(char *str) {
69 	if (str == NULL || str[0] == '\0')
70 		return;
71 	int length = strlen(str);
72 	if (str[length - 1] == '\r')
73 		str[length - 1] = '\0';
74 }
75 
execute_command(int wordnb)76 static int execute_command(int wordnb) {
77 	// search for the command in the list
78 	if (word[0] == NULL) return 1;
79 	int i = G_N_ELEMENTS(commands);
80 	while (g_ascii_strcasecmp(commands[--i].name, word[0])) {
81 		if (i == 0) {
82 			siril_log_message(_("Unknown command: '%s' or not implemented yet\n"), word[0]);
83 			return 1 ;
84 		}
85 	}
86 
87 	// verify argument count
88 	if (wordnb - 1 < commands[i].nbarg) {
89 		siril_log_message(_("Usage: %s\n"), commands[i].usage);
90 		return 1;
91 	}
92 
93 	// verify if command is scriptable
94 	if (com.script) {
95 		if (!commands[i].scriptable) {
96 			siril_log_message(_("This command cannot be used in a script: %s\n"), commands[i].name);
97 			return 1;
98 		}
99 	}
100 
101 	// process the command
102 	siril_log_color_message(_("Running command: %s\n"), "salmon", word[0]);
103 	fprintf(stdout, "%lu: running command %s\n", time(NULL), word[0]);
104 	return commands[i].process(wordnb);
105 }
106 
update_log_icon(gboolean is_running)107 static void update_log_icon(gboolean is_running) {
108 	GtkImage *image = GTK_IMAGE(lookup_widget("image_log"));
109 	if (is_running)
110 		gtk_image_set_from_icon_name(image, "gtk-yes", GTK_ICON_SIZE_LARGE_TOOLBAR);
111 	else
112 		gtk_image_set_from_icon_name(image, "gtk-no", GTK_ICON_SIZE_LARGE_TOOLBAR);
113 }
114 
115 struct log_status_bar_idle_data {
116 	gchar *myline;
117 	int line;
118 };
119 
log_status_bar_idle_callback(gpointer p)120 static gboolean log_status_bar_idle_callback(gpointer p) {
121 	struct log_status_bar_idle_data *data = (struct log_status_bar_idle_data *) p;
122 
123 	GtkStatusbar *statusbar_script = GTK_STATUSBAR(lookup_widget("statusbar_script"));
124 	gchar *status;
125 	gchar *newline;
126 
127 	update_log_icon(TRUE);
128 
129 	newline = g_strdup(data->myline);
130 	status = g_strdup_printf(_("Processing line %d: %s"), data->line, newline);
131 
132 	gtk_statusbar_push(statusbar_script, 0, status);
133 	g_free(newline);
134 	g_free(status);
135 	g_free(data->myline);
136 	free(data);
137 
138 	return FALSE;	// only run once
139 }
140 
display_command_on_status_bar(int line,char * myline)141 static void display_command_on_status_bar(int line, char *myline) {
142 	if (!com.headless) {
143 		struct log_status_bar_idle_data *data;
144 
145 		data = malloc(sizeof(struct log_status_bar_idle_data));
146 		data->line = line;
147 		data->myline = myline ? g_strdup(myline) : NULL;
148 		gdk_threads_add_idle(log_status_bar_idle_callback, data);
149 	}
150 }
151 
clear_status_bar()152 static void clear_status_bar() {
153 	if (!com.headless) {
154 		GtkStatusbar *bar = GTK_STATUSBAR(lookup_widget("statusbar_script"));
155 		gtk_statusbar_remove_all(bar, 0);
156 		update_log_icon(FALSE);
157 	}
158 }
159 
end_script(gpointer p)160 static gboolean end_script(gpointer p) {
161 	clear_status_bar();
162 	set_GUI_CWD();
163 
164 	set_cursor_waiting(FALSE);
165 	return FALSE;
166 }
167 
execute_script(gpointer p)168 gpointer execute_script(gpointer p) {
169 	GInputStream *input_stream = (GInputStream*) p;
170 	gboolean check_required = FALSE;
171 	gchar *buffer;
172 	int line = 0, retval = 0;
173 	int wordnb;
174 	int startmem, endmem;
175 	struct timeval t_start, t_end;
176 
177 	com.script = TRUE;
178 	com.stop_script = FALSE;
179 
180 	gettimeofday(&t_start, NULL);
181 
182 	/* Now we want to save the cwd in order to come back after
183 	 * script execution
184 	 */
185 	gchar *saved_cwd = g_strdup(com.wd);
186 	startmem = get_available_memory() / BYTES_IN_A_MB;
187 	gsize length = 0;
188 	GDataInputStream *data_input = g_data_input_stream_new(input_stream);
189 	while ((buffer = g_data_input_stream_read_line_utf8(data_input, &length,
190 			NULL, NULL))) {
191 		++line;
192 		if (com.stop_script) {
193 			retval = 1;
194 			g_free (buffer);
195 			break;
196 		}
197 		/* Displays comments */
198 		if (buffer[0] == '#') {
199 			siril_log_color_message("%s\n", "blue", buffer);
200 			g_free (buffer);
201 			continue;
202 		}
203 
204 		/* in Windows case, remove trailing CR */
205 		remove_trailing_cr(buffer);
206 
207 		if (buffer[0] == '\0') {
208 			g_free (buffer);
209 			continue;
210 		}
211 
212 
213 		display_command_on_status_bar(line, buffer);
214 		parse_line(buffer, length, &wordnb);
215 		/* check for requires command */
216 		if (!g_ascii_strcasecmp(word[0], "requires")) {
217 			check_required = TRUE;
218 		} else {
219 			if (com.pref.script_check_requires && !check_required) {
220 				siril_log_color_message(_("The \"requires\" command is missing at the top of the script file."
221 						" This command is needed to check script compatibility.\n"), "red");
222 				retval = 1;
223 				g_free (buffer);
224 				break;
225 			}
226 		}
227 		if ((retval = execute_command(wordnb))) {
228 			siril_log_message(_("Error in line %d: '%s'.\n"), line, buffer);
229 			siril_log_message(_("Exiting batch processing.\n"));
230 			g_free (buffer);
231 			break;
232 		}
233 		if (waiting_for_thread()) {
234 			retval = 1;
235 			g_free (buffer);
236 			break;	// abort script on command failure
237 		}
238 		endmem = get_available_memory() / BYTES_IN_A_MB;
239 		siril_debug_print("End of command %s, memory difference: %d MB\n", word[0], startmem - endmem);
240 		startmem = endmem;
241 		memset(word, 0, sizeof word);
242 		g_free (buffer);
243 	}
244 	g_object_unref(data_input);
245 	g_object_unref(input_stream);
246 	com.script = FALSE;
247 	/* Now we want to restore the saved cwd */
248 	siril_change_dir(saved_cwd, NULL);
249 	writeinitfile();
250 	siril_add_idle(end_script, NULL);
251 	if (!retval) {
252 		siril_log_message(_("Script execution finished successfully.\n"));
253 		gettimeofday(&t_end, NULL);
254 		show_time_msg(t_start, t_end, _("Total execution time"));
255 	} else {
256 		char *msg = siril_log_message(_("Script execution failed.\n"));
257 		msg[strlen(msg) - 1] = '\0';
258 		set_progress_bar_data(msg, PROGRESS_DONE);
259 	}
260 	g_free(saved_cwd);
261 	fprintf(stdout, "Script thread exiting\n");
262 	return GINT_TO_POINTER(retval);
263 }
264 
show_command_help_popup(GtkEntry * entry)265 static void show_command_help_popup(GtkEntry *entry) {
266 	gchar *helper = NULL;
267 
268 	const gchar *text = gtk_entry_get_text(entry);
269 	if (*text == '\0') {
270 		helper = g_strdup(_("Please enter an existing command before hitting this button"));
271 	} else {
272 		command *current = commands;
273 		gchar **command_line = g_strsplit_set(text, " ", -1);
274 		while (current->process) {
275 			if (!g_ascii_strcasecmp(current->name, command_line[0])) {
276 				gchar **token;
277 
278 				token = g_strsplit_set(current->usage, " ", -1);
279 				GString *str = g_string_new(token[0]);
280 				str = g_string_prepend(str, "<span foreground=\"red\"><b>");
281 				str = g_string_append(str, "</b>");
282 				if (token[1] != NULL) {
283 					str = g_string_append(str,
284 							current->usage + strlen(token[0]));
285 				}
286 				str = g_string_append(str, "</span>\n\n\t");
287 				str = g_string_append(str, _(current->definition));
288 				str = g_string_append(str, "\n\n<b>");
289 				str = g_string_append(str, _("Can be used in a script: "));
290 				str = g_string_append(str, "<span foreground=\"red\">");
291 				if (current->scriptable) {
292 					str = g_string_append(str, _("YES"));
293 				} else {
294 					str = g_string_append(str, _("NO"));
295 				}
296 				str = g_string_append(str, "</span></b>");
297 				helper = g_string_free(str, FALSE);
298 				g_strfreev(token);
299 				break;
300 			}
301 			current++;
302 		}
303 		g_strfreev(command_line);
304 	}
305 	if (!helper) {
306 		helper = g_strdup(_("No help for this command"));
307 	}
308 
309 	GtkWidget *popover = popover_new(lookup_widget("command"), helper);
310 #if GTK_CHECK_VERSION(3, 22, 0)
311 	gtk_popover_popup(GTK_POPOVER(popover));
312 #else
313 	gtk_widget_show(popover);
314 #endif
315 	g_free(helper);
316 }
317 
processcommand(const char * line)318 int processcommand(const char *line) {
319 	int wordnb = 0;
320 	gchar *myline;
321 	GError *error = NULL;
322 
323 	if (line[0] == '\0' || line[0] == '\n')
324 		return 0;
325 	if (line[0] == '@') { // case of files
326 		if (get_thread_run()) {
327 			PRINT_ANOTHER_THREAD_RUNNING;
328 			return 1;
329 		}
330 		if (com.script_thread)
331 			g_thread_join(com.script_thread);
332 
333 		/* Switch to console tab */
334 		control_window_switch_to_tab(OUTPUT_LOGS);
335 
336 		char filename[256];
337 		g_strlcpy(filename, line + 1, 250);
338 		expand_home_in_filename(filename, 256);
339 
340 		GFile *file = g_file_new_for_path(filename);
341 		GInputStream *input_stream = (GInputStream *)g_file_read(file, NULL, &error);
342 
343 		if (input_stream == NULL) {
344 			if (error != NULL) {
345 				g_clear_error(&error);
346 				siril_log_message(_("File [%s] does not exist\n"), filename);
347 			}
348 
349 			g_object_unref(file);
350 			return 1;
351 		}
352 		/* ensure that everything is closed */
353 		process_close(0);
354 		/* Then, run script */
355 		siril_log_message(_("Starting script %s\n"), filename);
356 		com.script_thread = g_thread_new("script", execute_script, input_stream);
357 		g_object_unref(file);
358 	} else {
359 		/* Switch to console tab */
360 		control_window_switch_to_tab(OUTPUT_LOGS);
361 
362 		myline = strdup(line);
363 		int len = strlen(line);
364 		parse_line(myline, len, &wordnb);
365 		if (execute_command(wordnb)) {
366 			siril_log_color_message(_("Command execution failed.\n"), "red");
367 			if (!com.script && !com.headless) {
368 				show_command_help_popup(GTK_ENTRY(lookup_widget("command")));
369 			}
370 			free(myline);
371 			return 1;
372 		}
373 		free(myline);
374 	}
375 	set_cursor_waiting(FALSE);
376 	return 0;
377 }
378 
379 // loads the sequence from com.wd
load_sequence(const char * name,char ** get_filename)380 sequence *load_sequence(const char *name, char **get_filename) {
381 	gchar *file = g_strdup(name);
382 	gchar *altfile = NULL;
383 	if (!g_str_has_suffix(name, ".seq")) {
384 		str_append(&file, ".seq");
385 		if (!g_str_has_suffix(name, "_"))
386 			altfile = g_strdup_printf("%s_.seq", name);
387 	}
388 
389 	if (!is_readable_file(file) && (!altfile || !is_readable_file(altfile))) {
390 		if (check_seq(FALSE)) {
391 			siril_log_message(_("No sequence `%s' found.\n"), name);
392 			g_free(file);
393 			g_free(altfile);
394 			return NULL;
395 		}
396 	}
397 
398 	sequence *seq;
399 	if ((seq = readseqfile(file))) {
400 		if (get_filename) {
401 			*get_filename = file;
402 			file = NULL; // do not free
403 		}
404 	}
405 	else if (altfile && (seq = readseqfile(altfile))) {
406 		if (get_filename) {
407 			*get_filename = altfile;
408 			altfile = NULL; // do not free
409 		}
410 	}
411 	if (!seq)
412 		siril_log_message(_("Loading sequence `%s' failed.\n"), name);
413 	else {
414 		if (seq_check_basic_data(seq, FALSE) == -1) {
415 			free(seq);
416 			seq = NULL;
417 		}
418 	}
419 	g_free(file);
420 	g_free(altfile);
421 	return seq;
422 }
423 
424 /* callback functions */
425 
426 #define COMPLETION_COLUMN 0
427 
on_match_selected(GtkEntryCompletion * widget,GtkTreeModel * model,GtkTreeIter * iter,gpointer user_data)428 static gboolean on_match_selected(GtkEntryCompletion *widget, GtkTreeModel *model,
429 		GtkTreeIter *iter, gpointer user_data) {
430 	const gchar *cmd;
431 	GtkEditable *e = (GtkEditable *) gtk_entry_completion_get_entry(widget);
432 	gchar *s = gtk_editable_get_chars(e, 0, -1);
433 	gint cur_pos = gtk_editable_get_position(e);
434 	gint p = cur_pos;
435 	gchar *end;
436 	gint del_end_pos = -1;
437 
438 	gtk_tree_model_get(model, iter, COMPLETION_COLUMN, &cmd, -1);
439 
440 	end = s + cur_pos;
441 
442 	if (end) {
443 		del_end_pos = end - s + 1;
444 	} else {
445 		del_end_pos = cur_pos;
446 	}
447 
448 	gtk_editable_delete_text(e, 0, del_end_pos);
449 	gtk_editable_insert_text(e, cmd, -1, &p);
450 	gtk_editable_set_position(e, p);
451 
452 	return TRUE;
453 }
454 
completion_match_func(GtkEntryCompletion * completion,const gchar * key,GtkTreeIter * iter,gpointer user_data)455 static gboolean completion_match_func(GtkEntryCompletion *completion,
456 		const gchar *key, GtkTreeIter *iter, gpointer user_data) {
457 	gboolean res = FALSE;
458 	char *tag = NULL;
459 
460 	if (*key == '\0') return FALSE;
461 
462 	GtkTreeModel *model = gtk_entry_completion_get_model(completion);
463 	int column = gtk_entry_completion_get_text_column(completion);
464 
465 	if (gtk_tree_model_get_column_type(model, column) != G_TYPE_STRING)
466 		return FALSE;
467 
468 	gtk_tree_model_get(model, iter, column, &tag, -1);
469 
470 	if (tag) {
471 		char *normalized = g_utf8_normalize(tag, -1, G_NORMALIZE_ALL);
472 		if (normalized) {
473 			char *casefold = g_utf8_casefold(normalized, -1);
474 			if (casefold) {
475 				res = g_strstr_len(casefold, -1, key) != NULL;
476 			}
477 			g_free(casefold);
478 		}
479 		g_free(normalized);
480 		g_free(tag);
481 	}
482 
483 	return res;
484 }
485 
init_completion_command()486 void init_completion_command() {
487 	GtkEntryCompletion *completion = gtk_entry_completion_new();
488 	GtkListStore *model = gtk_list_store_new(1, G_TYPE_STRING);
489 	GtkTreeIter iter;
490 	GtkEntry *entry = GTK_ENTRY(lookup_widget("command"));
491 
492 	gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(model));
493 	gtk_entry_completion_set_text_column(completion, COMPLETION_COLUMN);
494 	gtk_entry_completion_set_minimum_key_length(completion, 2);
495 	gtk_entry_completion_set_popup_completion(completion, TRUE);
496 	gtk_entry_completion_set_inline_completion(completion, TRUE);
497 	gtk_entry_completion_set_popup_single_match(completion, FALSE);
498 	gtk_entry_completion_set_match_func(completion, completion_match_func, NULL, NULL);
499 	gtk_entry_set_completion(entry, completion);
500 	g_signal_connect(G_OBJECT(completion), "match-selected", G_CALLBACK(on_match_selected), NULL);
501 
502 	/* Populate the completion database. */
503 	command *current = commands;
504 
505 	while (current->process){
506 		gtk_list_store_append(model, &iter);
507 		gtk_list_store_set(model, &iter, COMPLETION_COLUMN, current->name, -1);
508 		current++;
509 	}
510 	g_object_unref(model);
511 }
512 
on_GtkCommandHelper_clicked(GtkButton * button,gpointer user_data)513 void on_GtkCommandHelper_clicked(GtkButton *button, gpointer user_data) {
514 	show_command_help_popup((GtkEntry *)user_data);
515 }
516 
517 /** Callbacks **/
518 
519 /*
520  * Command line history static function
521  */
522 
history_add_line(char * line)523 static void history_add_line(char *line) {
524 	if (!com.cmd_history) {
525 		com.cmd_hist_size = CMD_HISTORY_SIZE;
526 		com.cmd_history = calloc(com.cmd_hist_size, sizeof(const char*));
527 		com.cmd_hist_current = 0;
528 		com.cmd_hist_display = 0;
529 	}
530 	com.cmd_history[com.cmd_hist_current] = line;
531 	com.cmd_hist_current++;
532 	// circle at the end
533 	if (com.cmd_hist_current == com.cmd_hist_size)
534 		com.cmd_hist_current = 0;
535 	if (com.cmd_history[com.cmd_hist_current]) {
536 		free(com.cmd_history[com.cmd_hist_current]);
537 		com.cmd_history[com.cmd_hist_current] = NULL;
538 	}
539 	com.cmd_hist_display = com.cmd_hist_current;
540 }
541 
on_command_activate(GtkEntry * entry,gpointer user_data)542 void on_command_activate(GtkEntry *entry, gpointer user_data) {
543 	const gchar *text = gtk_entry_get_text(entry);
544 	history_add_line(strdup(text));
545 	if (!(processcommand(text))) {
546 		gtk_entry_set_text(entry, "");
547 		set_precision_switch();
548 	}
549 }
550 
551 /* handler for the single-line console */
on_command_key_press_event(GtkWidget * widget,GdkEventKey * event,gpointer user_data)552 gboolean on_command_key_press_event(GtkWidget *widget, GdkEventKey *event,
553 		gpointer user_data) {
554 	int handled = 0;
555 	static GtkEntry *entry = NULL;
556 	if (!entry)
557 		entry = GTK_ENTRY(widget);
558 	GtkEditable *editable = GTK_EDITABLE(entry);
559 	int entrylength = 0;
560 
561 	switch (event->keyval) {
562 	case GDK_KEY_Up:
563 		handled = 1;
564 		if (!com.cmd_history)
565 			break;
566 		if (com.cmd_hist_display > 0) {
567 			if (com.cmd_history[com.cmd_hist_display - 1])
568 				--com.cmd_hist_display;
569 			// display previous entry
570 			gtk_entry_set_text(entry, com.cmd_history[com.cmd_hist_display]);
571 		} else if (com.cmd_history[com.cmd_hist_size - 1]) {
572 			// ring back, display previous
573 			com.cmd_hist_display = com.cmd_hist_size - 1;
574 			gtk_entry_set_text(entry, com.cmd_history[com.cmd_hist_display]);
575 		}
576 		entrylength = gtk_entry_get_text_length(entry);
577 		gtk_editable_set_position(editable, entrylength);
578 		break;
579 	case GDK_KEY_Down:
580 		handled = 1;
581 		if (!com.cmd_history)
582 			break;
583 		if (com.cmd_hist_display == com.cmd_hist_current)
584 			break;
585 		if (com.cmd_hist_display == com.cmd_hist_size - 1) {
586 			if (com.cmd_hist_current == 0) {
587 				// ring forward, end
588 				gtk_entry_set_text(entry, "");
589 				com.cmd_hist_display++;
590 			} else if (com.cmd_history[0]) {
591 				// ring forward, display next
592 				com.cmd_hist_display = 0;
593 				gtk_entry_set_text(entry, com.cmd_history[0]);
594 			}
595 		} else {
596 			if (com.cmd_hist_display == com.cmd_hist_current - 1) {
597 				// end
598 				gtk_entry_set_text(entry, "");
599 				com.cmd_hist_display++;
600 			} else if (com.cmd_history[com.cmd_hist_display + 1]) {
601 				// display next
602 				gtk_entry_set_text(entry,
603 						com.cmd_history[++com.cmd_hist_display]);
604 			}
605 		}
606 		entrylength = gtk_entry_get_text_length(entry);
607 		gtk_editable_set_position(editable, entrylength);
608 		break;
609 	case GDK_KEY_Page_Up:
610 	case GDK_KEY_Page_Down:
611 		handled = 1;
612 		// go to first and last in history
613 		break;
614 	}
615 	return (handled == 1);
616 }
617 
on_command_focus_in_event(GtkWidget * widget,GdkEvent * event,gpointer user_data)618 gboolean on_command_focus_in_event(GtkWidget *widget, GdkEvent *event,
619 		gpointer user_data) {
620 
621 	static const gchar * const accelmap[] = {
622 		"win.astrometry", NULL, NULL,
623 
624 		NULL /* Terminating NULL */
625 	};
626 	set_accel_map(accelmap);
627 
628 	return GDK_EVENT_PROPAGATE;
629 }
630 
on_command_focus_out_event(GtkWidget * widget,GdkEvent * event,gpointer user_data)631 gboolean on_command_focus_out_event(GtkWidget *widget, GdkEvent *event,
632 		gpointer user_data) {
633 
634 	static const gchar * const accelmap[] = {
635 		"win.astrometry", "<Primary>a", NULL,
636 
637 		NULL /* Terminating NULL */
638 	};
639 	set_accel_map(accelmap);
640 
641 	return GDK_EVENT_PROPAGATE;
642 }
643