Fix bug #3380. Initialize widget before callback handler for 'clicked'
[claws.git] / src / plugins / python / python-shell.c
1 /*
2  * Copyright (c) 2008-2009  Christian Hammond
3  * Copyright (c) 2008-2009  David Trowbridge
4  *
5  * Permission is hereby granted, free of charge, to any person obtaining a
6  * copy of this software and associated documentation files (the "Software"),
7  * to deal in the Software without restriction, including without limitation
8  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
9  * and/or sell copies of the Software, and to permit persons to whom the
10  * Software is furnished to do so, subject to the following conditions:
11  *
12  * The above copyright notice and this permission notice shall be included
13  * in all copies or substantial portions of the Software.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21  * THE SOFTWARE.
22  */
23 #include <gdk/gdkkeysyms.h>
24 #include <string.h>
25
26 #include "python-hooks.h"
27 #include "python-shell.h"
28
29 #define MAX_HISTORY_LENGTH 20
30
31 #define PARASITE_PYTHON_SHELL_GET_PRIVATE(obj) \
32     (G_TYPE_INSTANCE_GET_PRIVATE((obj), PARASITE_TYPE_PYTHON_SHELL, \
33                                  ParasitePythonShellPrivate))
34
35 typedef struct
36 {
37     GtkWidget *textview;
38
39     GtkTextMark *scroll_mark;
40     GtkTextMark *line_start_mark;
41
42     GQueue *history;
43     GList *cur_history_item;
44
45     GString *pending_command;
46     gboolean in_block;
47
48 } ParasitePythonShellPrivate;
49
50 enum
51 {
52     LAST_SIGNAL
53 };
54
55
56 /* Widget functions */
57 static void parasite_python_shell_finalize(GObject *obj);
58
59 /* Python integration */
60 static void parasite_python_shell_write_prompt(GtkWidget *python_shell);
61 static char *parasite_python_shell_get_input(GtkWidget *python_shell);
62
63 /* Callbacks */
64 static gboolean parasite_python_shell_key_press_cb(GtkWidget *textview,
65                                                    GdkEventKey *event,
66                                                    GtkWidget *python_shell);
67
68
69 static GtkVBoxClass *parent_class = NULL;
70 //static guint signals[LAST_SIGNAL] = {0};
71
72 G_DEFINE_TYPE(ParasitePythonShell, parasite_python_shell, GTK_TYPE_VBOX);
73
74
75 static void
76 parasite_python_shell_class_init(ParasitePythonShellClass *klass)
77 {
78     GObjectClass *object_class = G_OBJECT_CLASS(klass);
79
80     parent_class = g_type_class_peek_parent(klass);
81
82     object_class->finalize = parasite_python_shell_finalize;
83
84     g_type_class_add_private(klass, sizeof(ParasitePythonShellPrivate));
85 }
86
87 static void
88 parasite_python_shell_init(ParasitePythonShell *python_shell)
89 {
90     ParasitePythonShellPrivate *priv =
91         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
92     GtkWidget *swin;
93     GtkTextBuffer *buffer;
94     GtkTextIter iter;
95     PangoFontDescription *font_desc;
96
97     priv->history = g_queue_new();
98
99     gtk_box_set_spacing(GTK_BOX(python_shell), 6);
100
101     swin = gtk_scrolled_window_new(NULL, NULL);
102     gtk_widget_show(swin);
103     gtk_box_pack_start(GTK_BOX(python_shell), swin, TRUE, TRUE, 0);
104     gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swin),
105                                    GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
106     gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(swin),
107                                         GTK_SHADOW_IN);
108
109     priv->textview = gtk_text_view_new();
110     gtk_widget_show(priv->textview);
111     gtk_container_add(GTK_CONTAINER(swin), priv->textview);
112     gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(priv->textview), TRUE);
113     gtk_text_view_set_pixels_above_lines(GTK_TEXT_VIEW(priv->textview), 3);
114     gtk_text_view_set_left_margin(GTK_TEXT_VIEW(priv->textview), 3);
115     gtk_text_view_set_right_margin(GTK_TEXT_VIEW(priv->textview), 3);
116
117     g_signal_connect(G_OBJECT(priv->textview), "key_press_event",
118                      G_CALLBACK(parasite_python_shell_key_press_cb),
119                      python_shell);
120
121     /* Make the textview monospaced */
122     font_desc = pango_font_description_from_string("monospace");
123     pango_font_description_set_size(font_desc, 10 * PANGO_SCALE);
124     gtk_widget_modify_font(priv->textview, font_desc);
125     pango_font_description_free(font_desc);
126
127     /* Create the end-of-buffer mark */
128     buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview));
129     gtk_text_buffer_get_end_iter(buffer, &iter);
130     priv->scroll_mark = gtk_text_buffer_create_mark(buffer, "scroll_mark",
131                                                     &iter, FALSE);
132
133     /* Create the beginning-of-line mark */
134     priv->line_start_mark = gtk_text_buffer_create_mark(buffer,
135                                                         "line_start_mark",
136                                                         &iter, TRUE);
137
138     /* Register some tags */
139     gtk_text_buffer_create_tag(buffer, "stdout", NULL);
140     gtk_text_buffer_create_tag(buffer, "stderr",
141                                "foreground", "red",
142                                "paragraph-background", "#FFFFE0",
143                                NULL);
144     gtk_text_buffer_create_tag(buffer, "prompt",
145                                "foreground", "blue",
146                                NULL);
147
148     parasite_python_shell_write_prompt(GTK_WIDGET(python_shell));
149 }
150
151 static void
152 parasite_python_shell_finalize(GObject *python_shell)
153 {
154     ParasitePythonShellPrivate *priv =
155         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
156
157     g_queue_free(priv->history);
158 }
159
160 static void
161 parasite_python_shell_log_stdout(const char *text, gpointer python_shell)
162 {
163     parasite_python_shell_append_text(PARASITE_PYTHON_SHELL(python_shell),
164                                       text, "stdout");
165 }
166
167 static void
168 parasite_python_shell_log_stderr(const char *text, gpointer python_shell)
169 {
170     parasite_python_shell_append_text(PARASITE_PYTHON_SHELL(python_shell),
171                                       text, "stderr");
172 }
173
174 static void
175 parasite_python_shell_write_prompt(GtkWidget *python_shell)
176 {
177     ParasitePythonShellPrivate *priv =
178         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
179     GtkTextBuffer *buffer =
180         gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview));
181     GtkTextIter iter;
182     const char *prompt = (priv->pending_command == NULL ? ">>> " : "... ");
183
184     parasite_python_shell_append_text(PARASITE_PYTHON_SHELL(python_shell),
185                                       prompt, "prompt");
186
187     gtk_text_buffer_get_end_iter(buffer, &iter);
188     gtk_text_buffer_move_mark(buffer, priv->line_start_mark, &iter);
189 }
190
191 static void
192 parasite_python_shell_process_line(GtkWidget *python_shell)
193 {
194     ParasitePythonShellPrivate *priv =
195         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
196
197     char *command = parasite_python_shell_get_input(python_shell);
198     char last_char;
199
200     parasite_python_shell_append_text(PARASITE_PYTHON_SHELL(python_shell),
201                                       "\n", NULL);
202
203     if (*command != '\0')
204     {
205         /* Save this command in the history. */
206         g_queue_push_head(priv->history, command);
207         priv->cur_history_item = NULL;
208
209         if (g_queue_get_length(priv->history) > MAX_HISTORY_LENGTH)
210             g_free(g_queue_pop_tail(priv->history));
211     }
212
213     last_char = command[MAX(0, strlen(command) - 1)];
214
215     if (last_char == ':' || last_char == '\\' ||
216         (priv->in_block && g_ascii_isspace(command[0])))
217     {
218         printf("in block.. %c, %d, %d\n",
219                last_char, priv->in_block,
220                g_ascii_isspace(command[0]));
221         /* This is a multi-line expression */
222         if (priv->pending_command == NULL)
223             priv->pending_command = g_string_new(command);
224         else
225             g_string_append(priv->pending_command, command);
226
227         g_string_append_c(priv->pending_command, '\n');
228
229         if (last_char == ':')
230             priv->in_block = TRUE;
231     }
232     else
233     {
234         if (priv->pending_command != NULL)
235         {
236             g_string_append(priv->pending_command, command);
237             g_string_append_c(priv->pending_command, '\n');
238
239             /* We're not actually leaking this. It's in the history. */
240             command = g_string_free(priv->pending_command, FALSE);
241         }
242         parasite_python_run(command,
243                             parasite_python_shell_log_stdout,
244                             parasite_python_shell_log_stderr,
245                             python_shell);
246         if (priv->pending_command != NULL)
247         {
248             /* Now do the cleanup. */
249             g_free(command);
250             priv->pending_command = NULL;
251             priv->in_block = FALSE;
252         }
253     }
254     parasite_python_shell_write_prompt(python_shell);
255 }
256
257 static void
258 parasite_python_shell_replace_input(GtkWidget *python_shell,
259                                     const char *text)
260 {
261     ParasitePythonShellPrivate *priv =
262         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
263
264     GtkTextBuffer *buffer =
265         gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview));
266     GtkTextIter start_iter;
267     GtkTextIter end_iter;
268
269     gtk_text_buffer_get_iter_at_mark(buffer, &start_iter,
270                                      priv->line_start_mark);
271     gtk_text_buffer_get_end_iter(buffer, &end_iter);
272
273     gtk_text_buffer_delete(buffer, &start_iter, &end_iter);
274     gtk_text_buffer_insert(buffer, &end_iter, text, -1);
275 }
276
277 static char *
278 parasite_python_shell_get_input(GtkWidget *python_shell)
279 {
280     ParasitePythonShellPrivate *priv =
281         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
282     GtkTextBuffer *buffer =
283         gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview));
284     GtkTextIter start_iter;
285     GtkTextIter end_iter;
286
287     gtk_text_buffer_get_iter_at_mark(buffer, &start_iter,
288                                      priv->line_start_mark);
289     gtk_text_buffer_get_end_iter(buffer, &end_iter);
290
291     return gtk_text_buffer_get_text(buffer, &start_iter, &end_iter, FALSE);
292 }
293
294 static const char *
295 parasite_python_shell_get_history_back(GtkWidget *python_shell)
296 {
297     ParasitePythonShellPrivate *priv =
298         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
299
300     if (priv->cur_history_item == NULL)
301     {
302         priv->cur_history_item = g_queue_peek_head_link(priv->history);
303
304         if (priv->cur_history_item == NULL)
305             return "";
306     }
307     else if (priv->cur_history_item->next != NULL)
308         priv->cur_history_item = priv->cur_history_item->next;
309
310     return (const char *)priv->cur_history_item->data;
311 }
312
313 static const char *
314 parasite_python_shell_get_history_forward(GtkWidget *python_shell)
315 {
316     ParasitePythonShellPrivate *priv =
317         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
318
319     if (priv->cur_history_item == NULL || priv->cur_history_item->prev == NULL)
320     {
321         priv->cur_history_item = NULL;
322         return "";
323     }
324
325     priv->cur_history_item = priv->cur_history_item->prev;
326
327     return (const char *)priv->cur_history_item->data;
328 }
329
330 static gboolean
331 parasite_python_shell_key_press_cb(GtkWidget *textview,
332                                    GdkEventKey *event,
333                                    GtkWidget *python_shell)
334 {
335     if (event->keyval == GDK_Return)
336     {
337         parasite_python_shell_process_line(python_shell);
338         return TRUE;
339     }
340     else if (event->keyval == GDK_Up)
341     {
342         parasite_python_shell_replace_input(python_shell,
343             parasite_python_shell_get_history_back(python_shell));
344         return TRUE;
345     }
346     else if (event->keyval == GDK_Down)
347     {
348         parasite_python_shell_replace_input(python_shell,
349             parasite_python_shell_get_history_forward(python_shell));
350         return TRUE;
351     }
352     else if (event->string != NULL)
353     {
354         ParasitePythonShellPrivate *priv =
355             PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
356         GtkTextBuffer *buffer =
357             gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview));
358         GtkTextMark *insert_mark = gtk_text_buffer_get_insert(buffer);
359         GtkTextMark *selection_mark =
360             gtk_text_buffer_get_selection_bound(buffer);
361         GtkTextIter insert_iter;
362         GtkTextIter selection_iter;
363         GtkTextIter start_iter;
364         gint cmp_start_insert;
365         gint cmp_start_select;
366         gint cmp_insert_select;
367
368         gtk_text_buffer_get_iter_at_mark(buffer, &start_iter,
369                                          priv->line_start_mark);
370         gtk_text_buffer_get_iter_at_mark(buffer, &insert_iter, insert_mark);
371         gtk_text_buffer_get_iter_at_mark(buffer, &selection_iter,
372                                          selection_mark);
373
374         cmp_start_insert = gtk_text_iter_compare(&start_iter, &insert_iter);
375         cmp_start_select = gtk_text_iter_compare(&start_iter, &selection_iter);
376         cmp_insert_select = gtk_text_iter_compare(&insert_iter,
377                                                   &selection_iter);
378
379         if (cmp_start_insert == 0 && cmp_start_select == 0 &&
380             (event->keyval == GDK_BackSpace ||
381              event->keyval == GDK_Left))
382         {
383             return TRUE;
384         }
385         if (cmp_start_insert <= 0 && cmp_start_select <= 0)
386         {
387             return FALSE;
388         }
389         else if (cmp_start_insert > 0 && cmp_start_select > 0)
390         {
391             gtk_text_buffer_place_cursor(buffer, &start_iter);
392         }
393         else if (cmp_insert_select < 0)
394         {
395             gtk_text_buffer_move_mark(buffer, insert_mark, &start_iter);
396         }
397         else if (cmp_insert_select > 0)
398         {
399             gtk_text_buffer_move_mark(buffer, selection_mark, &start_iter);
400         }
401     }
402
403     return FALSE;
404 }
405
406 GtkWidget *
407 parasite_python_shell_new(void)
408 {
409     return g_object_new(PARASITE_TYPE_PYTHON_SHELL, NULL);
410 }
411
412 void
413 parasite_python_shell_append_text(ParasitePythonShell *python_shell,
414                                   const char *str,
415                                   const char *tag)
416 {
417     ParasitePythonShellPrivate *priv =
418         PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell);
419
420     GtkTextIter end;
421     GtkTextBuffer *buffer =
422         gtk_text_view_get_buffer(GTK_TEXT_VIEW(priv->textview));
423     GtkTextMark *mark = gtk_text_buffer_get_insert(buffer);
424
425     gtk_text_buffer_get_end_iter(buffer, &end);
426     gtk_text_buffer_move_mark(buffer, mark, &end);
427     gtk_text_buffer_insert_with_tags_by_name(buffer, &end, str, -1, tag, NULL);
428     gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(priv->textview), mark,
429                                  0, TRUE, 0, 1);
430 }
431
432 void
433 parasite_python_shell_focus(ParasitePythonShell *python_shell)
434 {
435    gtk_widget_grab_focus(PARASITE_PYTHON_SHELL_GET_PRIVATE(python_shell)->textview);
436 }
437
438 // vim: set et ts=4: