~ubuntu-branches/ubuntu/trusty/reinteract/trusty

« back to all changes in this revision

Viewing changes to lib/reinteract/shell_view.py

  • Committer: Bazaar Package Importer
  • Author(s): Chris Lamb
  • Date: 2009-03-28 00:53:14 UTC
  • Revision ID: james.westby@ubuntu.com-20090328005314-ramzoo0q6r8rmwuc
Tags: upstream-0.5.0
ImportĀ upstreamĀ versionĀ 0.5.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2007-2009 Owen Taylor
 
2
#
 
3
# This file is part of Reinteract and distributed under the terms
 
4
# of the BSD license. See the file COPYING in the Reinteract
 
5
# distribution for full details.
 
6
#
 
7
########################################################################
 
8
 
 
9
import gobject
 
10
import gtk
 
11
import re
 
12
from shell_buffer import ShellBuffer, ADJUST_NONE, ADJUST_BEFORE, ADJUST_AFTER
 
13
from chunks import StatementChunk, CommentChunk, BlankChunk
 
14
from completion_popup import CompletionPopup
 
15
from doc_popup import DocPopup
 
16
from global_settings import global_settings
 
17
from notebook import NotebookFile
 
18
import sanitize_textview_ipc
 
19
 
 
20
LEFT_MARGIN_WIDTH = 10
 
21
 
 
22
ALL_WHITESPACE_RE = re.compile("^\s*$")
 
23
 
 
24
class ShellView(gtk.TextView):
 
25
    __gsignals__ = {
 
26
        'backspace' : 'override',
 
27
        'expose-event': 'override',
 
28
        'focus-out-event': 'override',
 
29
        'button-press-event': 'override',
 
30
        'button-release-event': 'override',
 
31
        'motion-notify-event': 'override',
 
32
        'key-press-event': 'override',
 
33
        'leave-notify-event': 'override',
 
34
        'motion-notify-event': 'override',
 
35
        'realize': 'override',
 
36
        'unrealize': 'override',
 
37
        'size-allocate': 'override'
 
38
   }
 
39
        
 
40
    def __init__(self, buf):
 
41
        self.edit_only = buf.worksheet.edit_only
 
42
 
 
43
        if not self.edit_only:
 
44
            buf.worksheet.connect('chunk-inserted', self.on_chunk_inserted)
 
45
            buf.worksheet.connect('chunk-changed', self.on_chunk_changed)
 
46
            buf.worksheet.connect('chunk-status-changed', self.on_chunk_status_changed)
 
47
            buf.worksheet.connect('notify::state', self.on_notify_state)
 
48
 
 
49
            # Track changes to update completion
 
50
            buf.connect_after('insert-text', self.on_after_insert_text)
 
51
            buf.connect_after('delete-range', self.on_after_delete_range)
 
52
            buf.connect_after('end-user-action', self.on_after_end_user_action)
 
53
 
 
54
            self.__inserted_in_user_action = False
 
55
            self.__deleted_in_user_action = False
 
56
 
 
57
        buf.connect('add-custom-result', self.on_add_custom_result)
 
58
        buf.connect('pair-location-changed', self.on_pair_location_changed)
 
59
            
 
60
        gtk.TextView.__init__(self, buf)
 
61
        if not self.edit_only:
 
62
            self.set_border_window_size(gtk.TEXT_WINDOW_LEFT, LEFT_MARGIN_WIDTH)
 
63
        self.set_left_margin(2)
 
64
 
 
65
        # Attach a "behavior object" to the view which, by ugly hacks, makes it
 
66
        # do simply and reasonable things for cut-and-paste and DND
 
67
        sanitize_textview_ipc.sanitize_view(self)
 
68
 
 
69
        self.add_events(gtk.gdk.LEAVE_NOTIFY_MASK)
 
70
 
 
71
        self.__completion_popup = CompletionPopup(self)
 
72
        self.__doc_popup = DocPopup()
 
73
        self.__mouse_over_object = None
 
74
        self.__mouse_over_timeout = None
 
75
 
 
76
        self.__mouse_over_start = buf.create_mark(None, buf.get_start_iter(), True)
 
77
 
 
78
        self.__arg_highlight_start = None
 
79
        self.__arg_highlight_end = None
 
80
        buf.connect('mark-set', self.on_mark_set)
 
81
 
 
82
    def __get_worksheet_line_yrange(self, line):
 
83
        buffer_line = self.get_buffer().pos_to_iter(line)
 
84
        return self.get_line_yrange(buffer_line)
 
85
 
 
86
    def __get_worksheet_line_at_y(self, y, adjust):
 
87
        buf = self.get_buffer()
 
88
        (buffer_line, _) = self.get_line_at_y(y)
 
89
        return buf.iter_to_pos(buffer_line, adjust)[0]
 
90
 
 
91
    def paint_chunk(self, cr, area, chunk, fill_color, outline_color):
 
92
        buf = self.get_buffer()
 
93
        
 
94
        (y, _) = self.__get_worksheet_line_yrange(chunk.start)
 
95
        (end_y, end_height) = self.__get_worksheet_line_yrange(chunk.end - 1)
 
96
        height = end_y + end_height - y
 
97
        
 
98
        (_, window_y) = self.buffer_to_window_coords(gtk.TEXT_WINDOW_LEFT, 0, y)
 
99
        cr.rectangle(area.x, window_y, area.width, height)
 
100
        cr.set_source_rgb(*fill_color)
 
101
        cr.fill()
 
102
                
 
103
        cr.rectangle(0.5, window_y + 0.5, LEFT_MARGIN_WIDTH - 1, height - 1)
 
104
        cr.set_source_rgb(*outline_color)
 
105
        cr.set_line_width(1)
 
106
        cr.stroke()
 
107
 
 
108
    def do_realize(self):
 
109
        gtk.TextView.do_realize(self)
 
110
 
 
111
        if not self.edit_only:
 
112
            self.get_window(gtk.TEXT_WINDOW_LEFT).set_background(self.style.white)
 
113
 
 
114
        # While the the worksheet is executing, we want to display a watch cursor
 
115
        # Trying to override the cursor setting of GtkTextView is really hard because
 
116
        # of things like hiding the cursor when typing, so we take the simple approach
 
117
        # of using an input-only "cover window" that we set the cursor on and
 
118
        # show on top of the GtkTextView's normal window.
 
119
 
 
120
        self.__watch_window = gtk.gdk.Window(self.window,
 
121
                                             self.allocation.width, self.allocation.height,
 
122
                                             gtk.gdk.WINDOW_CHILD,
 
123
                                             (gtk.gdk.SCROLL_MASK |
 
124
                                              gtk.gdk.BUTTON_PRESS_MASK |
 
125
                                              gtk.gdk.BUTTON_RELEASE_MASK |
 
126
                                              gtk.gdk.POINTER_MOTION_MASK |
 
127
                                              gtk.gdk.POINTER_MOTION_HINT_MASK),
 
128
                                             gtk.gdk.INPUT_ONLY,
 
129
                                             x=0, y=0)
 
130
        self.__watch_window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
 
131
        self.__watch_window.set_user_data(self)
 
132
 
 
133
        if self.get_buffer().worksheet.state == NotebookFile.EXECUTING:
 
134
            self.__watch_window.show()
 
135
            self.__watch_window.raise_()
 
136
 
 
137
    def do_unrealize(self):
 
138
        self.__watch_window.set_user_data(None)
 
139
        self.__watch_window.destroy()
 
140
        self.__watch_window = None
 
141
        gtk.TextView.do_unrealize(self)
 
142
 
 
143
    def do_size_allocate(self, allocation):
 
144
        gtk.TextView.do_size_allocate(self, allocation)
 
145
        if (self.flags() & gtk.REALIZED) != 0:
 
146
            self.__watch_window.resize(allocation.width, allocation.height)
 
147
 
 
148
    def __expose_window_left(self, event):
 
149
        (_, start_y) = self.window_to_buffer_coords(gtk.TEXT_WINDOW_LEFT, 0, event.area.y)
 
150
        start_line = self.__get_worksheet_line_at_y(start_y, adjust=ADJUST_AFTER)
 
151
        
 
152
        (_, end_y) = self.window_to_buffer_coords(gtk.TEXT_WINDOW_LEFT, 0, event.area.y + event.area.height - 1)
 
153
        end_line = self.__get_worksheet_line_at_y(end_y, adjust=ADJUST_BEFORE)
 
154
 
 
155
        buf = self.get_buffer()
 
156
 
 
157
        cr = event.window.cairo_create()
 
158
 
 
159
        for chunk in buf.worksheet.iterate_chunks(start_line, end_line + 1):
 
160
            if isinstance(chunk, StatementChunk):
 
161
                if chunk.executing:
 
162
                    self.paint_chunk(cr, event.area, chunk, (0, 1, 0), (0, 0.5, 0))
 
163
                elif chunk.error_message != None:
 
164
                    self.paint_chunk(cr, event.area, chunk, (1, 0, 0), (0.5, 0, 0))
 
165
                elif chunk.needs_compile:
 
166
                    self.paint_chunk(cr, event.area, chunk, (1, 1, 0), (0.5, 0.5, 0))
 
167
                elif chunk.needs_execute:
 
168
                    self.paint_chunk(cr, event.area, chunk, (1, 0, 1), (0.5, 0.5, 0))
 
169
                else:
 
170
                    self.paint_chunk(cr, event.area, chunk, (0, 0, 1), (0, 0, 0.5))
 
171
 
 
172
    def __draw_rect_outline(self, event, rect):
 
173
        if (rect.y + rect.height <= event.area.y or rect.y >= event.area.y + event.area.height):
 
174
            return
 
175
 
 
176
        cr = event.window.cairo_create()
 
177
        cr.set_line_width(1.)
 
178
        cr.rectangle(rect.x + 0.5, rect.y + 0.5, rect.width - 1, rect.height - 1)
 
179
        cr.set_source_rgb(0.6, 0.6, 0.6)
 
180
        cr.stroke()
 
181
 
 
182
    def __expose_arg_highlight(self, event):
 
183
        buf = self.get_buffer()
 
184
 
 
185
        # We want a rectangle enclosing the range between arg_highlight_start and
 
186
        # arg_highlight_end; the method here isn't correct in the presence of
 
187
        # RTL text, but the necessary Pango functionality isn't exposed to
 
188
        # a GtkTextView user. RTL code is rare. We also don't handle multiple-line
 
189
        # highlight regions right. (Return ends the highlight, so you'd need to paste)
 
190
        rect = self.get_iter_location(buf.get_iter_at_mark (self.__arg_highlight_start))
 
191
        rect.x, rect.y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
 
192
                                                      rect.x, rect.y)
 
193
        rect.width = 0
 
194
        end_rect = self.get_iter_location(buf.get_iter_at_mark (self.__arg_highlight_end))
 
195
        end_rect.x, end_rect.y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
 
196
                                                              end_rect.x, end_rect.y)
 
197
        end_rect.width = 0
 
198
 
 
199
        rect = rect.union(end_rect)
 
200
 
 
201
        self.__draw_rect_outline(event, rect)
 
202
 
 
203
    def __expose_pair_location(self, event):
 
204
        pair_location = self.get_buffer().get_pair_location()
 
205
        if pair_location == None:
 
206
            return
 
207
        
 
208
        rect = self.get_iter_location(pair_location)
 
209
 
 
210
        rect.x, rect.y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, rect.x, rect.y)
 
211
 
 
212
        self.__draw_rect_outline(event, rect)
 
213
 
 
214
    def do_expose_event(self, event):
 
215
        if not self.edit_only and event.window == self.get_window(gtk.TEXT_WINDOW_LEFT):
 
216
            self.__expose_window_left(event)
 
217
            return False
 
218
        
 
219
        gtk.TextView.do_expose_event(self, event)
 
220
 
 
221
        if event.window == self.get_window(gtk.TEXT_WINDOW_TEXT):
 
222
            if self.__arg_highlight_start:
 
223
                self.__expose_arg_highlight(event)
 
224
            else:
 
225
                self.__expose_pair_location(event)
 
226
        
 
227
        return False
 
228
 
 
229
    def __get_line_text(self, iter):
 
230
        start = iter.copy()
 
231
        start.set_line_index(0)
 
232
        end = iter.copy()
 
233
        end.forward_to_line_end()
 
234
        
 
235
        return start.get_slice(end)
 
236
    
 
237
    # This is likely overengineered, since we're going to try as hard as possible not to
 
238
    # have tabs in our worksheets. We don't do the funky handling of \f.
 
239
    def __count_indent(self, text):
 
240
        indent = 0
 
241
        for c in text:
 
242
            if c == ' ':
 
243
                indent += 1
 
244
            elif c == '\t':
 
245
                indent += 8 - (indent % 8)
 
246
            else:
 
247
                break
 
248
 
 
249
        return indent
 
250
 
 
251
    def __find_outdent(self, iter):
 
252
        buf = self.get_buffer()
 
253
        line, _ = buf.iter_to_pos(iter)
 
254
 
 
255
        current_indent = self.__count_indent(buf.worksheet.get_line(line))
 
256
 
 
257
        while line > 0:
 
258
            line -= 1
 
259
            line_text = buf.worksheet.get_line(line)
 
260
            # Empty lines don't establish indentation
 
261
            if ALL_WHITESPACE_RE.match(line_text):
 
262
                continue
 
263
 
 
264
            indent = self.__count_indent(line_text)
 
265
            if indent < current_indent:
 
266
                return re.match(r"^[\t ]*", line_text).group(0)
 
267
 
 
268
        return ""
 
269
 
 
270
    def __find_default_indent(self, iter):
 
271
        buf = self.get_buffer()
 
272
        line, offset = buf.iter_to_pos(iter)
 
273
 
 
274
        while line > 0:
 
275
            line -= 1
 
276
            chunk = buf.worksheet.get_chunk(line)
 
277
            if isinstance(chunk, StatementChunk):
 
278
                return chunk.tokenized.get_next_line_indent(line - chunk.start)
 
279
            elif isinstance(chunk, CommentChunk):
 
280
                return " " * self.__count_indent(buf.worksheet.get_line(line))
 
281
 
 
282
        return ""
 
283
 
 
284
    def __reindent_line(self, iter, indent_text):
 
285
        buf = self.get_buffer()
 
286
 
 
287
        line_text = self.__get_line_text(iter)
 
288
        prefix = re.match(r"^[\t ]*", line_text).group(0)
 
289
 
 
290
        diff = self.__count_indent(indent_text) - self.__count_indent(prefix)
 
291
        if diff == 0:
 
292
            return 0
 
293
 
 
294
        common_len = 0
 
295
        for a, b in zip(prefix, indent_text):
 
296
            if a != b:
 
297
                break
 
298
            common_len += 1
 
299
    
 
300
        start = iter.copy()
 
301
        start.set_line_offset(common_len)
 
302
        end = iter.copy()
 
303
        end.set_line_offset(len(prefix))
 
304
 
 
305
        # Nitpicky-detail. If the selection starts at the start of the line, and we are
 
306
        # inserting white-space there, then the whitespace should be *inside* the selection
 
307
        mark_to_start = None
 
308
        if common_len == 0 and buf.get_has_selection():
 
309
            mark = buf.get_insert()
 
310
            if buf.get_iter_at_mark(mark).compare(start) == 0:
 
311
                mark_to_start = mark
 
312
                
 
313
            mark = buf.get_selection_bound()
 
314
            if buf.get_iter_at_mark(mark).compare(start) == 0:
 
315
                mark_to_start = mark
 
316
        
 
317
        buf.delete(start, end)
 
318
        buf.insert(end, indent_text[common_len:])
 
319
 
 
320
        if mark_to_start != None:
 
321
            end.set_line_offset(0)
 
322
            buf.move_mark(mark_to_start, end)
 
323
 
 
324
        return diff
 
325
 
 
326
    def __reindent_selection(self, outdent):
 
327
        buf = self.get_buffer()
 
328
 
 
329
        bounds = buf.get_selection_bounds()
 
330
        if bounds == ():
 
331
            insert_mark = buf.get_insert()
 
332
            bounds = buf.get_iter_at_mark(insert_mark), buf.get_iter_at_mark(insert_mark)
 
333
        start, end = bounds
 
334
 
 
335
        line, _ = buf.iter_to_pos(start, adjust=ADJUST_AFTER)
 
336
        end_line, end_offset = buf.iter_to_pos(end, adjust=ADJUST_BEFORE)
 
337
        if end_offset == 0 and end_line > line:
 
338
            end_line -= 1
 
339
 
 
340
        iter = buf.pos_to_iter(line)
 
341
 
 
342
        if outdent:
 
343
            indent_text = self.__find_outdent(iter)
 
344
        else:
 
345
            indent_text = self.__find_default_indent(iter)
 
346
 
 
347
        diff = self.__reindent_line(iter, indent_text)
 
348
        while True:
 
349
            line += 1
 
350
            if line > end_line:
 
351
                return
 
352
 
 
353
            iter = buf.pos_to_iter(line)
 
354
            current_indent = self.__count_indent(self.__get_line_text(iter))
 
355
            self.__reindent_line(iter, max(0, " " * (current_indent + diff)))
 
356
 
 
357
    def __hide_completion(self):
 
358
        if self.__completion_popup.showing:
 
359
            self.__completion_popup.popdown()
 
360
            
 
361
    def do_focus_out_event(self, event):
 
362
        self.__hide_completion()
 
363
        self.__doc_popup.popdown()
 
364
        return gtk.TextView.do_focus_out_event(self, event)
 
365
 
 
366
    def __rewrite_window(self, event):
 
367
        # Mouse events on the "watch window" that covers the text view
 
368
        # during calculation need to be forwarded to the real text window
 
369
        # since it looks bad if keynav works, but you can't click on the
 
370
        # window to set the cursor, select text, and so forth
 
371
 
 
372
        if event.window == self.__watch_window:
 
373
            event.window = self.get_window(gtk.TEXT_WINDOW_TEXT)
 
374
 
 
375
        # Events on the left-margin window also get written so the user can
 
376
        # click there when starting a drag selection. We need to adjust the
 
377
        # X coordinate in that case
 
378
        if not self.edit_only and event.window == self.get_window(gtk.TEXT_WINDOW_LEFT):
 
379
            event.window = self.get_window(gtk.TEXT_WINDOW_TEXT)
 
380
            if event.type == gtk.gdk._3BUTTON_PRESS:
 
381
                # Workaround for http://bugzilla.gnome.org/show_bug.cgi?id=573664
 
382
                event.x = 50.
 
383
            else:
 
384
                event.x -= LEFT_MARGIN_WIDTH
 
385
 
 
386
    def do_button_press_event(self, event):
 
387
        self.__rewrite_window(event)
 
388
 
 
389
        self.__doc_popup.popdown()
 
390
 
 
391
        return gtk.TextView.do_button_press_event(self, event)
 
392
 
 
393
    def do_button_release_event(self, event):
 
394
        self.__rewrite_window(event)
 
395
 
 
396
        return gtk.TextView.do_button_release_event(self, event)
 
397
 
 
398
    def do_motion_notify_event(self, event):
 
399
        self.__rewrite_window(event)
 
400
 
 
401
        return gtk.TextView.do_motion_notify_event(self, event)
 
402
 
 
403
    def __remove_arg_highlight(self, cursor_to_end=True):
 
404
        buf = self.get_buffer()
 
405
 
 
406
        end = buf.get_iter_at_mark (self.__arg_highlight_end)
 
407
 
 
408
        buf.delete_mark(self.__arg_highlight_start)
 
409
        self.__arg_highlight_start = None
 
410
        buf.delete_mark(self.__arg_highlight_end)
 
411
        self.__arg_highlight_end = None
 
412
 
 
413
        if cursor_to_end:
 
414
            # If the arg_highlight ends at closing punctuation, skip over it
 
415
            tmp = end.copy()
 
416
            tmp.forward_char()
 
417
            text = buf.get_slice(end, tmp)
 
418
 
 
419
            if text in (")", "]", "}"):
 
420
                buf.place_cursor(tmp)
 
421
            else:
 
422
                buf.place_cursor(end)
 
423
 
 
424
    def do_key_press_event(self, event):
 
425
        buf = self.get_buffer()
 
426
 
 
427
        if self.__completion_popup.focused and self.__completion_popup.on_key_press_event(event):
 
428
            return True
 
429
        
 
430
        if self.__doc_popup.focused:
 
431
            self.__doc_popup.on_key_press_event(event)
 
432
            return True
 
433
 
 
434
        if not self.edit_only and event.keyval in (gtk.keysyms.F2, gtk.keysyms.KP_F2):
 
435
            self.__hide_completion()
 
436
 
 
437
            if self.__doc_popup.showing:
 
438
                self.__doc_popup.focus()
 
439
            else:
 
440
                self.show_doc_popup(focus_popup=True)
 
441
 
 
442
            return True
 
443
 
 
444
        if not self.__arg_highlight_start:
 
445
            self.__doc_popup.popdown()
 
446
        
 
447
        if event.keyval in (gtk.keysyms.KP_Enter, gtk.keysyms.Return):
 
448
            self.__hide_completion()
 
449
 
 
450
            if self.__arg_highlight_start:
 
451
                self.__remove_arg_highlight()
 
452
                self.__doc_popup.popdown()
 
453
                return True
 
454
            
 
455
            increase_indent = False
 
456
            insert = buf.get_iter_at_mark(buf.get_insert())
 
457
            line, pos = buf.iter_to_pos(insert, adjust=ADJUST_NONE)
 
458
 
 
459
            # Inserting return inside a ResultChunk would normally do nothing
 
460
            # but we want to make it insert a line after the chunk
 
461
            if line == None and not buf.get_has_selection():
 
462
                line, pos = buf.iter_to_pos(insert, adjust=ADJUST_BEFORE)
 
463
                iter = buf.pos_to_iter(line, -1)
 
464
                buf.place_cursor(iter)
 
465
                buf.insert_interactive(iter, "\n", True)
 
466
                
 
467
                return True
 
468
 
 
469
            buf.begin_user_action()
 
470
            
 
471
            gtk.TextView.do_key_press_event(self, event)
 
472
            # We need the chunks to be updated when computing the line indents
 
473
            buf.worksheet.rescan()
 
474
 
 
475
            insert = buf.get_iter_at_mark(buf.get_insert())
 
476
            
 
477
            self.__reindent_line(insert, self.__find_default_indent(insert))
 
478
 
 
479
            # If we have two comment lines in a row, assume a block comment
 
480
            if (line > 0 and
 
481
                isinstance(buf.worksheet.get_chunk(line), CommentChunk) and
 
482
                isinstance(buf.worksheet.get_chunk(line - 1), CommentChunk)):
 
483
                self.get_buffer().insert_interactive_at_cursor("# ", True)
 
484
 
 
485
            buf.end_user_action()
 
486
                
 
487
            return True
 
488
        elif event.keyval in (gtk.keysyms.Tab, gtk.keysyms.KP_Tab) and event.state & gtk.gdk.CONTROL_MASK == 0:
 
489
            buf.begin_user_action()
 
490
            self.__reindent_selection(outdent=False)
 
491
            buf.end_user_action()
 
492
 
 
493
            return True
 
494
        elif event.keyval == gtk.keysyms.ISO_Left_Tab and event.state & gtk.gdk.CONTROL_MASK == 0:
 
495
            buf.begin_user_action()
 
496
            self.__reindent_selection(outdent=True)
 
497
            buf.end_user_action()
 
498
 
 
499
            return True
 
500
        elif event.keyval == gtk.keysyms.space and event.state & gtk.gdk.CONTROL_MASK != 0:
 
501
            if self.__completion_popup.showing:
 
502
                if self.__completion_popup.spontaneous:
 
503
                    self.__completion_popup.popup(spontaneous=False)
 
504
                else:
 
505
                    self.__completion_popup.popdown()
 
506
            else:
 
507
                if self.__doc_popup.showing:
 
508
                    self.__doc_popup.popdown()
 
509
                self.__completion_popup.popup(spontaneous=False)
 
510
            return True
 
511
        elif event.keyval in (gtk.keysyms.z, gtk.keysyms.Z) and \
 
512
                (event.state & gtk.gdk.CONTROL_MASK) != 0 and \
 
513
                (event.state & gtk.gdk.SHIFT_MASK) == 0:
 
514
            buf.worksheet.undo()
 
515
            
 
516
            return True
 
517
        # This is the gedit/gtksourceview binding to cause your hands to fall off
 
518
        elif event.keyval in (gtk.keysyms.z, gtk.keysyms.Z) and \
 
519
                (event.state & gtk.gdk.CONTROL_MASK) != 0 and \
 
520
                (event.state & gtk.gdk.SHIFT_MASK) != 0:
 
521
            buf.worksheet.redo()
 
522
            
 
523
            return True
 
524
        # This is the familiar binding (Eclipse, etc). Firefox supports both
 
525
        elif event.keyval in (gtk.keysyms.y, gtk.keysyms.Y) and event.state & gtk.gdk.CONTROL_MASK != 0:
 
526
            buf.worksheet.redo()
 
527
 
 
528
            return True
 
529
        
 
530
        return gtk.TextView.do_key_press_event(self, event)
 
531
 
 
532
    def __show_mouse_over(self):
 
533
        self.__mouse_over_timeout = None
 
534
        
 
535
        if self.__completion_popup.showing:
 
536
            return
 
537
        
 
538
        self.__doc_popup.set_target(self.__mouse_over_object)
 
539
        location = self.get_buffer().get_iter_at_mark(self.__mouse_over_start)
 
540
        self.__doc_popup.position_at_location(self, location)
 
541
        self.__doc_popup.popup()
 
542
 
 
543
        return False
 
544
        
 
545
    def __stop_mouse_over(self):
 
546
        if self.__mouse_over_timeout:
 
547
            gobject.source_remove(self.__mouse_over_timeout)
 
548
            self.__mouse_over_timeout = None
 
549
            
 
550
        self.__mouse_over_object = None
 
551
        
 
552
    def do_motion_notify_event(self, event):
 
553
        # Successful mousing-over depends on knowing the types of symbols so doing the
 
554
        # checks are pointless in edit-only mode
 
555
        if not self.edit_only and event.window == self.get_window(gtk.TEXT_WINDOW_TEXT) and not self.__doc_popup.focused:
 
556
            buf = self.get_buffer()
 
557
            
 
558
            x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, int(event.x), int(event.y))
 
559
            iter, _ = self.get_iter_at_position(x, y)
 
560
            line, offset = buf.iter_to_pos(iter, adjust=ADJUST_NONE)
 
561
            if line != None:
 
562
                obj, start_line, start_offset, _,_ = buf.worksheet.get_object_at_location(line, offset)
 
563
            else:
 
564
                obj = None
 
565
 
 
566
            if not obj is self.__mouse_over_object:
 
567
                self.__stop_mouse_over()
 
568
                self.__doc_popup.popdown()
 
569
                if obj != None:
 
570
                    start = buf.pos_to_iter(start_line, start_offset)
 
571
                    buf.move_mark(self.__mouse_over_start, start)
 
572
 
 
573
                    self.__mouse_over_object = obj
 
574
                    try:
 
575
                        timeout = self.get_settings().get_property('gtk-tooltip-timeout')
 
576
                    except TypeError: # GTK+ < 2.12
 
577
                        timeout = 500
 
578
                    self.__mouse_over_timeout = gobject.timeout_add(timeout, self.__show_mouse_over)
 
579
                
 
580
        return gtk.TextView.do_motion_notify_event(self, event)
 
581
 
 
582
    def do_leave_notify_event(self, event):
 
583
        self.__stop_mouse_over()
 
584
        if not self.__doc_popup.focused:
 
585
            self.__doc_popup.popdown()
 
586
        return False
 
587
 
 
588
    def do_backspace(self):
 
589
        buf = self.get_buffer()
 
590
        
 
591
        insert = buf.get_iter_at_mark(buf.get_insert())
 
592
        line, offset = buf.iter_to_pos(insert)
 
593
        
 
594
        current_chunk = buf.worksheet.get_chunk(line)
 
595
        if isinstance(current_chunk, StatementChunk) or isinstance(current_chunk, BlankChunk):
 
596
            line_text = buf.worksheet.get_line(line)[0:offset]
 
597
 
 
598
            if re.match(r"^[\t ]+$", line_text):
 
599
                self.__reindent_line(insert, self.__find_outdent(insert))
 
600
                return
 
601
                       
 
602
        return gtk.TextView.do_backspace(self)
 
603
 
 
604
    def __invalidate_status(self, chunk):
 
605
        buf = self.get_buffer()
 
606
        
 
607
        (start_y, start_height) = self.__get_worksheet_line_yrange(chunk.start)
 
608
        (end_y, end_height) = self.__get_worksheet_line_yrange(chunk.end - 1)
 
609
 
 
610
        (_, window_y) = self.buffer_to_window_coords(gtk.TEXT_WINDOW_LEFT, 0, start_y)
 
611
 
 
612
        if self.window:
 
613
            left_margin_window = self.get_window(gtk.TEXT_WINDOW_LEFT)
 
614
            left_margin_window.invalidate_rect((0, window_y, LEFT_MARGIN_WIDTH, end_y + end_height - start_y),
 
615
                                               False)
 
616
 
 
617
    def on_chunk_inserted(self, worksheet, chunk):
 
618
        self.__invalidate_status(chunk)
 
619
 
 
620
    def on_chunk_changed(self, worksheet, chunk, changed_lines):
 
621
        self.__invalidate_status(chunk)
 
622
 
 
623
    def on_chunk_status_changed(self, worksheet, chunk):
 
624
        self.__invalidate_status(chunk)
 
625
 
 
626
    def on_notify_state(self, worksheet, param_spec):
 
627
        if (self.flags() & gtk.REALIZED) != 0:
 
628
            if worksheet.state == NotebookFile.EXECUTING:
 
629
                self.__watch_window.show()
 
630
                self.__watch_window.raise_()
 
631
            else:
 
632
                self.__watch_window.hide()
 
633
 
 
634
    def on_after_insert_text(self, buf, location, text, len):
 
635
        if buf.worksheet.in_user_action() and not buf.in_modification():
 
636
            self.__inserted_in_user_action = True
 
637
 
 
638
    def on_after_delete_range(self, buf, start, end):
 
639
        if buf.worksheet.in_user_action() and not buf.in_modification():
 
640
            self.__deleted_in_user_action = True
 
641
 
 
642
    def on_after_end_user_action(self, buf):
 
643
        if not buf.worksheet.in_user_action():
 
644
            if self.__completion_popup.showing:
 
645
                if self.__inserted_in_user_action or self.__deleted_in_user_action:
 
646
                    self.__completion_popup.update()
 
647
            else:
 
648
                if self.__inserted_in_user_action and global_settings.autocomplete:
 
649
                    self.__completion_popup.popup(spontaneous=True)
 
650
            self.__inserted_in_user_action = False
 
651
            self.__deleted_in_user_action = False
 
652
 
 
653
    def on_add_custom_result(self, buf, result, anchor):
 
654
        widget = result.create_widget()
 
655
        widget.show()
 
656
        self.add_child_at_anchor(widget, anchor)
 
657
 
 
658
    def on_mark_set(self, buffer, iter, mark):
 
659
        if self.__arg_highlight_start:
 
660
            # See if the user moved the cursor out of the highlight regionb
 
661
            buf = self.get_buffer()
 
662
            if mark != buf.get_insert():
 
663
                return
 
664
 
 
665
            if (iter.compare(buf.get_iter_at_mark(self.__arg_highlight_start)) < 0 or
 
666
                iter.compare(buf.get_iter_at_mark(self.__arg_highlight_end)) > 0):
 
667
                self.__remove_arg_highlight(cursor_to_end=False)
 
668
 
 
669
    def __invalidate_char_position(self, iter):
 
670
        y, height = self.get_line_yrange(iter)
 
671
        if self.window:
 
672
            text_window = self.get_window(gtk.TEXT_WINDOW_TEXT)
 
673
            width, _ = text_window.get_size()
 
674
            text_window.invalidate_rect((0, y, width, height), False)
 
675
        
 
676
    def on_pair_location_changed(self, buf, old_position, new_position):
 
677
        if old_position:
 
678
            self.__invalidate_char_position(old_position)
 
679
        if new_position:
 
680
            self.__invalidate_char_position(new_position)
 
681
 
 
682
    def calculate(self):
 
683
        buf = self.get_buffer()
 
684
 
 
685
        buf.worksheet.calculate()
 
686
 
 
687
        # This is a hack to work around the fact that scroll_mark_onscreen()
 
688
        # doesn't wait for a size-allocate cycle, so doesn't properly handle
 
689
        # embedded request widgets
 
690
        self.size_request()
 
691
        self.size_allocate((self.allocation.x, self.allocation.y,
 
692
                            self.allocation.width, self.allocation.height))
 
693
 
 
694
        self.scroll_mark_onscreen(buf.get_insert())
 
695
 
 
696
    def copy_as_doctests(self):
 
697
        buf = self.get_buffer()
 
698
 
 
699
        bounds = buf.get_selection_bounds()
 
700
        if bounds == ():
 
701
            start, end = buf.get_iter_at_mark(buf.get_insert())
 
702
        else:
 
703
            start, end = bounds
 
704
 
 
705
        start_line, start_offset = buf.iter_to_pos(start, adjust=ADJUST_BEFORE)
 
706
        end_line, end_offset = buf.iter_to_pos(end, adjust=ADJUST_BEFORE)
 
707
 
 
708
        doctests = buf.worksheet.get_doctests(start_line, end_line + 1)
 
709
        self.get_clipboard(gtk.gdk.SELECTION_CLIPBOARD).set_text(doctests)
 
710
 
 
711
    def show_doc_popup(self, focus_popup=False):
 
712
        """Pop up the doc popup for the symbol at the insertion point, if any
 
713
 
 
714
        @param focus_popup: if True, the popup will be given keyboard focus
 
715
 
 
716
        """
 
717
 
 
718
        buf = self.get_buffer()
 
719
 
 
720
        insert = buf.get_iter_at_mark(buf.get_insert())
 
721
        line, offset = buf.iter_to_pos(insert, adjust=ADJUST_NONE)
 
722
        if line != None:
 
723
            obj, start_line, start_offset, _, _ = buf.worksheet.get_object_at_location(line, offset, include_adjacent=True)
 
724
        else:
 
725
            obj = None
 
726
 
 
727
        if obj != None:
 
728
            start = buf.pos_to_iter(start_line, start_offset)
 
729
            self.__stop_mouse_over()
 
730
            self.__doc_popup.set_target(obj)
 
731
            self.__doc_popup.position_at_location(self, start)
 
732
            if focus_popup:
 
733
                self.__doc_popup.popup_focused()
 
734
            else:
 
735
                self.__doc_popup.popup()
 
736
 
 
737
    def highlight_arg_region(self, start, end):
 
738
        """Highlight the region between start and end for argument insertion.
 
739
        A box will be drawn around the region as long as the cursor is inside
 
740
        the region. If the user hits return, the cursor will be moved to the
 
741
        end (and past a single closing punctuation at the end, if any.)
 
742
 
 
743
        @param start iter at the start of the highlight region
 
744
        @param end iter at the end of the highlight region
 
745
 
 
746
        """
 
747
 
 
748
        buf = self.get_buffer()
 
749
 
 
750
        self.__arg_highlight_start = buf.create_mark(None, start, left_gravity=True)
 
751
        self.__arg_highlight_end = buf.create_mark(None, end, left_gravity=False)