controls.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. """
  2. User interface Controls for the layout.
  3. """
  4. from __future__ import unicode_literals
  5. from abc import ABCMeta, abstractmethod
  6. from collections import namedtuple
  7. from six import with_metaclass
  8. from six.moves import range
  9. from prompt_toolkit.cache import SimpleCache
  10. from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
  11. from prompt_toolkit.filters import to_cli_filter
  12. from prompt_toolkit.mouse_events import MouseEventType
  13. from prompt_toolkit.search_state import SearchState
  14. from prompt_toolkit.selection import SelectionType
  15. from prompt_toolkit.token import Token
  16. from prompt_toolkit.utils import get_cwidth
  17. from .lexers import Lexer, SimpleLexer
  18. from .processors import Processor
  19. from .screen import Char, Point
  20. from .utils import token_list_width, split_lines, token_list_to_text
  21. import six
  22. import time
  23. __all__ = (
  24. 'BufferControl',
  25. 'FillControl',
  26. 'TokenListControl',
  27. 'UIControl',
  28. 'UIContent',
  29. )
  30. class UIControl(with_metaclass(ABCMeta, object)):
  31. """
  32. Base class for all user interface controls.
  33. """
  34. def reset(self):
  35. # Default reset. (Doesn't have to be implemented.)
  36. pass
  37. def preferred_width(self, cli, max_available_width):
  38. return None
  39. def preferred_height(self, cli, width, max_available_height, wrap_lines):
  40. return None
  41. def has_focus(self, cli):
  42. """
  43. Return ``True`` when this user control has the focus.
  44. If so, the cursor will be displayed according to the cursor position
  45. reported by :meth:`.UIControl.create_content`. If the created content
  46. has the property ``show_cursor=False``, the cursor will be hidden from
  47. the output.
  48. """
  49. return False
  50. @abstractmethod
  51. def create_content(self, cli, width, height):
  52. """
  53. Generate the content for this user control.
  54. Returns a :class:`.UIContent` instance.
  55. """
  56. def mouse_handler(self, cli, mouse_event):
  57. """
  58. Handle mouse events.
  59. When `NotImplemented` is returned, it means that the given event is not
  60. handled by the `UIControl` itself. The `Window` or key bindings can
  61. decide to handle this event as scrolling or changing focus.
  62. :param cli: `CommandLineInterface` instance.
  63. :param mouse_event: `MouseEvent` instance.
  64. """
  65. return NotImplemented
  66. def move_cursor_down(self, cli):
  67. """
  68. Request to move the cursor down.
  69. This happens when scrolling down and the cursor is completely at the
  70. top.
  71. """
  72. def move_cursor_up(self, cli):
  73. """
  74. Request to move the cursor up.
  75. """
  76. class UIContent(object):
  77. """
  78. Content generated by a user control. This content consists of a list of
  79. lines.
  80. :param get_line: Callable that returns the current line. This is a list of
  81. (Token, text) tuples.
  82. :param line_count: The number of lines.
  83. :param cursor_position: a :class:`.Point` for the cursor position.
  84. :param menu_position: a :class:`.Point` for the menu position.
  85. :param show_cursor: Make the cursor visible.
  86. :param default_char: The default :class:`.Char` for filling the background.
  87. """
  88. def __init__(self, get_line=None, line_count=0,
  89. cursor_position=None, menu_position=None, show_cursor=True,
  90. default_char=None):
  91. assert callable(get_line)
  92. assert isinstance(line_count, six.integer_types)
  93. assert cursor_position is None or isinstance(cursor_position, Point)
  94. assert menu_position is None or isinstance(menu_position, Point)
  95. assert default_char is None or isinstance(default_char, Char)
  96. self.get_line = get_line
  97. self.line_count = line_count
  98. self.cursor_position = cursor_position or Point(0, 0)
  99. self.menu_position = menu_position
  100. self.show_cursor = show_cursor
  101. self.default_char = default_char
  102. # Cache for line heights. Maps (lineno, width) -> height.
  103. self._line_heights = {}
  104. def __getitem__(self, lineno):
  105. " Make it iterable (iterate line by line). "
  106. if lineno < self.line_count:
  107. return self.get_line(lineno)
  108. else:
  109. raise IndexError
  110. def get_height_for_line(self, lineno, width):
  111. """
  112. Return the height that a given line would need if it is rendered in a
  113. space with the given width.
  114. """
  115. try:
  116. return self._line_heights[lineno, width]
  117. except KeyError:
  118. text = token_list_to_text(self.get_line(lineno))
  119. result = self.get_height_for_text(text, width)
  120. # Cache and return
  121. self._line_heights[lineno, width] = result
  122. return result
  123. @staticmethod
  124. def get_height_for_text(text, width):
  125. # Get text width for this line.
  126. line_width = get_cwidth(text)
  127. # Calculate height.
  128. try:
  129. quotient, remainder = divmod(line_width, width)
  130. except ZeroDivisionError:
  131. # Return something very big.
  132. # (This can happen, when the Window gets very small.)
  133. return 10 ** 10
  134. else:
  135. if remainder:
  136. quotient += 1 # Like math.ceil.
  137. return max(1, quotient)
  138. class TokenListControl(UIControl):
  139. """
  140. Control that displays a list of (Token, text) tuples.
  141. (It's mostly optimized for rather small widgets, like toolbars, menus, etc...)
  142. Mouse support:
  143. The list of tokens can also contain tuples of three items, looking like:
  144. (Token, text, handler). When mouse support is enabled and the user
  145. clicks on this token, then the given handler is called. That handler
  146. should accept two inputs: (CommandLineInterface, MouseEvent) and it
  147. should either handle the event or return `NotImplemented` in case we
  148. want the containing Window to handle this event.
  149. :param get_tokens: Callable that takes a `CommandLineInterface` instance
  150. and returns the list of (Token, text) tuples to be displayed right now.
  151. :param default_char: default :class:`.Char` (character and Token) to use
  152. for the background when there is more space available than `get_tokens`
  153. returns.
  154. :param get_default_char: Like `default_char`, but this is a callable that
  155. takes a :class:`prompt_toolkit.interface.CommandLineInterface` and
  156. returns a :class:`.Char` instance.
  157. :param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`,
  158. this UI control will take the focus. The cursor will be shown in the
  159. upper left corner of this control, unless `get_token` returns a
  160. ``Token.SetCursorPosition`` token somewhere in the token list, then the
  161. cursor will be shown there.
  162. """
  163. def __init__(self, get_tokens, default_char=None, get_default_char=None,
  164. align_right=False, align_center=False, has_focus=False):
  165. assert callable(get_tokens)
  166. assert default_char is None or isinstance(default_char, Char)
  167. assert get_default_char is None or callable(get_default_char)
  168. assert not (default_char and get_default_char)
  169. self.align_right = to_cli_filter(align_right)
  170. self.align_center = to_cli_filter(align_center)
  171. self._has_focus_filter = to_cli_filter(has_focus)
  172. self.get_tokens = get_tokens
  173. # Construct `get_default_char` callable.
  174. if default_char:
  175. get_default_char = lambda _: default_char
  176. elif not get_default_char:
  177. get_default_char = lambda _: Char(' ', Token.Transparent)
  178. self.get_default_char = get_default_char
  179. #: Cache for the content.
  180. self._content_cache = SimpleCache(maxsize=18)
  181. self._token_cache = SimpleCache(maxsize=1)
  182. # Only cache one token list. We don't need the previous item.
  183. # Render info for the mouse support.
  184. self._tokens = None
  185. def reset(self):
  186. self._tokens = None
  187. def __repr__(self):
  188. return '%s(%r)' % (self.__class__.__name__, self.get_tokens)
  189. def _get_tokens_cached(self, cli):
  190. """
  191. Get tokens, but only retrieve tokens once during one render run.
  192. (This function is called several times during one rendering, because
  193. we also need those for calculating the dimensions.)
  194. """
  195. return self._token_cache.get(
  196. cli.render_counter, lambda: self.get_tokens(cli))
  197. def has_focus(self, cli):
  198. return self._has_focus_filter(cli)
  199. def preferred_width(self, cli, max_available_width):
  200. """
  201. Return the preferred width for this control.
  202. That is the width of the longest line.
  203. """
  204. text = token_list_to_text(self._get_tokens_cached(cli))
  205. line_lengths = [get_cwidth(l) for l in text.split('\n')]
  206. return max(line_lengths)
  207. def preferred_height(self, cli, width, max_available_height, wrap_lines):
  208. content = self.create_content(cli, width, None)
  209. return content.line_count
  210. def create_content(self, cli, width, height):
  211. # Get tokens
  212. tokens_with_mouse_handlers = self._get_tokens_cached(cli)
  213. default_char = self.get_default_char(cli)
  214. # Wrap/align right/center parameters.
  215. right = self.align_right(cli)
  216. center = self.align_center(cli)
  217. def process_line(line):
  218. " Center or right align a single line. "
  219. used_width = token_list_width(line)
  220. padding = width - used_width
  221. if center:
  222. padding = int(padding / 2)
  223. return [(default_char.token, default_char.char * padding)] + line
  224. if right or center:
  225. token_lines_with_mouse_handlers = []
  226. for line in split_lines(tokens_with_mouse_handlers):
  227. token_lines_with_mouse_handlers.append(process_line(line))
  228. else:
  229. token_lines_with_mouse_handlers = list(split_lines(tokens_with_mouse_handlers))
  230. # Strip mouse handlers from tokens.
  231. token_lines = [
  232. [tuple(item[:2]) for item in line]
  233. for line in token_lines_with_mouse_handlers
  234. ]
  235. # Keep track of the tokens with mouse handler, for later use in
  236. # `mouse_handler`.
  237. self._tokens = tokens_with_mouse_handlers
  238. # If there is a `Token.SetCursorPosition` in the token list, set the
  239. # cursor position here.
  240. def get_cursor_position():
  241. SetCursorPosition = Token.SetCursorPosition
  242. for y, line in enumerate(token_lines):
  243. x = 0
  244. for token, text in line:
  245. if token == SetCursorPosition:
  246. return Point(x=x, y=y)
  247. x += len(text)
  248. return None
  249. # Create content, or take it from the cache.
  250. key = (default_char.char, default_char.token,
  251. tuple(tokens_with_mouse_handlers), width, right, center)
  252. def get_content():
  253. return UIContent(get_line=lambda i: token_lines[i],
  254. line_count=len(token_lines),
  255. default_char=default_char,
  256. cursor_position=get_cursor_position())
  257. return self._content_cache.get(key, get_content)
  258. @classmethod
  259. def static(cls, tokens):
  260. def get_static_tokens(cli):
  261. return tokens
  262. return cls(get_static_tokens)
  263. def mouse_handler(self, cli, mouse_event):
  264. """
  265. Handle mouse events.
  266. (When the token list contained mouse handlers and the user clicked on
  267. on any of these, the matching handler is called. This handler can still
  268. return `NotImplemented` in case we want the `Window` to handle this
  269. particular event.)
  270. """
  271. if self._tokens:
  272. # Read the generator.
  273. tokens_for_line = list(split_lines(self._tokens))
  274. try:
  275. tokens = tokens_for_line[mouse_event.position.y]
  276. except IndexError:
  277. return NotImplemented
  278. else:
  279. # Find position in the token list.
  280. xpos = mouse_event.position.x
  281. # Find mouse handler for this character.
  282. count = 0
  283. for item in tokens:
  284. count += len(item[1])
  285. if count >= xpos:
  286. if len(item) >= 3:
  287. # Handler found. Call it.
  288. # (Handler can return NotImplemented, so return
  289. # that result.)
  290. handler = item[2]
  291. return handler(cli, mouse_event)
  292. else:
  293. break
  294. # Otherwise, don't handle here.
  295. return NotImplemented
  296. class FillControl(UIControl):
  297. """
  298. Fill whole control with characters with this token.
  299. (Also helpful for debugging.)
  300. :param char: :class:`.Char` instance to use for filling.
  301. :param get_char: A callable that takes a CommandLineInterface and returns a
  302. :class:`.Char` object.
  303. """
  304. def __init__(self, character=None, token=Token, char=None, get_char=None): # 'character' and 'token' parameters are deprecated.
  305. assert char is None or isinstance(char, Char)
  306. assert get_char is None or callable(get_char)
  307. assert not (char and get_char)
  308. self.char = char
  309. if character:
  310. # Passing (character=' ', token=token) is deprecated.
  311. self.character = character
  312. self.token = token
  313. self.get_char = lambda cli: Char(character, token)
  314. elif get_char:
  315. # When 'get_char' is given.
  316. self.get_char = get_char
  317. else:
  318. # When 'char' is given.
  319. self.char = self.char or Char()
  320. self.get_char = lambda cli: self.char
  321. self.char = char
  322. def __repr__(self):
  323. if self.char:
  324. return '%s(char=%r)' % (self.__class__.__name__, self.char)
  325. else:
  326. return '%s(get_char=%r)' % (self.__class__.__name__, self.get_char)
  327. def reset(self):
  328. pass
  329. def has_focus(self, cli):
  330. return False
  331. def create_content(self, cli, width, height):
  332. def get_line(i):
  333. return []
  334. return UIContent(
  335. get_line=get_line,
  336. line_count=100 ** 100, # Something very big.
  337. default_char=self.get_char(cli))
  338. _ProcessedLine = namedtuple('_ProcessedLine', 'tokens source_to_display display_to_source')
  339. class BufferControl(UIControl):
  340. """
  341. Control for visualising the content of a `Buffer`.
  342. :param input_processors: list of :class:`~prompt_toolkit.layout.processors.Processor`.
  343. :param lexer: :class:`~prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting.
  344. :param preview_search: `bool` or `CLIFilter`: Show search while typing.
  345. :param get_search_state: Callable that takes a CommandLineInterface and
  346. returns the SearchState to be used. (If not CommandLineInterface.search_state.)
  347. :param buffer_name: String representing the name of the buffer to display.
  348. :param default_char: :class:`.Char` instance to use to fill the background. This is
  349. transparent by default.
  350. :param focus_on_click: Focus this buffer when it's click, but not yet focussed.
  351. """
  352. def __init__(self,
  353. buffer_name=DEFAULT_BUFFER,
  354. input_processors=None,
  355. lexer=None,
  356. preview_search=False,
  357. search_buffer_name=SEARCH_BUFFER,
  358. get_search_state=None,
  359. menu_position=None,
  360. default_char=None,
  361. focus_on_click=False):
  362. assert input_processors is None or all(isinstance(i, Processor) for i in input_processors)
  363. assert menu_position is None or callable(menu_position)
  364. assert lexer is None or isinstance(lexer, Lexer)
  365. assert get_search_state is None or callable(get_search_state)
  366. assert default_char is None or isinstance(default_char, Char)
  367. self.preview_search = to_cli_filter(preview_search)
  368. self.get_search_state = get_search_state
  369. self.focus_on_click = to_cli_filter(focus_on_click)
  370. self.input_processors = input_processors or []
  371. self.buffer_name = buffer_name
  372. self.menu_position = menu_position
  373. self.lexer = lexer or SimpleLexer()
  374. self.default_char = default_char or Char(token=Token.Transparent)
  375. self.search_buffer_name = search_buffer_name
  376. #: Cache for the lexer.
  377. #: Often, due to cursor movement, undo/redo and window resizing
  378. #: operations, it happens that a short time, the same document has to be
  379. #: lexed. This is a faily easy way to cache such an expensive operation.
  380. self._token_cache = SimpleCache(maxsize=8)
  381. self._xy_to_cursor_position = None
  382. self._last_click_timestamp = None
  383. self._last_get_processed_line = None
  384. def _buffer(self, cli):
  385. """
  386. The buffer object that contains the 'main' content.
  387. """
  388. return cli.buffers[self.buffer_name]
  389. def has_focus(self, cli):
  390. # This control gets the focussed if the actual `Buffer` instance has the
  391. # focus or when any of the `InputProcessor` classes tells us that it
  392. # wants the focus. (E.g. in case of a reverse-search, where the actual
  393. # search buffer may not be displayed, but the "reverse-i-search" text
  394. # should get the focus.)
  395. return cli.current_buffer_name == self.buffer_name or \
  396. any(i.has_focus(cli) for i in self.input_processors)
  397. def preferred_width(self, cli, max_available_width):
  398. """
  399. This should return the preferred width.
  400. Note: We don't specify a preferred width according to the content,
  401. because it would be too expensive. Calculating the preferred
  402. width can be done by calculating the longest line, but this would
  403. require applying all the processors to each line. This is
  404. unfeasible for a larger document, and doing it for small
  405. documents only would result in inconsistent behaviour.
  406. """
  407. return None
  408. def preferred_height(self, cli, width, max_available_height, wrap_lines):
  409. # Calculate the content height, if it was drawn on a screen with the
  410. # given width.
  411. height = 0
  412. content = self.create_content(cli, width, None)
  413. # When line wrapping is off, the height should be equal to the amount
  414. # of lines.
  415. if not wrap_lines:
  416. return content.line_count
  417. # When the number of lines exceeds the max_available_height, just
  418. # return max_available_height. No need to calculate anything.
  419. if content.line_count >= max_available_height:
  420. return max_available_height
  421. for i in range(content.line_count):
  422. height += content.get_height_for_line(i, width)
  423. if height >= max_available_height:
  424. return max_available_height
  425. return height
  426. def _get_tokens_for_line_func(self, cli, document):
  427. """
  428. Create a function that returns the tokens for a given line.
  429. """
  430. # Cache using `document.text`.
  431. def get_tokens_for_line():
  432. return self.lexer.lex_document(cli, document)
  433. return self._token_cache.get(document.text, get_tokens_for_line)
  434. def _create_get_processed_line_func(self, cli, document):
  435. """
  436. Create a function that takes a line number of the current document and
  437. returns a _ProcessedLine(processed_tokens, source_to_display, display_to_source)
  438. tuple.
  439. """
  440. def transform(lineno, tokens):
  441. " Transform the tokens for a given line number. "
  442. source_to_display_functions = []
  443. display_to_source_functions = []
  444. # Get cursor position at this line.
  445. if document.cursor_position_row == lineno:
  446. cursor_column = document.cursor_position_col
  447. else:
  448. cursor_column = None
  449. def source_to_display(i):
  450. """ Translate x position from the buffer to the x position in the
  451. processed token list. """
  452. for f in source_to_display_functions:
  453. i = f(i)
  454. return i
  455. # Apply each processor.
  456. for p in self.input_processors:
  457. transformation = p.apply_transformation(
  458. cli, document, lineno, source_to_display, tokens)
  459. tokens = transformation.tokens
  460. if cursor_column:
  461. cursor_column = transformation.source_to_display(cursor_column)
  462. display_to_source_functions.append(transformation.display_to_source)
  463. source_to_display_functions.append(transformation.source_to_display)
  464. def display_to_source(i):
  465. for f in reversed(display_to_source_functions):
  466. i = f(i)
  467. return i
  468. return _ProcessedLine(tokens, source_to_display, display_to_source)
  469. def create_func():
  470. get_line = self._get_tokens_for_line_func(cli, document)
  471. cache = {}
  472. def get_processed_line(i):
  473. try:
  474. return cache[i]
  475. except KeyError:
  476. processed_line = transform(i, get_line(i))
  477. cache[i] = processed_line
  478. return processed_line
  479. return get_processed_line
  480. return create_func()
  481. def create_content(self, cli, width, height):
  482. """
  483. Create a UIContent.
  484. """
  485. buffer = self._buffer(cli)
  486. # Get the document to be shown. If we are currently searching (the
  487. # search buffer has focus, and the preview_search filter is enabled),
  488. # then use the search document, which has possibly a different
  489. # text/cursor position.)
  490. def preview_now():
  491. """ True when we should preview a search. """
  492. return bool(self.preview_search(cli) and
  493. cli.buffers[self.search_buffer_name].text)
  494. if preview_now():
  495. if self.get_search_state:
  496. ss = self.get_search_state(cli)
  497. else:
  498. ss = cli.search_state
  499. document = buffer.document_for_search(SearchState(
  500. text=cli.current_buffer.text,
  501. direction=ss.direction,
  502. ignore_case=ss.ignore_case))
  503. else:
  504. document = buffer.document
  505. get_processed_line = self._create_get_processed_line_func(cli, document)
  506. self._last_get_processed_line = get_processed_line
  507. def translate_rowcol(row, col):
  508. " Return the content column for this coordinate. "
  509. return Point(y=row, x=get_processed_line(row).source_to_display(col))
  510. def get_line(i):
  511. " Return the tokens for a given line number. "
  512. tokens = get_processed_line(i).tokens
  513. # Add a space at the end, because that is a possible cursor
  514. # position. (When inserting after the input.) We should do this on
  515. # all the lines, not just the line containing the cursor. (Because
  516. # otherwise, line wrapping/scrolling could change when moving the
  517. # cursor around.)
  518. tokens = tokens + [(self.default_char.token, ' ')]
  519. return tokens
  520. content = UIContent(
  521. get_line=get_line,
  522. line_count=document.line_count,
  523. cursor_position=translate_rowcol(document.cursor_position_row,
  524. document.cursor_position_col),
  525. default_char=self.default_char)
  526. # If there is an auto completion going on, use that start point for a
  527. # pop-up menu position. (But only when this buffer has the focus --
  528. # there is only one place for a menu, determined by the focussed buffer.)
  529. if cli.current_buffer_name == self.buffer_name:
  530. menu_position = self.menu_position(cli) if self.menu_position else None
  531. if menu_position is not None:
  532. assert isinstance(menu_position, int)
  533. menu_row, menu_col = buffer.document.translate_index_to_position(menu_position)
  534. content.menu_position = translate_rowcol(menu_row, menu_col)
  535. elif buffer.complete_state:
  536. # Position for completion menu.
  537. # Note: We use 'min', because the original cursor position could be
  538. # behind the input string when the actual completion is for
  539. # some reason shorter than the text we had before. (A completion
  540. # can change and shorten the input.)
  541. menu_row, menu_col = buffer.document.translate_index_to_position(
  542. min(buffer.cursor_position,
  543. buffer.complete_state.original_document.cursor_position))
  544. content.menu_position = translate_rowcol(menu_row, menu_col)
  545. else:
  546. content.menu_position = None
  547. return content
  548. def mouse_handler(self, cli, mouse_event):
  549. """
  550. Mouse handler for this control.
  551. """
  552. buffer = self._buffer(cli)
  553. position = mouse_event.position
  554. # Focus buffer when clicked.
  555. if self.has_focus(cli):
  556. if self._last_get_processed_line:
  557. processed_line = self._last_get_processed_line(position.y)
  558. # Translate coordinates back to the cursor position of the
  559. # original input.
  560. xpos = processed_line.display_to_source(position.x)
  561. index = buffer.document.translate_row_col_to_index(position.y, xpos)
  562. # Set the cursor position.
  563. if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
  564. buffer.exit_selection()
  565. buffer.cursor_position = index
  566. elif mouse_event.event_type == MouseEventType.MOUSE_UP:
  567. # When the cursor was moved to another place, select the text.
  568. # (The >1 is actually a small but acceptable workaround for
  569. # selecting text in Vi navigation mode. In navigation mode,
  570. # the cursor can never be after the text, so the cursor
  571. # will be repositioned automatically.)
  572. if abs(buffer.cursor_position - index) > 1:
  573. buffer.start_selection(selection_type=SelectionType.CHARACTERS)
  574. buffer.cursor_position = index
  575. # Select word around cursor on double click.
  576. # Two MOUSE_UP events in a short timespan are considered a double click.
  577. double_click = self._last_click_timestamp and time.time() - self._last_click_timestamp < .3
  578. self._last_click_timestamp = time.time()
  579. if double_click:
  580. start, end = buffer.document.find_boundaries_of_current_word()
  581. buffer.cursor_position += start
  582. buffer.start_selection(selection_type=SelectionType.CHARACTERS)
  583. buffer.cursor_position += end - start
  584. else:
  585. # Don't handle scroll events here.
  586. return NotImplemented
  587. # Not focussed, but focussing on click events.
  588. else:
  589. if self.focus_on_click(cli) and mouse_event.event_type == MouseEventType.MOUSE_UP:
  590. # Focus happens on mouseup. (If we did this on mousedown, the
  591. # up event will be received at the point where this widget is
  592. # focussed and be handled anyway.)
  593. cli.focus(self.buffer_name)
  594. else:
  595. return NotImplemented
  596. def move_cursor_down(self, cli):
  597. b = self._buffer(cli)
  598. b.cursor_position += b.document.get_cursor_down_position()
  599. def move_cursor_up(self, cli):
  600. b = self._buffer(cli)
  601. b.cursor_position += b.document.get_cursor_up_position()