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 ########################################################################
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
):
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()
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:
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
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
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
)
150 cr
.rectangle(0.5, window_y
+ 0.5, LEFT_MARGIN_WIDTH
- 1, chunk_height
- 1)
151 cr
.set_source_rgb(*outline_color
)
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
),
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
):
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))
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
):
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)
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
,
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
)
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:
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()
276 line_iter
= buf
.pos_to_iter(line
)
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
,
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
])
315 cr
.set_source_color(self
.style
.base
[self
.state
])
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
])
324 cr
.set_source_color(self
.style
.base
[self
.state
])
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
354 self
.__update
_sidebar
_positions
_idle
= 0
355 buf
= self
.get_buffer()
360 slots
= self
.sidebar
.slots
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.
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
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()
395 slot_start_chunk
= None
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
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
)
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
444 # - Our idle runs, we compute positions, and add space
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
452 if len(self
.sidebar
.slots
) == 0 and self
.__pixels
_below
_buffer
== 0:
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:
461 priority
= PRIORITY_SIDEBAR_BEFORE_RESIZE
463 priority
= PRIORITY_SIDEBAR_AFTER_VALIDATE
465 self
.__update
_sidebar
_positions
_idle
= glib
.idle_add(self
.__update
_sidebar
_positions
,
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
)
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
)
482 self
.__expose
_pair
_location
(event
)
483 if buf
.get_has_selection():
484 self
.__expose
_padding
_areas
(event
)
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
):
496 indent
+= 8 - (indent
% 8)
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
))
510 line_text
= buf
.worksheet
.get_line(line
)
511 # Empty lines don't establish indentation
512 if ALL_WHITESPACE_RE
.match(line_text
):
515 indent
= self
.__count
_indent
(line_text
)
516 if indent
< current_indent
:
517 return re
.match(r
"^[\t ]*", line_text
).group(0)
521 def __find_default_indent(self
, iter):
522 buf
= self
.get_buffer()
523 line
, offset
= buf
.iter_to_pos(iter)
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
))
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
)
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
)
551 for a
, b
in zip(prefix
, indent_text
):
557 start
.set_line_offset(common_len
)
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
564 if common_len
== 0 and buf
.get_has_selection():
565 if buf
.get_iter_at_mark(insert_mark
).compare(start
) == 0:
568 mark
= buf
.get_selection_bound()
569 if buf
.get_iter_at_mark(mark
).compare(start
) == 0:
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
)
590 def __reindent_selection(self
, outdent
):
591 buf
= self
.get_buffer()
593 bounds
= buf
.get_selection_bounds()
595 insert_mark
= buf
.get_insert()
596 bounds
= buf
.get_iter_at_mark(insert_mark
), buf
.get_iter_at_mark(insert_mark
)
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
:
604 iter = buf
.pos_to_iter(line
)
607 indent_text
= self
.__find
_outdent
(iter)
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)
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
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
685 # If the arg_highlight ends at closing punctuation, skip over it
688 text
= buf
.get_slice(end
, tmp
)
690 if text
in (")", "]", "}"):
691 buf
.place_cursor(tmp
)
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
):
701 if self
.__doc
_popup
.focused
:
702 if self
.__doc
_popup
.on_key_press_event(event
):
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()
711 self
.show_doc_popup(focus_popup
=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()
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)
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
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()
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()
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()
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)
776 self
.__completion
_popup
.popdown()
778 if self
.__doc
_popup
.showing
:
779 self
.__doc
_popup
.popdown()
780 self
.__completion
_popup
.popup(spontaneous
=False)
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:
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:
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:
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
:
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()
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
)
833 obj
, start_line
, start_offset
, _
,_
= buf
.worksheet
.get_object_at_location(line
, offset
)
837 if not obj
is self
.__mouse
_over
_object
:
838 self
.__stop
_mouse
_over
()
839 self
.__doc
_popup
.popdown()
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
846 timeout
= self
.get_settings().get_property('gtk-tooltip-timeout')
847 except TypeError: # GTK+ < 2.12
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()
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)
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:
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
)
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
),
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
)
932 iter = buf
.pos_to_iter(self
.__cursor
_chunk
.end
) # Start of next chunk
934 iter = buf
.get_end_iter()
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
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_()
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()
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()
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
1004 for result
in chunk
.sidebar_results
:
1005 widget
= result
.create_widget()
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():
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)
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
):
1044 self
.__invalidate
_char
_position
(old_position
)
1046 self
.__invalidate
_char
_position
(new_position
)
1048 #######################################################
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
)
1057 end_line
= line
+ 1 # +1 to include line with cursor
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
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
1084 in_statement
= False
1086 def copy_as_doctests(self
):
1087 buf
= self
.get_buffer()
1089 bounds
= buf
.get_selection_bounds()
1091 start
, end
= buf
.get_iter_at_mark(buf
.get_insert())
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)
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
)
1123 self
.__doc
_popup
.popup_focused()
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)