Remove arg-highlight on calculate
[reinteract.git] / lib / reinteract / shell_view.py
bloba4c42fd0bc6e28bb4f0a428f44764aa11e9109f8
1 # Copyright 2007-2011 Owen Taylor
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.
7 ########################################################################
9 import glib
10 import gobject
11 import gtk
12 import re
13 from shell_buffer import ShellBuffer, ADJUST_NONE, ADJUST_BEFORE, ADJUST_AFTER
14 from chunks import StatementChunk, CommentChunk, BlankChunk
15 from completion_popup import CompletionPopup
16 from doc_popup import DocPopup
17 from global_settings import global_settings
18 from notebook import NotebookFile
19 import sanitize_textview_ipc
20 from sidebar import Sidebar
22 LEFT_MARGIN_WIDTH = 10
24 ALL_WHITESPACE_RE = re.compile("^\s*$")
26 # We depend on knowing what priorities GDK and GTK+ use to hook in and
27 # do stuff at the right time in the update cycle
29 PRIORITY_SIDEBAR_BEFORE_RESIZE = glib.PRIORITY_HIGH_IDLE + 9
30 # GTK_PRIORITY_RESIZE = G_PRIORITY_HIGH_IDLE + 10
31 # GDK_PRIORITY_REDRAW = G_PRIORITY_HIGH_IDLE + 20
32 # GTK_TEXT_VIEW_PRIORITY_VALIDATE = GDK_PRIORITY_REDRAW + 5
33 PRIORITY_SIDEBAR_AFTER_VALIDATE = glib.PRIORITY_HIGH_IDLE + 26
34 PRIORITY_SCROLL_RESULT_ONSCREEN = glib.PRIORITY_HIGH_IDLE + 27
36 class ShellView(gtk.TextView):
37 __gsignals__ = {}
39 sidebar_open = gobject.property(type=bool, default=False)
41 def __init__(self, buf):
42 self.edit_only = buf.worksheet.edit_only
44 if not self.edit_only:
45 buf.worksheet.connect('chunk-inserted', self.on_chunk_inserted)
46 buf.worksheet.connect('chunk-changed', self.on_chunk_changed)
47 buf.worksheet.connect('chunk-status-changed', self.on_chunk_status_changed)
48 buf.worksheet.connect('chunk-deleted', self.on_chunk_deleted)
49 buf.worksheet.connect('notify::state', self.on_notify_state)
51 # Track changes to update completion
52 buf.connect_after('insert-text', self.on_after_insert_text)
53 buf.connect_after('delete-range', self.on_after_delete_range)
54 buf.connect_after('end-user-action', self.on_after_end_user_action)
56 self.__inserted_in_user_action = False
57 self.__deleted_in_user_action = False
59 if not self.edit_only:
60 self.sidebar = Sidebar()
61 else:
62 self.sidebar = None
64 buf.connect('add-custom-result', self.on_add_custom_result)
65 buf.connect('add-sidebar-results', self.on_add_sidebar_results)
66 buf.connect('remove-sidebar-results', self.on_remove_sidebar_results)
67 buf.connect('pair-location-changed', self.on_pair_location_changed)
69 gtk.TextView.__init__(self, buf)
70 if not self.edit_only:
71 self.set_border_window_size(gtk.TEXT_WINDOW_LEFT, LEFT_MARGIN_WIDTH)
72 self.set_left_margin(2)
74 # Attach a "behavior object" to the view which, by ugly hacks, makes it
75 # do simply and reasonable things for cut-and-paste and DND
76 sanitize_textview_ipc.sanitize_view(self)
78 self.add_events(gtk.gdk.LEAVE_NOTIFY_MASK)
80 self.__completion_popup = CompletionPopup(self)
81 self.__doc_popup = DocPopup()
82 self.__mouse_over_object = None
83 self.__mouse_over_timeout = None
85 self.__mouse_over_start = buf.create_mark(None, buf.get_start_iter(), True)
87 self.__arg_highlight_start = None
88 self.__arg_highlight_end = None
89 buf.connect('mark-set', self.on_mark_set)
91 self.__cursor_chunk = None
92 self.__scroll_to_result = False
93 self.__scroll_to = buf.create_mark(None, buf.get_start_iter(), True)
94 self.__scroll_idle = None
96 self.__update_sidebar_positions_idle = 0
97 self.__pixels_below_buffer = 0
98 self.__last_chunk = None
100 self.connect('destroy', self.on_destroy)
102 def __get_worksheet_line_yrange(self, line):
103 buffer_line = self.get_buffer().pos_to_iter(line)
104 return self.get_line_yrange(buffer_line)
106 def __get_chunk_yrange(self, chunk, include_padding=False, include_results=False):
107 # include_padding: whether to include pixels_above/pixels_below
108 # include_results: whether to include the results of the chunk
110 y, _ = self.__get_worksheet_line_yrange(chunk.start)
111 end_y, end_height = self.__get_worksheet_line_yrange(chunk.end - 1)
112 height = end_y + end_height - y
114 if isinstance(chunk, StatementChunk) and chunk.results_end_mark is not None:
115 if include_results:
116 buf = self.get_buffer()
117 end_iter = buf.get_iter_at_mark(chunk.results_end_mark)
118 end_line_y, end_line_height = self.get_line_yrange(end_iter)
119 height = end_line_y + end_line_height - y
121 if not include_padding:
122 y += chunk.pixels_above
123 height -= chunk.pixels_above + chunk.pixels_below
124 else:
125 # In this case, pixels_below is part of the results, which we don't include
126 if not include_padding:
127 y += chunk.pixels_above
128 height -= chunk.pixels_above
129 elif not include_padding:
130 y += chunk.pixels_above
131 height -= chunk.pixels_above + chunk.pixels_below
133 return y, height
135 def __get_worksheet_line_at_y(self, y, adjust):
136 buf = self.get_buffer()
137 (buffer_line, _) = self.get_line_at_y(y)
138 return buf.iter_to_pos(buffer_line, adjust)[0]
140 def paint_chunk(self, cr, area, chunk, fill_color, outline_color):
141 buf = self.get_buffer()
143 chunk_y, chunk_height = self.__get_chunk_yrange(chunk)
145 _, window_y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_LEFT, 0, chunk_y)
146 cr.rectangle(area.x, window_y, area.width, chunk_height)
147 cr.set_source_rgb(*fill_color)
148 cr.fill()
150 cr.rectangle(0.5, window_y + 0.5, LEFT_MARGIN_WIDTH - 1, chunk_height - 1)
151 cr.set_source_rgb(*outline_color)
152 cr.set_line_width(1)
153 cr.stroke()
155 def do_realize(self):
156 gtk.TextView.do_realize(self)
158 if not self.edit_only:
159 self.get_window(gtk.TEXT_WINDOW_LEFT).set_background(self.style.white)
161 # While the the worksheet is executing, we want to display a watch cursor
162 # Trying to override the cursor setting of GtkTextView is really hard because
163 # of things like hiding the cursor when typing, so we take the simple approach
164 # of using an input-only "cover window" that we set the cursor on and
165 # show on top of the GtkTextView's normal window.
167 self.__watch_window = gtk.gdk.Window(self.window,
168 self.allocation.width, self.allocation.height,
169 gtk.gdk.WINDOW_CHILD,
170 (gtk.gdk.SCROLL_MASK |
171 gtk.gdk.BUTTON_PRESS_MASK |
172 gtk.gdk.BUTTON_RELEASE_MASK |
173 gtk.gdk.POINTER_MOTION_MASK |
174 gtk.gdk.POINTER_MOTION_HINT_MASK),
175 gtk.gdk.INPUT_ONLY,
176 x=0, y=0)
177 self.__watch_window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
178 self.__watch_window.set_user_data(self)
180 if self.get_buffer().worksheet.state == NotebookFile.EXECUTING:
181 self.__watch_window.show()
182 self.__watch_window.raise_()
184 def on_destroy(self, obj):
185 self.__completion_popup.destroy()
186 self.__completion_popup = None
187 self.__doc_popup.destroy()
188 self.__doc_popup = None
190 if self.__scroll_idle is not None:
191 glib.source_remove(self.__scroll_idle)
193 def do_unrealize(self):
194 self.__watch_window.set_user_data(None)
195 self.__watch_window.destroy()
196 self.__watch_window = None
198 gtk.TextView.do_unrealize(self)
200 def do_size_allocate(self, allocation):
201 gtk.TextView.do_size_allocate(self, allocation)
202 if (self.flags() & gtk.REALIZED) != 0:
203 self.__watch_window.resize(allocation.width, allocation.height)
205 def __iterate_expose_chunks(self, event):
206 _, start_y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_LEFT, 0, event.area.y)
207 start_line = self.__get_worksheet_line_at_y(start_y, adjust=ADJUST_AFTER)
209 _, end_y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_LEFT, 0, event.area.y + event.area.height - 1)
210 end_line = self.__get_worksheet_line_at_y(end_y, adjust=ADJUST_BEFORE)
212 return self.get_buffer().worksheet.iterate_chunks(start_line, end_line + 1)
214 def __expose_window_left(self, event):
215 cr = event.window.cairo_create()
217 for chunk in self.__iterate_expose_chunks(event):
218 if isinstance(chunk, StatementChunk):
219 if chunk.executing:
220 self.paint_chunk(cr, event.area, chunk, (0, 1, 0), (0, 0.5, 0))
221 elif chunk.error_message is not None:
222 self.paint_chunk(cr, event.area, chunk, (1, 0, 0), (0.5, 0, 0))
223 elif chunk.needs_compile:
224 self.paint_chunk(cr, event.area, chunk, (1, 1, 0), (0.5, 0.5, 0))
225 elif chunk.needs_execute:
226 self.paint_chunk(cr, event.area, chunk, (1, 0, 1), (0.5, 0, 0.5))
227 else:
228 self.paint_chunk(cr, event.area, chunk, (0, 0, 1), (0, 0, 0.5))
230 def __draw_rect_outline(self, event, rect):
231 if (rect.y + rect.height <= event.area.y or rect.y >= event.area.y + event.area.height):
232 return
234 cr = event.window.cairo_create()
235 cr.set_line_width(1.)
236 cr.rectangle(rect.x + 0.5, rect.y + 0.5, rect.width - 1, rect.height - 1)
237 cr.set_source_rgb(0.6, 0.6, 0.6)
238 cr.stroke()
240 def __expose_arg_highlight(self, event):
241 buf = self.get_buffer()
243 # We want a rectangle enclosing the range between arg_highlight_start and
244 # arg_highlight_end; the method here isn't correct in the presence of
245 # RTL text, but the necessary Pango functionality isn't exposed to
246 # a GtkTextView user. RTL code is rare. We also don't handle multiple-line
247 # highlight regions right. (Return ends the highlight, so you'd need to paste)
248 rect = self.get_iter_location(buf.get_iter_at_mark (self.__arg_highlight_start))
249 rect.x, rect.y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
250 rect.x, rect.y)
251 rect.width = 0
252 end_rect = self.get_iter_location(buf.get_iter_at_mark (self.__arg_highlight_end))
253 end_rect.x, end_rect.y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
254 end_rect.x, end_rect.y)
255 end_rect.width = 0
257 rect = rect.union(end_rect)
259 self.__draw_rect_outline(event, rect)
261 def __expose_pair_location(self, event):
262 pair_location = self.get_buffer().get_pair_location()
263 if pair_location is None:
264 return
266 rect = self.get_iter_location(pair_location)
268 rect.x, rect.y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, rect.x, rect.y)
270 self.__draw_rect_outline(event, rect)
272 def __line_boundary_in_selection(self, line):
273 buf = self.get_buffer()
275 try:
276 line_iter = buf.pos_to_iter(line)
277 except IndexError:
278 return
280 sel_start, sel_end = buf.get_selection_bounds()
281 return sel_start.compare(line_iter) < 0 and sel_end.compare(line_iter) >= 0
283 def __expose_padding_areas(self, event):
284 buf = self.get_buffer()
286 # This is a fixup for the padding areas we add to chunks when leaving
287 # space for sidebar widgets - gtk.TextView draws these areas as part
288 # of the chunk that is being padded (so partially selected when the
289 # line is partially selected.) This just looks wrong, so we paint over
290 # that so that padding areas are _between_ lines.
292 cr = event.window.cairo_create()
294 left_margin = self.get_property('left-margin')
295 right_margin = self.get_property('right-margin')
296 selection_left, _ = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
297 left_margin, 0)
298 window_width, _ = event.window.get_size()
299 # 1 here is gtktextview.c:SPACE_FOR_CURSOR - padding on the right for the cursor
300 selection_width = window_width - left_margin - right_margin - 1
302 for chunk in self.__iterate_expose_chunks(event):
303 if chunk.pixels_above != 0 or chunk.pixels_below != 0:
304 total_y, total_height = self.__get_chunk_yrange(chunk, include_padding=True, include_results=True)
305 no_pad_y, no_pad_height = self.__get_chunk_yrange(chunk, include_padding=False, include_results=True)
306 _, total_y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, 0, total_y)
307 _, no_pad_y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, 0, no_pad_y)
309 if chunk.pixels_above != 0:
310 cr.rectangle(selection_left, total_y,
311 selection_width, no_pad_y - total_y)
312 if self.__line_boundary_in_selection(chunk.start):
313 cr.set_source_color(self.style.base[gtk.STATE_SELECTED])
314 else:
315 cr.set_source_color(self.style.base[self.state])
316 cr.fill()
318 if chunk.pixels_below != 0:
319 cr.rectangle(selection_left, no_pad_y + no_pad_height,
320 selection_width, total_y + total_height - (no_pad_y + no_pad_height))
321 if self.__line_boundary_in_selection(chunk.end):
322 cr.set_source_color(self.style.base[gtk.STATE_SELECTED])
323 else:
324 cr.set_source_color(self.style.base[self.state])
325 cr.fill()
327 def __update_last_chunk(self, new_last_chunk):
328 buf = self.get_buffer()
330 if self.__last_chunk and self.__pixels_below_buffer != 0:
331 buf.set_pixels_below(self.__last_chunk, 0)
333 self.__last_chunk = new_last_chunk
334 if self.__pixels_below_buffer != 0:
335 buf.set_pixels_below(self.__last_chunk, self.__pixels_below_buffer)
337 def __set_pixels_below_buffer(self, pixels_below):
338 self.set_pixels_below_lines(pixels_below)
339 self.get_buffer().set_pixels_below(self.__last_chunk, pixels_below)
340 self.__pixels_below_buffer = pixels_below
342 def __update_sidebar_positions(self):
343 # Each StatementChunk with sidebar widgets has already been added as a "slot"
344 # to the sidebar. For each of these slots we determine a vertical
345 # range which includes the StatementChunk, and any preceding CommentChunk.
347 # We also handle adding space before slots with sidebar widgets and at the end
348 # of the buffer so that previous sidebar widgets have enough room.
350 # Rather than trying to update things incrementally, we just run
351 # through the chunks in the buffer twice and compute the positions from
352 # scratch.
354 self.__update_sidebar_positions_idle = 0
355 buf = self.get_buffer()
357 set_chunks = set()
358 widget_end_y = None
360 slots = self.sidebar.slots
361 if len(slots) > 0:
362 self.sidebar.freeze_positions()
364 # Main position loop where we determine the slot positions
366 # The "slot_extra" here is the number of pixels inside the slot which were
367 # added as chunk_above/chunk_below in previous runs of this code and will
368 # be removed unless we add them again in this run.
370 slot_index = 0
371 for chunk in buf.worksheet.iterate_chunks(0, slots[-1].chunk.end):
372 if chunk == slots[slot_index].chunk:
373 if slot_start_chunk is None:
374 slot_start_chunk = chunk
375 slot_extra = chunk.pixels_above + chunk.pixels_below
376 else:
377 slot_extra += chunk.pixels_above + chunk.pixels_below
379 slot_start_y, _ = self.__get_chunk_yrange(slot_start_chunk,
380 include_results=True, include_padding=True)
382 chunk_start_y, chunk_height = self.__get_chunk_yrange(chunk,
383 include_results=True, include_padding=True)
384 slot_height = chunk_start_y + chunk_height - slot_start_y - slot_extra
386 if widget_end_y is not None and widget_end_y > slot_start_y:
387 buf.set_pixels_above(slot_start_chunk, widget_end_y - slot_start_y)
388 set_chunks.add(slot_start_chunk)
389 slot_start_y = widget_end_y
391 slots[slot_index].set_position(slot_start_y, slot_height)
393 widget_end_y = slot_start_y + slots[slot_index].get_results_height()
394 slot_index += 1
395 slot_start_chunk = None
396 else:
397 if isinstance(chunk, CommentChunk):
398 slot_start_chunk = chunk
399 slot_extra = chunk.pixels_above + chunk.pixels_below
400 elif isinstance(chunk, StatementChunk):
401 slot_start_chunk = None
402 elif slot_start_chunk is not None:
403 slot_extra += chunk.pixels_above + chunk.pixels_below
405 self.sidebar.thaw_positions()
407 # Any chunk we where didn't assign pixels_above needs to have pixels_above
408 # set back to zero - it might have been set for some previous pass
409 for chunk in buf.worksheet.iterate_chunks():
410 if chunk.pixels_above != 0 and not chunk in set_chunks:
411 buf.set_pixels_above(chunk, 0)
413 # Finally, add space at the end of the buffer, if necessary
415 pixels_below = 0
416 if widget_end_y is not None:
417 end_line_y, end_line_height = self.get_line_yrange(buf.get_end_iter())
418 end_y = end_line_y + end_line_height - self.__pixels_below_buffer
419 if widget_end_y > end_y:
420 pixels_below = widget_end_y - end_y
422 self.__set_pixels_below_buffer(pixels_below)
424 return False
426 def __queue_update_sidebar_positions(self, before_resize=False):
428 # We want to position the sidebar results with respect to the buffer
429 # contents, so position has to be done after validation finishes, but there
430 # is no hook to figure that out for gtk.TextView(). So, what we do is
431 # that we figure that if anything _interesting_ gets repositioned there
432 # will be an expose event, and in the expose event, add an idle that
433 # is low enough priority to run after validation finishes.
435 # Now, in the positioning process, we actually _modify_ the buffer
436 # contents by adding space above chunks to give previoous sidebar
437 # results enough room. This produces a quasi-circular dependency
438 # and a loop - but since the addition of space modifies the layout
439 # in a predictable way, we can just subtract that back out. What we
440 # expect to happen is:
442 # - Buffer modification
443 # - Expose event
444 # - Our idle runs, we compute positions, and add space
445 # - Expose event
446 # - Our idle runs, we compute positions, compensating for the added
447 # space; the new computation is the same, and nothing further happens.
449 # (If there is a bug in the compensation code, an infinite loop will
450 # be triggered.)
452 if len(self.sidebar.slots) == 0 and self.__pixels_below_buffer == 0:
453 return
455 if before_resize and self.__update_sidebar_positions_idle != 0:
456 glib.source_remove(self.__update_sidebar_positions_idle)
457 self.__update_sidebar_positions_idle = 0
459 if self.__update_sidebar_positions_idle == 0:
460 if before_resize:
461 priority = PRIORITY_SIDEBAR_BEFORE_RESIZE
462 else:
463 priority = PRIORITY_SIDEBAR_AFTER_VALIDATE
465 self.__update_sidebar_positions_idle = glib.idle_add(self.__update_sidebar_positions,
466 priority=priority)
468 def do_expose_event(self, event):
469 buf = self.get_buffer()
471 if not self.edit_only and event.window == self.get_window(gtk.TEXT_WINDOW_LEFT):
472 self.__queue_update_sidebar_positions()
473 self.__expose_window_left(event)
474 return False
476 gtk.TextView.do_expose_event(self, event)
478 if event.window == self.get_window(gtk.TEXT_WINDOW_TEXT):
479 if self.__arg_highlight_start:
480 self.__expose_arg_highlight(event)
481 else:
482 self.__expose_pair_location(event)
483 if buf.get_has_selection():
484 self.__expose_padding_areas(event)
486 return False
488 # This is likely overengineered, since we're going to try as hard as possible not to
489 # have tabs in our worksheets. We don't do the funky handling of \f.
490 def __count_indent(self, text):
491 indent = 0
492 for c in text:
493 if c == ' ':
494 indent += 1
495 elif c == '\t':
496 indent += 8 - (indent % 8)
497 else:
498 break
500 return indent
502 def __find_outdent(self, iter):
503 buf = self.get_buffer()
504 line, _ = buf.iter_to_pos(iter)
506 current_indent = self.__count_indent(buf.worksheet.get_line(line))
508 while line > 0:
509 line -= 1
510 line_text = buf.worksheet.get_line(line)
511 # Empty lines don't establish indentation
512 if ALL_WHITESPACE_RE.match(line_text):
513 continue
515 indent = self.__count_indent(line_text)
516 if indent < current_indent:
517 return re.match(r"^[\t ]*", line_text).group(0)
519 return ""
521 def __find_default_indent(self, iter):
522 buf = self.get_buffer()
523 line, offset = buf.iter_to_pos(iter)
525 while line > 0:
526 line -= 1
527 chunk = buf.worksheet.get_chunk(line)
528 if isinstance(chunk, StatementChunk):
529 return chunk.tokenized.get_next_line_indent(line - chunk.start)
530 elif isinstance(chunk, CommentChunk) or isinstance(chunk, BlankChunk):
531 return " " * self.__count_indent(buf.worksheet.get_line(line))
533 return ""
535 def __reindent_line(self, iter, indent_text):
536 buf = self.get_buffer()
537 insert_mark = buf.get_insert()
539 line, pos = buf.iter_to_pos(iter, adjust=ADJUST_NONE)
540 if line == None:
541 return
543 line_text = buf.worksheet.get_line(line)
544 prefix = re.match(r"^[\t ]*", line_text).group(0)
546 diff = self.__count_indent(indent_text) - self.__count_indent(prefix)
547 if diff == 0:
548 return 0
550 common_len = 0
551 for a, b in zip(prefix, indent_text):
552 if a != b:
553 break
554 common_len += 1
556 start = iter.copy()
557 start.set_line_offset(common_len)
558 end = iter.copy()
559 end.set_line_offset(len(prefix))
561 # Nitpicky-detail. If the selection starts at the start of the line, and we are
562 # inserting white-space there, then the whitespace should be *inside* the selection
563 mark_to_start = None
564 if common_len == 0 and buf.get_has_selection():
565 if buf.get_iter_at_mark(insert_mark).compare(start) == 0:
566 mark_to_start = mark
568 mark = buf.get_selection_bound()
569 if buf.get_iter_at_mark(mark).compare(start) == 0:
570 mark_to_start = mark
572 buf.delete(start, end)
573 buf.insert(end, indent_text[common_len:])
575 if mark_to_start is not None:
576 end.set_line_offset(0)
577 buf.move_mark(mark_to_start, end)
579 insert_iter = buf.get_iter_at_mark(insert_mark)
580 insert_line, _ = buf.iter_to_pos(insert_iter, adjust=ADJUST_NONE)
581 if insert_line == line:
582 # We shifted the insertion cursor around behind gtk.TextView's back,
583 # by inserting text on the same line; this will result in a wrong
584 # virtual cursor position. Calling buf.place_cursor() will cause
585 # the virtual cursor position to be reset to the proper value.
586 buf.place_cursor(insert_iter)
588 return diff
590 def __reindent_selection(self, outdent):
591 buf = self.get_buffer()
593 bounds = buf.get_selection_bounds()
594 if bounds == ():
595 insert_mark = buf.get_insert()
596 bounds = buf.get_iter_at_mark(insert_mark), buf.get_iter_at_mark(insert_mark)
597 start, end = bounds
599 line, _ = buf.iter_to_pos(start, adjust=ADJUST_AFTER)
600 end_line, end_offset = buf.iter_to_pos(end, adjust=ADJUST_BEFORE)
601 if end_offset == 0 and end_line > line:
602 end_line -= 1
604 iter = buf.pos_to_iter(line)
606 if outdent:
607 indent_text = self.__find_outdent(iter)
608 else:
609 indent_text = self.__find_default_indent(iter)
611 diff = self.__reindent_line(iter, indent_text)
613 if not buf.get_has_selection():
614 iter = buf.get_iter_at_mark(buf.get_insert())
615 if iter.get_line_offset() < len(indent_text):
616 iter.set_line_offset(len(indent_text))
617 buf.place_cursor(iter)
619 while True:
620 line += 1
621 if line > end_line:
622 return
624 iter = buf.pos_to_iter(line)
625 current_indent = self.__count_indent(buf.worksheet.get_line(line))
626 self.__reindent_line(iter, max(0, " " * (current_indent + diff)))
628 def __hide_completion(self):
629 if self.__completion_popup.showing:
630 self.__completion_popup.popdown()
632 def do_focus_out_event(self, event):
633 self.__hide_completion()
634 self.__doc_popup.popdown()
635 return gtk.TextView.do_focus_out_event(self, event)
637 def __rewrite_window(self, event):
638 # Mouse events on the "watch window" that covers the text view
639 # during calculation need to be forwarded to the real text window
640 # since it looks bad if keynav works, but you can't click on the
641 # window to set the cursor, select text, and so forth
643 if event.window == self.__watch_window:
644 event.window = self.get_window(gtk.TEXT_WINDOW_TEXT)
646 # Events on the left-margin window also get written so the user can
647 # click there when starting a drag selection. We need to adjust the
648 # X coordinate in that case
649 if not self.edit_only and event.window == self.get_window(gtk.TEXT_WINDOW_LEFT):
650 event.window = self.get_window(gtk.TEXT_WINDOW_TEXT)
651 if event.type == gtk.gdk._3BUTTON_PRESS:
652 # Workaround for http://bugzilla.gnome.org/show_bug.cgi?id=573664
653 event.x = 50.
654 else:
655 event.x -= LEFT_MARGIN_WIDTH
657 def do_button_press_event(self, event):
658 self.__rewrite_window(event)
660 self.__doc_popup.popdown()
662 return gtk.TextView.do_button_press_event(self, event)
664 def do_button_release_event(self, event):
665 self.__rewrite_window(event)
667 return gtk.TextView.do_button_release_event(self, event)
669 def do_motion_notify_event(self, event):
670 self.__rewrite_window(event)
672 return gtk.TextView.do_motion_notify_event(self, event)
674 def __remove_arg_highlight(self, cursor_to_end=True):
675 buf = self.get_buffer()
677 end = buf.get_iter_at_mark (self.__arg_highlight_end)
679 buf.delete_mark(self.__arg_highlight_start)
680 self.__arg_highlight_start = None
681 buf.delete_mark(self.__arg_highlight_end)
682 self.__arg_highlight_end = None
684 if cursor_to_end:
685 # If the arg_highlight ends at closing punctuation, skip over it
686 tmp = end.copy()
687 tmp.forward_char()
688 text = buf.get_slice(end, tmp)
690 if text in (")", "]", "}"):
691 buf.place_cursor(tmp)
692 else:
693 buf.place_cursor(end)
695 def do_key_press_event(self, event):
696 buf = self.get_buffer()
698 if self.__completion_popup.focused and self.__completion_popup.on_key_press_event(event):
699 return True
701 if self.__doc_popup.focused:
702 if self.__doc_popup.on_key_press_event(event):
703 return True
705 if not self.edit_only and event.keyval in (gtk.keysyms.F2, gtk.keysyms.KP_F2):
706 self.__hide_completion()
708 if self.__doc_popup.showing:
709 self.__doc_popup.focus()
710 else:
711 self.show_doc_popup(focus_popup=True)
713 return True
715 if not self.__doc_popup.focused:
716 self.__doc_popup.popdown()
718 if event.keyval in (gtk.keysyms.KP_Enter, gtk.keysyms.Return):
719 self.__hide_completion()
721 if self.__arg_highlight_start:
722 self.__remove_arg_highlight()
723 self.__doc_popup.popdown()
724 return True
726 increase_indent = False
727 insert = buf.get_iter_at_mark(buf.get_insert())
728 line, pos = buf.iter_to_pos(insert, adjust=ADJUST_NONE)
730 # Inserting return inside a ResultChunk would normally do nothing
731 # but we want to make it insert a line after the chunk
732 if line is None and not buf.get_has_selection():
733 line, pos = buf.iter_to_pos(insert, adjust=ADJUST_BEFORE)
734 iter = buf.pos_to_iter(line, -1)
735 buf.place_cursor(iter)
736 buf.insert_interactive(iter, "\n", True)
738 return True
740 buf.begin_user_action()
742 gtk.TextView.do_key_press_event(self, event)
743 # We need the chunks to be updated when computing the line indents
744 buf.worksheet.rescan()
746 insert = buf.get_iter_at_mark(buf.get_insert())
748 self.__reindent_line(insert, self.__find_default_indent(insert))
750 # If we have two comment lines in a row, assume a block comment
751 if (line > 0 and
752 isinstance(buf.worksheet.get_chunk(line), CommentChunk) and
753 isinstance(buf.worksheet.get_chunk(line - 1), CommentChunk)):
754 self.get_buffer().insert_interactive_at_cursor("# ", True)
756 buf.end_user_action()
758 return True
759 elif event.keyval in (gtk.keysyms.Tab, gtk.keysyms.KP_Tab) and event.state & gtk.gdk.CONTROL_MASK == 0:
760 buf.begin_user_action()
761 self.__reindent_selection(outdent=False)
762 buf.end_user_action()
764 return True
765 elif event.keyval == gtk.keysyms.ISO_Left_Tab and event.state & gtk.gdk.CONTROL_MASK == 0:
766 buf.begin_user_action()
767 self.__reindent_selection(outdent=True)
768 buf.end_user_action()
770 return True
771 elif event.keyval == gtk.keysyms.space and event.state & gtk.gdk.CONTROL_MASK != 0:
772 if self.__completion_popup.showing:
773 if self.__completion_popup.spontaneous:
774 self.__completion_popup.popup(spontaneous=False)
775 else:
776 self.__completion_popup.popdown()
777 else:
778 if self.__doc_popup.showing:
779 self.__doc_popup.popdown()
780 self.__completion_popup.popup(spontaneous=False)
781 return True
782 elif event.keyval in (gtk.keysyms.z, gtk.keysyms.Z) and \
783 (event.state & gtk.gdk.CONTROL_MASK) != 0 and \
784 (event.state & gtk.gdk.SHIFT_MASK) == 0:
785 buf.worksheet.undo()
787 return True
788 # This is the gedit/gtksourceview binding to cause your hands to fall off
789 elif event.keyval in (gtk.keysyms.z, gtk.keysyms.Z) and \
790 (event.state & gtk.gdk.CONTROL_MASK) != 0 and \
791 (event.state & gtk.gdk.SHIFT_MASK) != 0:
792 buf.worksheet.redo()
794 return True
795 # This is the familiar binding (Eclipse, etc). Firefox supports both
796 elif event.keyval in (gtk.keysyms.y, gtk.keysyms.Y) and event.state & gtk.gdk.CONTROL_MASK != 0:
797 buf.worksheet.redo()
799 return True
801 return gtk.TextView.do_key_press_event(self, event)
803 def __show_mouse_over(self):
804 self.__mouse_over_timeout = None
806 if self.__completion_popup.showing:
807 return
809 self.__doc_popup.set_target(self.__mouse_over_object)
810 location = self.get_buffer().get_iter_at_mark(self.__mouse_over_start)
811 self.__doc_popup.position_at_location(self, location)
812 self.__doc_popup.popup()
814 return False
816 def __stop_mouse_over(self):
817 if self.__mouse_over_timeout:
818 glib.source_remove(self.__mouse_over_timeout)
819 self.__mouse_over_timeout = None
821 self.__mouse_over_object = None
823 def do_motion_notify_event(self, event):
824 # Successful mousing-over depends on knowing the types of symbols so doing the
825 # checks are pointless in edit-only mode
826 if not self.edit_only and event.window == self.get_window(gtk.TEXT_WINDOW_TEXT) and not self.__doc_popup.focused:
827 buf = self.get_buffer()
829 x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, int(event.x), int(event.y))
830 iter, _ = self.get_iter_at_position(x, y)
831 line, offset = buf.iter_to_pos(iter, adjust=ADJUST_NONE)
832 if line is not None:
833 obj, start_line, start_offset, _,_ = buf.worksheet.get_object_at_location(line, offset)
834 else:
835 obj = None
837 if not obj is self.__mouse_over_object:
838 self.__stop_mouse_over()
839 self.__doc_popup.popdown()
840 if obj is not None:
841 start = buf.pos_to_iter(start_line, start_offset)
842 buf.move_mark(self.__mouse_over_start, start)
844 self.__mouse_over_object = obj
845 try:
846 timeout = self.get_settings().get_property('gtk-tooltip-timeout')
847 except TypeError: # GTK+ < 2.12
848 timeout = 500
849 self.__mouse_over_timeout = glib.timeout_add(timeout, self.__show_mouse_over)
851 return gtk.TextView.do_motion_notify_event(self, event)
853 def do_leave_notify_event(self, event):
854 self.__stop_mouse_over()
855 if not self.__doc_popup.focused:
856 self.__doc_popup.popdown()
857 return False
859 def do_backspace(self):
860 buf = self.get_buffer()
862 if buf.get_has_selection():
863 return gtk.TextView.do_backspace(self)
865 insert = buf.get_iter_at_mark(buf.get_insert())
866 line, offset = buf.iter_to_pos(insert)
868 current_chunk = buf.worksheet.get_chunk(line)
869 if isinstance(current_chunk, StatementChunk) or isinstance(current_chunk, BlankChunk):
870 line_text = buf.worksheet.get_line(line)[0:offset]
872 if re.match(r"^[\t ]+$", line_text) and not (line > 0 and self.is_continued(line - 1)):
873 self.__reindent_selection(outdent=True)
874 return
876 return gtk.TextView.do_backspace(self)
878 def is_continued(self, line):
879 """Determine if line causes a continuation."""
880 buf = self.get_buffer()
881 chunk = buf.worksheet.get_chunk(line)
882 while not isinstance(chunk, StatementChunk) and line > 0:
883 line -= 1
884 chunk = buf.worksheet.get_chunk(line)
885 return isinstance(chunk, StatementChunk) and chunk.tokenized.is_continued(line - chunk.start)
887 def __invalidate_status(self, chunk):
888 buf = self.get_buffer()
890 chunk_y, chunk_height = self.__get_chunk_yrange(chunk, include_padding=True)
892 _, window_y = self.buffer_to_window_coords(gtk.TEXT_WINDOW_LEFT, 0, chunk_y)
894 if self.window:
895 left_margin_window = self.get_window(gtk.TEXT_WINDOW_LEFT)
896 left_margin_window.invalidate_rect((0, window_y, LEFT_MARGIN_WIDTH, chunk_height),
897 False)
899 def on_chunk_inserted(self, worksheet, chunk):
900 buf = self.get_buffer()
902 self.__invalidate_status(chunk)
903 if chunk.end == buf.worksheet.get_line_count():
904 self.__update_last_chunk(chunk)
906 def on_chunk_changed(self, worksheet, chunk, changed_lines):
907 self.__invalidate_status(chunk)
909 def on_chunk_status_changed(self, worksheet, chunk):
910 self.__invalidate_status(chunk)
912 if self.__cursor_chunk == chunk and not chunk.executing:
913 # This is the chunk with the cursor and it's done executing
914 self.__scroll_idle = glib.idle_add(self.scroll_result_onscreen,
915 priority=PRIORITY_SCROLL_RESULT_ONSCREEN)
917 def scroll_result_onscreen(self):
918 """Scroll so that both the insertion cursor and the following result
919 are onscreen. If we cannot get both, get as much of the result while
920 still getting the insertion cursor."""
922 buf = self.get_buffer()
923 if self.__scroll_to_result:
924 if self.__cursor_chunk.sidebar_results:
925 # We assume that we don't have both sidebar and normal results
926 for slot in self.sidebar.slots:
927 if slot.chunk == self.__cursor_chunk:
928 self.sidebar.scroll_to_slot_results(slot)
929 break
930 else:
931 try:
932 iter = buf.pos_to_iter(self.__cursor_chunk.end) # Start of next chunk
933 except IndexError:
934 iter = buf.get_end_iter()
935 else:
936 iter.backward_line() # Move to line before start of next chunk
938 buf.move_mark(self.__scroll_to, iter)
939 self.scroll_mark_onscreen(self.__scroll_to)
941 self.scroll_mark_onscreen(buf.get_insert())
943 self.__cursor_chunk = None
944 self.__scroll_idle = None
945 return False
947 def on_chunk_deleted(self, worksheet, chunk):
948 buf = self.get_buffer()
950 if self.__cursor_chunk == chunk:
951 self.__cursor_chunk = None
952 if self.__scroll_idle is not None:
953 glib.source_remove(self.__scroll_idle)
954 self.__scroll_idle = None
956 if self.__last_chunk == chunk:
957 self.__last_chunk = None
959 new_last_chunk = buf.worksheet.get_chunk(buf.worksheet.get_line_count() - 1)
960 # We might find a newly created chunk that hasn't been "inserted" yet -
961 # in that case, we wait until we get chunk-inserted
962 if hasattr(new_last_chunk, 'pixels_above'):
963 self.__update_last_chunk(new_last_chunk)
965 def on_notify_state(self, worksheet, param_spec):
966 if (self.flags() & gtk.REALIZED) != 0:
967 if worksheet.state == NotebookFile.EXECUTING:
968 self.__watch_window.show()
969 self.__watch_window.raise_()
970 else:
971 self.__watch_window.hide()
973 self.set_cursor_visible(worksheet.state != NotebookFile.EXECUTING)
975 def on_after_insert_text(self, buf, location, text, len):
976 if buf.worksheet.in_user_action() and not buf.in_modification():
977 self.__inserted_in_user_action = True
979 def on_after_delete_range(self, buf, start, end):
980 if buf.worksheet.in_user_action() and not buf.in_modification():
981 self.__deleted_in_user_action = True
983 def on_after_end_user_action(self, buf):
984 if not buf.worksheet.in_user_action():
985 if self.__completion_popup.showing:
986 if self.__inserted_in_user_action or self.__deleted_in_user_action:
987 self.__completion_popup.update()
988 else:
989 if self.__inserted_in_user_action and global_settings.autocomplete:
990 self.__completion_popup.popup(spontaneous=True)
991 self.__inserted_in_user_action = False
992 self.__deleted_in_user_action = False
994 def on_add_custom_result(self, buf, result, anchor):
995 widget = result.create_widget()
996 widget.show()
997 self.add_child_at_anchor(widget, anchor)
999 def on_add_sidebar_results(self, buf, chunk):
1000 if len(self.sidebar.slots) == 0:
1001 self.sidebar_open = True
1003 widgets = []
1004 for result in chunk.sidebar_results:
1005 widget = result.create_widget()
1006 widget.show()
1007 widgets.append(widget)
1009 self.sidebar.add_slot(chunk, widgets)
1011 # The final sidebar position calculation aren't be done until
1012 # we've finished revalidating the text view, but we need
1013 # to get some approproximate guess done before we allocate
1014 # the sidebar, or we'll get flashing
1015 self.__queue_update_sidebar_positions(before_resize=True)
1017 def on_remove_sidebar_results(self, buf, chunk):
1018 if len(self.sidebar.slots) == 1:
1019 self.__queue_update_sidebar_positions()
1020 self.sidebar_open = False
1022 self.sidebar.remove_slot(chunk)
1024 def on_mark_set(self, buffer, iter, mark):
1025 if self.__arg_highlight_start:
1026 # See if the user moved the cursor out of the highlight regionb
1027 buf = self.get_buffer()
1028 if mark != buf.get_insert():
1029 return
1031 if (iter.compare(buf.get_iter_at_mark(self.__arg_highlight_start)) < 0 or
1032 iter.compare(buf.get_iter_at_mark(self.__arg_highlight_end)) > 0):
1033 self.__remove_arg_highlight(cursor_to_end=False)
1035 def __invalidate_char_position(self, iter):
1036 y, height = self.get_line_yrange(iter)
1037 if self.window:
1038 text_window = self.get_window(gtk.TEXT_WINDOW_TEXT)
1039 width, _ = text_window.get_size()
1040 text_window.invalidate_rect((0, y, width, height), False)
1042 def on_pair_location_changed(self, buf, old_position, new_position):
1043 if old_position:
1044 self.__invalidate_char_position(old_position)
1045 if new_position:
1046 self.__invalidate_char_position(new_position)
1048 #######################################################
1049 # Public API
1050 #######################################################
1052 def calculate(self, end_at_insert=False):
1053 buf = self.get_buffer()
1054 line, _ = buf.iter_to_pos(buf.get_iter_at_mark(buf.get_insert()), ADJUST_BEFORE)
1056 if end_at_insert:
1057 end_line = line + 1 # +1 to include line with cursor
1058 else:
1059 end_line = None
1061 self.__hide_completion()
1062 if self.__arg_highlight_start:
1063 self.__remove_arg_highlight(cursor_to_end=False)
1064 self.__doc_popup.popdown()
1066 buf.worksheet.calculate(end_line=end_line)
1068 # If the cursor is in a StatementChunk or we are executing up to
1069 # the cursor, we will scroll the result into view. Otherwise,
1070 # we will just scroll the cursor into view. If there is no
1071 # StatementChunk between the cursor and the beginning of the
1072 # worksheet, or if the StatementChunk has already been executed,
1073 # there is no need to scroll at all.
1074 statement_line = line
1075 in_statement = True
1076 while line >= 0:
1077 chunk = buf.worksheet.get_chunk(statement_line)
1078 if isinstance(chunk, StatementChunk):
1079 if chunk.needs_compile or chunk.needs_execute:
1080 self.__cursor_chunk = chunk
1081 self.__scroll_to_result = end_at_insert or in_statement
1082 return
1083 statement_line -= 1
1084 in_statement = False
1086 def copy_as_doctests(self):
1087 buf = self.get_buffer()
1089 bounds = buf.get_selection_bounds()
1090 if bounds == ():
1091 start, end = buf.get_iter_at_mark(buf.get_insert())
1092 else:
1093 start, end = bounds
1095 start_line, start_offset = buf.iter_to_pos(start, adjust=ADJUST_BEFORE)
1096 end_line, end_offset = buf.iter_to_pos(end, adjust=ADJUST_BEFORE)
1098 doctests = buf.worksheet.get_doctests(start_line, end_line + 1)
1099 self.get_clipboard(gtk.gdk.SELECTION_CLIPBOARD).set_text(doctests)
1101 def show_doc_popup(self, focus_popup=False):
1102 """Pop up the doc popup for the symbol at the insertion point, if any
1104 @param focus_popup: if True, the popup will be given keyboard focus
1108 buf = self.get_buffer()
1110 insert = buf.get_iter_at_mark(buf.get_insert())
1111 line, offset = buf.iter_to_pos(insert, adjust=ADJUST_NONE)
1112 if line is not None:
1113 obj, start_line, start_offset, _, _ = buf.worksheet.get_object_at_location(line, offset, include_adjacent=True)
1114 else:
1115 obj = None
1117 if obj is not None:
1118 start = buf.pos_to_iter(start_line, start_offset)
1119 self.__stop_mouse_over()
1120 self.__doc_popup.set_target(obj)
1121 self.__doc_popup.position_at_location(self, start)
1122 if focus_popup:
1123 self.__doc_popup.popup_focused()
1124 else:
1125 self.__doc_popup.popup()
1127 def highlight_arg_region(self, start, end):
1128 """Highlight the region between start and end for argument insertion.
1129 A box will be drawn around the region as long as the cursor is inside
1130 the region. If the user hits return, the cursor will be moved to the
1131 end (and past a single closing punctuation at the end, if any.)
1133 @param start iter at the start of the highlight region
1134 @param end iter at the end of the highlight region
1138 buf = self.get_buffer()
1140 self.__arg_highlight_start = buf.create_mark(None, start, left_gravity=True)
1141 self.__arg_highlight_end = buf.create_mark(None, end, left_gravity=False)