コード例 #1
0
class TokenListControl(UIControl):
    """
    Control that displays a list of (Token, text) tuples.
    (It's mostly optimized for rather small widgets, like toolbars, menus, etc...)

    Mouse support:

        The list of tokens can also contain tuples of three items, looking like:
        (Token, text, handler). When mouse support is enabled and the user
        clicks on this token, then the given handler is called. That handler
        should accept two inputs: (CommandLineInterface, MouseEvent) and it
        should either handle the event or return `NotImplemented` in case we
        want the containing Window to handle this event.

    :param get_tokens: Callable that takes a `CommandLineInterface` instance
        and returns the list of (Token, text) tuples to be displayed right now.
    :param default_char: default :class:`.Char` (character and Token) to use
        for the background when there is more space available than `get_tokens`
        returns.
    :param get_default_char: Like `default_char`, but this is a callable that
        takes a :class:`prompt_toolkit.interface.CommandLineInterface` and
        returns a :class:`.Char` instance.
    :param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`,
        this UI control will take the focus. The cursor will be shown in the
        upper left corner of this control, unless `get_token` returns a
        ``Token.SetCursorPosition`` token somewhere in the token list, then the
        cursor will be shown there.
    :param wrap_lines: `bool` or `CLIFilter`: Wrap long lines.
    """
    def __init__(self,
                 get_tokens,
                 default_char=None,
                 get_default_char=None,
                 align_right=False,
                 align_center=False,
                 has_focus=False,
                 wrap_lines=True):
        assert default_char is None or isinstance(default_char, Char)
        assert get_default_char is None or callable(get_default_char)
        assert not (default_char and get_default_char)

        self.align_right = to_cli_filter(align_right)
        self.align_center = to_cli_filter(align_center)
        self._has_focus_filter = to_cli_filter(has_focus)
        self.wrap_lines = to_cli_filter(wrap_lines)

        self.get_tokens = get_tokens

        # Construct `get_default_char` callable.
        if default_char:
            get_default_char = lambda _: default_char
        elif not get_default_char:
            get_default_char = lambda _: Char(' ', Token)

        self.get_default_char = get_default_char

        #: Cache for rendered screens.
        self._screen_lru_cache = SimpleLRUCache(maxsize=18)
        self._token_lru_cache = SimpleLRUCache(maxsize=1)
        # Only cache one token list. We don't need the previous item.

        # Render info for the mouse support.
        self._tokens = None  # The last rendered tokens.
        self._pos_to_indexes = None  # Mapping from mouse positions (x,y) to
        # positions in the token list.

    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.get_tokens)

    def _get_tokens_cached(self, cli):
        """
        Get tokens, but only retrieve tokens once during one render run.
        (This function is called several times during one rendering, because
        we also need those for calculating the dimensions.)
        """
        return self._token_lru_cache.get(cli.render_counter,
                                         lambda: self.get_tokens(cli))

    def has_focus(self, cli):
        return self._has_focus_filter(cli)

    def preferred_width(self, cli, max_available_width):
        """
        Return the preferred width for this control.
        That is the width of the longest line.
        """
        text = ''.join(t[1] for t in self._get_tokens_cached(cli))
        line_lengths = [get_cwidth(l) for l in text.split('\n')]
        return max(line_lengths)

    def preferred_height(self, cli, width):
        screen = self.create_screen(cli, width, None)
        return screen.height

    def create_screen(self, cli, width, height):
        # Get tokens
        tokens_with_mouse_handlers = self._get_tokens_cached(cli)

        default_char = self.get_default_char(cli)

        # Strip mouse handlers from tokens.
        tokens = [tuple(item[:2]) for item in tokens_with_mouse_handlers]

        # Wrap/align right/center parameters.
        wrap_lines = self.wrap_lines(cli)
        right = self.align_right(cli)
        center = self.align_center(cli)

        # Create screen, or take it from the cache.
        key = (default_char, tokens_with_mouse_handlers, width, wrap_lines,
               right, center)
        params = (default_char, tokens, width, wrap_lines, right, center)
        screen, self._pos_to_indexes = self._screen_lru_cache.get(
            key, lambda: self._get_screen(*params))

        self._tokens = tokens_with_mouse_handlers
        return screen

    @classmethod
    def _get_screen(cls, default_char, tokens, width, wrap_lines, right,
                    center):
        screen = Screen(default_char, initial_width=width)

        # Only call write_data when we actually have tokens.
        # (Otherwise the screen height will go up from 0 to 1 while we don't
        # want that. -- An empty control should not take up any space.)
        if tokens:

            def process_line(line):
                " Center or right align a single line. "
                used_width = token_list_width(line)
                padding = width - used_width
                if center:
                    padding = int(padding / 2)
                return [(default_char.token, default_char.char * padding)
                        ] + line + [(Token, '\n')]

            if right or center:
                tokens2 = []
                for line in split_lines(tokens):
                    tokens2.extend(process_line(line))
                tokens = tokens2

            indexes_to_pos = screen.write_data(
                tokens, width=(width if wrap_lines else None))
            pos_to_indexes = dict((v, k) for k, v in indexes_to_pos.items())
        else:
            pos_to_indexes = {}

        return screen, pos_to_indexes

    @classmethod
    def static(cls, tokens):
        def get_static_tokens(cli):
            return tokens

        return cls(get_static_tokens)

    def mouse_handler(self, cli, mouse_event):
        """
        Handle mouse events.

        (When the token list contained mouse handlers and the user clicked on
        on any of these, the matching handler is called. This handler can still
        return `NotImplemented` in case we want the `Window` to handle this
        particular event.)
        """
        if self._pos_to_indexes:
            # Find position in the token list.
            position = mouse_event.position
            index = self._pos_to_indexes.get((position.x, position.y))

            if index is not None:
                # Find mouse handler for this character.
                count = 0
                for item in self._tokens:
                    count += len(item[1])
                    if count >= index:
                        if len(item) >= 3:
                            # Handler found. Call it.
                            handler = item[2]
                            handler(cli, mouse_event)
                            return
                        else:
                            break

        # Otherwise, don't handle here.
        return NotImplemented
コード例 #2
0
class BufferControl(UIControl):
    """
    Control for visualising the content of a `Buffer`.

    :param input_processors: list of :class:`~prompt_toolkit.layout.processors.Processor`.
    :param lexer: :class:`~prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting.
    :param preview_search: `bool` or `CLIFilter`: Show search while typing.
    :param wrap_lines: `bool` or `CLIFilter`: Wrap long lines.
    :param buffer_name: String representing the name of the buffer to display.
    :param default_char: :class:`.Char` instance to use to fill the background. This is
        transparent by default.
    """
    def __init__(self,
                 buffer_name=DEFAULT_BUFFER,
                 input_processors=None,
                 lexer=None,
                 preview_search=False,
                 wrap_lines=True,
                 menu_position=None,
                 default_char=None):
        assert input_processors is None or all(
            isinstance(i, Processor) for i in input_processors)
        assert menu_position is None or callable(menu_position)
        assert lexer is None or isinstance(lexer, Lexer)

        self.preview_search = to_cli_filter(preview_search)
        self.wrap_lines = to_cli_filter(wrap_lines)

        self.input_processors = input_processors or []
        self.buffer_name = buffer_name
        self.menu_position = menu_position
        self.lexer = lexer or SimpleLexer()
        self.default_char = default_char or Char(token=Token.Transparent)

        #: LRU cache for the lexer.
        #: Often, due to cursor movement, undo/redo and window resizing
        #: operations, it happens that a short time, the same document has to be
        #: lexed. This is a faily easy way to cache such an expensive operation.
        self._token_lru_cache = SimpleLRUCache(maxsize=8)

        #: Keep a similar cache for rendered screens. (when we scroll up/down
        #: through the screen, or when we change another buffer, we don't want
        #: to recreate the same screen again.)
        self._screen_lru_cache = SimpleLRUCache(maxsize=8)

        self._xy_to_cursor_position = None
        self._last_click_timestamp = None

    def _buffer(self, cli):
        """
        The buffer object that contains the 'main' content.
        """
        return cli.buffers[self.buffer_name]

    def has_focus(self, cli):
        # This control gets the focussed if the actual `Buffer` instance has the
        # focus or when any of the `InputProcessor` classes tells us that it
        # wants the focus. (E.g. in case of a reverse-search, where the actual
        # search buffer may not be displayed, but the "reverse-i-search" text
        # should get the focus.)
        return cli.focus_stack.current == self.buffer_name or \
            any(i.has_focus(cli) for i in self.input_processors)

    def preferred_width(self, cli, max_available_width):
        # Return the length of the longest line.
        return max(map(len, self._buffer(cli).document.lines))

    def preferred_height(self, cli, width):
        # Draw content on a screen using this width. Measure the height of the
        # result.
        screen = self.create_screen(cli, width, None)
        return screen.height

    def _get_input_tokens(self, cli, document):
        """
        Tokenize input text for highlighting.
        Return (tokens, source_to_display, display_to_source) tuple.

        :param document: The document to be shown. This can be `buffer.document`
                         but could as well be a different one, in case we are
                         searching through the history. (Buffer.document_for_search)
        """
        def get():
            # Call lexer.
            tokens = list(self.lexer.get_tokens(cli, document.text))

            # 'Explode' tokens in characters.
            # (Some input processors -- like search/selection highlighter --
            # rely on that each item in the tokens array only contains one
            # character.)
            tokens = [(token, c) for token, text in tokens for c in text]

            # Run all processors over the input.
            # (They can transform both the tokens and the cursor position.)
            source_to_display_functions = []
            display_to_source_functions = []

            d_ = document  # Each processor receives the document of the previous one.

            for p in self.input_processors:
                transformation = p.apply_transformation(cli, d_, tokens)
                d_ = transformation.document
                assert isinstance(transformation, Transformation)

                tokens = transformation.tokens
                source_to_display_functions.append(
                    transformation.source_to_display)
                display_to_source_functions.append(
                    transformation.display_to_source)

            # Chain cursor transformation (movement) functions.

            def source_to_display(cursor_position):
                " Chained source_to_display. "
                for f in source_to_display_functions:
                    cursor_position = f(cursor_position)
                return cursor_position

            def display_to_source(cursor_position):
                " Chained display_to_source. "
                for f in reversed(display_to_source_functions):
                    cursor_position = f(cursor_position)
                return cursor_position

            return tokens, source_to_display, display_to_source

        key = (
            document.text,

            # Include invalidation_hashes from all processors.
            tuple(
                p.invalidation_hash(cli, document)
                for p in self.input_processors),
        )

        return self._token_lru_cache.get(key, get)

    def create_screen(self, cli, width, height):
        buffer = self._buffer(cli)

        # Get the document to be shown. If we are currently searching (the
        # search buffer has focus, and the preview_search filter is enabled),
        # then use the search document, which has possibly a different
        # text/cursor position.)
        def preview_now():
            """ True when we should preview a search. """
            return bool(
                self.preview_search(cli) and cli.is_searching
                and cli.current_buffer.text)

        if preview_now():
            document = buffer.document_for_search(
                SearchState(text=cli.current_buffer.text,
                            direction=cli.search_state.direction,
                            ignore_case=cli.search_state.ignore_case))
        else:
            document = buffer.document

        # Wrap.
        wrap_width = width if self.wrap_lines(cli) else None

        def _create_screen():
            screen = Screen(self.default_char, initial_width=width)

            # Get tokens
            # Note: we add the space character at the end, because that's where
            #       the cursor can also be.
            input_tokens, source_to_display, display_to_source = self._get_input_tokens(
                cli, document)
            input_tokens += [(self.default_char.token, ' ')]

            indexes_to_pos = screen.write_data(input_tokens, width=wrap_width)
            pos_to_indexes = dict((v, k) for k, v in indexes_to_pos.items())

            def cursor_position_to_xy(cursor_position):
                """ Turn a cursor position in the buffer into x/y coordinates
                on the screen. """
                # First get the real token position by applying all transformations.
                cursor_position = source_to_display(cursor_position)

                # Then look up into the table.
                try:
                    return indexes_to_pos[cursor_position]
                except KeyError:
                    # This can fail with KeyError, but only if one of the
                    # processors is returning invalid key locations.
                    raise
                    # return 0, 0

            def xy_to_cursor_position(x, y):
                """ Turn x/y screen coordinates back to the original cursor
                position in the buffer. """
                # Look up reverse in table.
                while x > 0 or y > 0:
                    try:
                        index = pos_to_indexes[x, y]
                        break
                    except KeyError:
                        # No match found -> mouse click outside of region
                        # containing text. Look to the left or up.
                        if x: x -= 1
                        elif y: y -= 1
                else:
                    # Nobreak.
                    index = 0

                # Transform.
                return display_to_source(index)

            return screen, cursor_position_to_xy, xy_to_cursor_position

        # Build a key for the caching. If any of these parameters changes, we
        # have to recreate a new screen.
        key = (
            # When the text changes, we obviously have to recreate a new screen.
            document.text,

            # When the width changes, line wrapping will be different.
            # (None when disabled.)
            wrap_width,

            # Include invalidation_hashes from all processors.
            tuple(
                p.invalidation_hash(cli, document)
                for p in self.input_processors),
        )

        # Get from cache, or create if this doesn't exist yet.
        screen, cursor_position_to_xy, self._xy_to_cursor_position = self._screen_lru_cache.get(
            key, _create_screen)

        x, y = cursor_position_to_xy(document.cursor_position)
        screen.cursor_position = Point(y=y, x=x)

        # If there is an auto completion going on, use that start point for a
        # pop-up menu position. (But only when this buffer has the focus --
        # there is only one place for a menu, determined by the focussed buffer.)
        if cli.current_buffer_name == self.buffer_name:
            menu_position = self.menu_position(
                cli) if self.menu_position else None
            if menu_position is not None:
                assert isinstance(menu_position, int)
                x, y = cursor_position_to_xy(menu_position)
                screen.menu_position = Point(y=y, x=x)
            elif buffer.complete_state:
                # Position for completion menu.
                # Note: We use 'min', because the original cursor position could be
                #       behind the input string when the actual completion is for
                #       some reason shorter than the text we had before. (A completion
                #       can change and shorten the input.)
                x, y = cursor_position_to_xy(
                    min(
                        buffer.cursor_position, buffer.complete_state.
                        original_document.cursor_position))
                screen.menu_position = Point(y=y, x=x)
            else:
                screen.menu_position = None

        return screen

    def mouse_handler(self, cli, mouse_event):
        """
        Mouse handler for this control.
        """
        buffer = self._buffer(cli)
        position = mouse_event.position

        if self._xy_to_cursor_position:
            # Translate coordinates back to the cursor position of the
            # original input.
            pos = self._xy_to_cursor_position(position.x, position.y)

            # Set the cursor position.
            if pos <= len(buffer.text):
                if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN:
                    buffer.exit_selection()
                    buffer.cursor_position = pos

                elif mouse_event.event_type == MouseEventTypes.MOUSE_UP:
                    # When the cursor was moved to another place, select the text.
                    # (The >1 is actually a small but acceptable workaround for
                    # selecting text in Vi navigation mode. In navigation mode,
                    # the cursor can never be after the text, so the cursor
                    # will be repositioned automatically.)
                    if abs(buffer.cursor_position - pos) > 1:
                        buffer.start_selection(
                            selection_type=SelectionType.CHARACTERS)
                        buffer.cursor_position = pos

                    # Select word around cursor on double click.
                    # Two MOUSE_UP events in a short timespan are considered a double click.
                    double_click = self._last_click_timestamp and time.time(
                    ) - self._last_click_timestamp < .3
                    self._last_click_timestamp = time.time()

                    if double_click:
                        start, end = buffer.document.find_boundaries_of_current_word(
                        )
                        buffer.cursor_position += start
                        buffer.start_selection(
                            selection_type=SelectionType.CHARACTERS)
                        buffer.cursor_position += end - start
                else:
                    # Don't handle scroll events here.
                    return NotImplemented

    def move_cursor_down(self, cli):
        b = self._buffer(cli)
        b.cursor_position += b.document.get_cursor_down_position()

    def move_cursor_up(self, cli):
        b = self._buffer(cli)
        b.cursor_position += b.document.get_cursor_up_position()
コード例 #3
0
class Window(Container):
    """
    Container that holds a control.

    :param content: :class:`~prompt_toolkit.layout.controls.UIControl` instance.
    :param width: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance.
    :param height: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance.
    :param get_width: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`.
    :param get_height: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`.
    :param dont_extend_width: When `True`, don't take up more width then the
                              preferred width reported by the control.
    :param dont_extend_height: When `True`, don't take up more width then the
                               preferred height reported by the control.
    :param left_margins: A list of :class:`~prompt_toolkit.layout.margins.Margin`
        instance to be displayed on the left. For instance:
        :class:`~prompt_toolkit.layout.margins.NumberredMargin` can be one of
        them in order to show line numbers.
    :param right_margins: Like `left_margins`, but on the other side.
    :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the
        preferred amount of lines/columns to be always visible before/after the
        cursor. When both top and bottom are a very high number, the cursor
        will be centered vertically most of the time.
    :param allow_scroll_beyond_bottom: A `bool` or
        :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, allow
        scrolling so far, that the top part of the content is not visible
        anymore, while there is still empty space available at the bottom of
        the window. In the Vi editor for instance, this is possible. You will
        see tildes while the top part of the body is hidden.
    :param get_vertical_scroll: Callable that takes this window
        instance as input and returns a preferred vertical scroll.
        (When this is `None`, the scroll is only determined by the last and
        current cursor position.)
    :param get_horizontal_scroll: Callable that takes this window
        instance as input and returns a preferred vertical scroll.
    :param always_hide_cursor: A `bool` or
        :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, never
        display the cursor, even when the user control specifies a cursor
        position.
    """
    def __init__(self, content, width=None, height=None, get_width=None,
                 get_height=None, dont_extend_width=False, dont_extend_height=False,
                 left_margins=None, right_margins=None, scroll_offsets=None,
                 allow_scroll_beyond_bottom=False,
                 get_vertical_scroll=None, get_horizontal_scroll=None, always_hide_cursor=False):
        assert isinstance(content, UIControl)
        assert width is None or isinstance(width, LayoutDimension)
        assert height is None or isinstance(height, LayoutDimension)
        assert get_width is None or callable(get_width)
        assert get_height is None or callable(get_height)
        assert width is None or get_width is None
        assert height is None or get_height is None
        assert scroll_offsets is None or isinstance(scroll_offsets, ScrollOffsets)
        assert left_margins is None or all(isinstance(m, Margin) for m in left_margins)
        assert right_margins is None or all(isinstance(m, Margin) for m in right_margins)
        assert get_vertical_scroll is None or callable(get_vertical_scroll)
        assert get_horizontal_scroll is None or callable(get_horizontal_scroll)

        self.allow_scroll_beyond_bottom = to_cli_filter(allow_scroll_beyond_bottom)
        self.always_hide_cursor = to_cli_filter(always_hide_cursor)

        self.content = content
        self.dont_extend_width = dont_extend_width
        self.dont_extend_height = dont_extend_height
        self.left_margins = left_margins or []
        self.right_margins = right_margins or []
        self.scroll_offsets = scroll_offsets or ScrollOffsets()
        self.get_vertical_scroll = get_vertical_scroll
        self.get_horizontal_scroll = get_horizontal_scroll
        self._width = get_width or (lambda cli: width)
        self._height = get_height or (lambda cli: height)

        # Cache for the screens generated by the margin.
        self._margin_cache = SimpleLRUCache(maxsize=8)

        self.reset()

    def __repr__(self):
        return 'Window(content=%r)' % self.content

    def reset(self):
        self.content.reset()

        #: Scrolling position of the main content.
        self.vertical_scroll = 0
        self.horizontal_scroll = 0

        #: Keep render information (mappings between buffer input and render
        #: output.)
        self.render_info = None

    def preferred_width(self, cli, max_available_width):
        # Width of the margins.
        total_margin_width = sum(m.get_width(cli) for m in
                                 self.left_margins + self.right_margins)

        # Window of the content.
        preferred_width = self.content.preferred_width(
                cli, max_available_width - total_margin_width)

        if preferred_width is not None:
            preferred_width += total_margin_width

        # Merge.
        return self._merge_dimensions(
            dimension=self._width(cli),
            preferred=preferred_width,
            dont_extend=self.dont_extend_width)

    def preferred_height(self, cli, width):
        return self._merge_dimensions(
            dimension=self._height(cli),
            preferred=self.content.preferred_height(cli, width),
            dont_extend=self.dont_extend_height)

    @staticmethod
    def _merge_dimensions(dimension, preferred=None, dont_extend=False):
        """
        Take the LayoutDimension from this `Window` class and the received
        preferred size from the `UIControl` and return a `LayoutDimension` to
        report to the parent container.
        """
        dimension = dimension or LayoutDimension()

        # When a preferred dimension was explicitely given to the Window,
        # ignore the UIControl.
        if dimension.preferred_specified:
            preferred = dimension.preferred

        # When a 'preferred' dimension is given by the UIControl, make sure
        # that it stays within the bounds of the Window.
        if preferred is not None:
            if dimension.max:
                preferred = min(preferred, dimension.max)

            if dimension.min:
                preferred = max(preferred, dimension.min)

        # When a `dont_extend` flag has been given, use the preferred dimension
        # also as the max demension.
        if dont_extend and preferred is not None:
            max_ = min(dimension.max, preferred)
        else:
            max_ = dimension.max

        return LayoutDimension(min=dimension.min, max=max_, preferred=preferred)

    def write_to_screen(self, cli, screen, mouse_handlers, write_position):
        """
        Write window to screen. This renders the user control, the margins and
        copies everything over to the absolute position at the given screen.
        """
        # Render margins.
        left_margin_widths = [m.get_width(cli) for m in self.left_margins]
        right_margin_widths = [m.get_width(cli) for m in self.right_margins]
        total_margin_width = sum(left_margin_widths + right_margin_widths)

        # Render UserControl.
        temp_screen = self.content.create_screen(
            cli, write_position.width - total_margin_width, write_position.height)

        # Scroll content.
        applied_scroll_offsets = self._scroll(
            temp_screen, write_position.width - total_margin_width, write_position.height, cli)

        # Write body to screen.
        self._copy_body(cli, temp_screen, screen, write_position,
                        sum(left_margin_widths), write_position.width - total_margin_width,
                        applied_scroll_offsets)

        # Remember render info. (Set before generating the margins. They need this.)
        self.render_info = WindowRenderInfo(
            original_screen=temp_screen,
            horizontal_scroll=self.horizontal_scroll,
            vertical_scroll=self.vertical_scroll,
            window_width=write_position.width,
            window_height=write_position.height,
            cursor_position=Point(y=temp_screen.cursor_position.y - self.vertical_scroll,
                                  x=temp_screen.cursor_position.x - self.horizontal_scroll),
            configured_scroll_offsets=self.scroll_offsets,
            applied_scroll_offsets=applied_scroll_offsets)

        # Set mouse handlers.
        def mouse_handler(cli, mouse_event):
            """ Wrapper around the mouse_handler of the `UIControl` that turns
            absolute coordinates into relative coordinates. """
            position = mouse_event.position

            # Call the mouse handler of the UIControl first.
            result = self.content.mouse_handler(
                cli, MouseEvent(
                    position=Point(x=position.x - write_position.xpos - sum(left_margin_widths),
                                   y=position.y - write_position.ypos + self.vertical_scroll),
                    event_type=mouse_event.event_type))

            # If it returns NotImplemented, handle it here.
            if result == NotImplemented:
                return self._mouse_handler(cli, mouse_event)

            return result

        mouse_handlers.set_mouse_handler_for_range(
                x_min=write_position.xpos + sum(left_margin_widths),
                x_max=write_position.xpos + write_position.width - total_margin_width,
                y_min=write_position.ypos,
                y_max=write_position.ypos + write_position.height,
                handler=mouse_handler)

        # Render and copy margins.
        move_x = 0

        def render_margin(m, width):
            " Render margin. return `Screen`. "
            # Retrieve margin tokens.
            tokens = m.create_margin(cli, self.render_info, width, write_position.height)

            # Turn it into a screen. (Take a screen from the cache if we
            # already rendered those tokens using this size.)
            def create_screen():
                return TokenListControl.static(tokens).create_screen(
                    cli, width + 1, write_position.height)

            key = (tokens, width, write_position.height)
            return self._margin_cache.get(key, create_screen)

        for m, width in zip(self.left_margins, left_margin_widths):
            # Create screen for margin.
            margin_screen = render_margin(m, width)

            # Copy and shift X.
            self._copy_margin(margin_screen, screen, write_position, move_x, width)
            move_x += width

        move_x = write_position.width - sum(right_margin_widths)

        for m, width in zip(self.right_margins, right_margin_widths):
            # Create screen for margin.
            margin_screen = render_margin(m, width)

            # Copy and shift X.
            self._copy_margin(margin_screen, screen, write_position, move_x, width)
            move_x += width

    def _copy_body(self, cli, temp_screen, new_screen, write_position, move_x, width, applied_scroll_offsets):
        """
        Copy characters from the temp screen that we got from the `UIControl`
        to the real screen.
        """
        xpos = write_position.xpos + move_x
        ypos = write_position.ypos
        height = write_position.height

        temp_buffer = temp_screen.data_buffer
        new_buffer = new_screen.data_buffer
        temp_screen_height = temp_screen.height
        y = 0

        # Now copy the region we need to the real screen.
        for y in range(0, height):
            # We keep local row variables. (Don't look up the row in the dict
            # for each iteration of the nested loop.)
            new_row = new_buffer[y + ypos]

            if y >= temp_screen_height and y >= write_position.height:
                # Break out of for loop when we pass after the last row of the
                # temp screen. (We use the 'y' position for calculation of new
                # screen's height.)
                break
            else:
                temp_row = temp_buffer[y + self.vertical_scroll]

                # Copy row content, except for transparent tokens.
                # (This is useful in case of floats.)
                for x in range(0, width):
                    cell = temp_row[x + self.horizontal_scroll]
                    if cell.token != Transparent:
                        new_row[x + xpos] = cell

        if self.content.has_focus(cli):
            new_screen.cursor_position = Point(y=temp_screen.cursor_position.y + ypos - self.vertical_scroll,
                                               x=temp_screen.cursor_position.x + xpos - self.horizontal_scroll)

            if not self.always_hide_cursor(cli):
                new_screen.show_cursor = temp_screen.show_cursor

        if not new_screen.menu_position and temp_screen.menu_position:
            new_screen.menu_position = Point(y=temp_screen.menu_position.y + ypos - self.vertical_scroll,
                                             x=temp_screen.menu_position.x + xpos - self.horizontal_scroll)

        # Update height of the output screen. (new_screen.write_data is not
        # called, so the screen is not aware of its height.)
        new_screen.height = max(new_screen.height, ypos + y + 1)

    def _copy_margin(self, temp_screen, new_screen, write_position, move_x, width):
        """
        Copy characters from the margin screen to the real screen.
        """
        xpos = write_position.xpos + move_x
        ypos = write_position.ypos

        temp_buffer = temp_screen.data_buffer
        new_buffer = new_screen.data_buffer

        # Now copy the region we need to the real screen.
        for y in range(0, write_position.height):
            new_row = new_buffer[y + ypos]
            temp_row = temp_buffer[y]

            # Copy row content, except for transparent tokens.
            # (This is useful in case of floats.)
            for x in range(0, width):
                cell = temp_row[x]
                if cell.token != Transparent:
                    new_row[x + xpos] = cell

    def _scroll(self, temp_screen, width, height, cli):
        """
        Scroll to make sure the cursor position is visible and that we maintain the
        requested scroll offset.
        Return the applied scroll offsets.
        """
        def do_scroll(current_scroll, scroll_offset_start, scroll_offset_end, cursor_pos, window_size, content_size):
            " Scrolling algorithm. Used for both horizontal and vertical scrolling. "
            # Calculate the scroll offset to apply.
            # This can obviously never be more than have the screen size. Also, when the
            # cursor appears at the top or bottom, we don't apply the offset.
            scroll_offset_start = int(min(scroll_offset_start, window_size / 2, cursor_pos))
            scroll_offset_end = int(min(scroll_offset_end, window_size / 2,
                                           content_size - 1 - cursor_pos))

            # Prevent negative scroll offsets.
            if current_scroll < 0:
                current_scroll = 0

            # Scroll back if we scrolled to much and there's still space to show more of the document.
            if (not self.allow_scroll_beyond_bottom(cli) and
                    current_scroll > content_size - window_size):
                current_scroll = max(0, content_size - window_size)

            # Scroll up if cursor is before visible part.
            if current_scroll > cursor_pos - scroll_offset_start:
                current_scroll = max(0, cursor_pos - scroll_offset_start)

            # Scroll down if cursor is after visible part.
            if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end:
                current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end

            # Calculate the applied scroll offset. This value can be lower than what we had.
            scroll_offset_start = max(0, min(current_scroll, scroll_offset_start))
            scroll_offset_end = max(0, min(content_size - current_scroll - window_size, scroll_offset_end))

            return current_scroll, scroll_offset_start, scroll_offset_end

        # When a preferred scroll is given, take that first into account.
        if self.get_vertical_scroll:
            self.vertical_scroll = self.get_vertical_scroll(self)
            assert isinstance(self.vertical_scroll, int)
        if self.get_horizontal_scroll:
            self.horizontal_scroll = self.get_horizontal_scroll(self)
            assert isinstance(self.horizontal_scroll, int)

        # Update horizontal/vertical scroll to make sure that the cursor
        # remains visible.
        offsets = self.scroll_offsets

        self.vertical_scroll, scroll_offset_top, scroll_offset_bottom  = do_scroll(
            current_scroll=self.vertical_scroll,
            scroll_offset_start=offsets.top,
            scroll_offset_end=offsets.bottom,
            cursor_pos=temp_screen.cursor_position.y,
            window_size=height,
            content_size=temp_screen.height)

        self.horizontal_scroll, scroll_offset_left, scroll_offset_right = do_scroll(
            current_scroll=self.horizontal_scroll,
            scroll_offset_start=offsets.left,
            scroll_offset_end=offsets.right,
            cursor_pos=temp_screen.cursor_position.x,
            window_size=width,
            content_size=temp_screen.width)

        applied_scroll_offsets = ScrollOffsets(
            top=scroll_offset_top,
            bottom=scroll_offset_bottom,
            left=scroll_offset_left,
            right=scroll_offset_right)

        return applied_scroll_offsets

    def _mouse_handler(self, cli, mouse_event):
        """
        Mouse handler. Called when the UI control doesn't handle this
        particular event.
        """
        if mouse_event.event_type == MouseEventTypes.SCROLL_DOWN:
            self._scroll_down(cli)
        elif mouse_event.event_type == MouseEventTypes.SCROLL_UP:
            self._scroll_up(cli)

    def _scroll_down(self, cli):
        " Scroll window down. "
        info = self.render_info

        if self.vertical_scroll < info.content_height - info.window_height:
            if info.cursor_position.y <= info.configured_scroll_offsets.top:
                self.content.move_cursor_down(cli)

            self.vertical_scroll += 1

    def _scroll_up(self, cli):
        " Scroll window up. "
        info = self.render_info

        if info.vertical_scroll > 0:
            if info.cursor_position.y >= info.window_height - 1 - info.configured_scroll_offsets.bottom:
                self.content.move_cursor_up(cli)

            self.vertical_scroll -= 1

    def walk(self):
        # Only yield self. A window doesn't have children.
        yield self
コード例 #4
0
class TokenListControl(UIControl):
    """
    Control that displays a list of (Token, text) tuples.
    (It's mostly optimized for rather small widgets, like toolbars, menus, etc...)

    Mouse support:

        The list of tokens can also contain tuples of three items, looking like:
        (Token, text, handler). When mouse support is enabled and the user
        clicks on this token, then the given handler is called. That handler
        should accept two inputs: (CommandLineInterface, MouseEvent) and it
        should either handle the event or return `NotImplemented` in case we
        want the containing Window to handle this event.

    :param get_tokens: Callable that takes a `CommandLineInterface` instance
        and returns the list of (Token, text) tuples to be displayed right now.
    :param default_char: default :class:`.Char` (character and Token) to use
        for the background when there is more space available than `get_tokens`
        returns.
    :param get_default_char: Like `default_char`, but this is a callable that
        takes a :class:`prompt_toolkit.interface.CommandLineInterface` and
        returns a :class:`.Char` instance.
    :param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`,
        this UI control will take the focus. The cursor will be shown in the
        upper left corner of this control, unless `get_token` returns a
        ``Token.SetCursorPosition`` token somewhere in the token list, then the
        cursor will be shown there.
    :param wrap_lines: `bool` or `CLIFilter`: Wrap long lines.
    """
    def __init__(self, get_tokens, default_char=None, get_default_char=None,
                 align_right=False, align_center=False,
                 has_focus=False, wrap_lines=True):
        assert default_char is None or isinstance(default_char, Char)
        assert get_default_char is None or callable(get_default_char)
        assert not (default_char and get_default_char)

        self.align_right = to_cli_filter(align_right)
        self.align_center = to_cli_filter(align_center)
        self._has_focus_filter = to_cli_filter(has_focus)
        self.wrap_lines = to_cli_filter(wrap_lines)

        self.get_tokens = get_tokens

        # Construct `get_default_char` callable.
        if default_char:
            get_default_char = lambda _: default_char
        elif not get_default_char:
            get_default_char = lambda _: Char(' ', Token)

        self.get_default_char = get_default_char

        #: Cache for rendered screens.
        self._screen_lru_cache = SimpleLRUCache(maxsize=18)
        self._token_lru_cache = SimpleLRUCache(maxsize=1)
            # Only cache one token list. We don't need the previous item.

        # Render info for the mouse support.
        self._tokens = None  # The last rendered tokens.
        self._pos_to_indexes = None  # Mapping from mouse positions (x,y) to
                                     # positions in the token list.

    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.get_tokens)

    def _get_tokens_cached(self, cli):
        """
        Get tokens, but only retrieve tokens once during one render run.
        (This function is called several times during one rendering, because
        we also need those for calculating the dimensions.)
        """
        return self._token_lru_cache.get(
            cli.render_counter, lambda: self.get_tokens(cli))

    def has_focus(self, cli):
        return self._has_focus_filter(cli)

    def preferred_width(self, cli, max_available_width):
        """
        Return the preferred width for this control.
        That is the width of the longest line.
        """
        text = ''.join(t[1] for t in self._get_tokens_cached(cli))
        line_lengths = [get_cwidth(l) for l in text.split('\n')]
        return max(line_lengths)

    def preferred_height(self, cli, width):
        screen = self.create_screen(cli, width, None)
        return screen.height

    def create_screen(self, cli, width, height):
        # Get tokens
        tokens_with_mouse_handlers = self._get_tokens_cached(cli)

        default_char = self.get_default_char(cli)

        # Wrap/align right/center parameters.
        wrap_lines = self.wrap_lines(cli)
        right = self.align_right(cli)
        center = self.align_center(cli)

        def process_line(line):
            " Center or right align a single line. "
            used_width = token_list_width(line)
            padding = width - used_width
            if center:
                padding = int(padding / 2)
            return [(default_char.token, default_char.char * padding)] + line + [(Token, '\n')]

        if right or center:
            tokens2 = []
            for line in split_lines(tokens_with_mouse_handlers):
                tokens2.extend(process_line(line))
            tokens_with_mouse_handlers = tokens2

        # Strip mouse handlers from tokens.
        tokens = [tuple(item[:2]) for item in tokens_with_mouse_handlers]

        # Create screen, or take it from the cache.
        key = (default_char, tokens_with_mouse_handlers, width, wrap_lines, right, center)
        params = (default_char, tokens, width, wrap_lines, right, center)
        screen, self._pos_to_indexes = self._screen_lru_cache.get(key, lambda: self._get_screen(*params))

        self._tokens = tokens_with_mouse_handlers
        return screen

    @classmethod
    def _get_screen(cls, default_char, tokens, width, wrap_lines, right, center):
        screen = Screen(default_char, initial_width=width)

        # Only call write_data when we actually have tokens.
        # (Otherwise the screen height will go up from 0 to 1 while we don't
        # want that. -- An empty control should not take up any space.)
        if tokens:
            write_data_result = screen.write_data(tokens, width=(width if wrap_lines else None))

            indexes_to_pos = write_data_result.indexes_to_pos
            pos_to_indexes = dict((v, k) for k, v in indexes_to_pos.items())
        else:
            pos_to_indexes = {}

        return screen, pos_to_indexes

    @classmethod
    def static(cls, tokens):
        def get_static_tokens(cli):
            return tokens
        return cls(get_static_tokens)

    def mouse_handler(self, cli, mouse_event):
        """
        Handle mouse events.

        (When the token list contained mouse handlers and the user clicked on
        on any of these, the matching handler is called. This handler can still
        return `NotImplemented` in case we want the `Window` to handle this
        particular event.)
        """
        if self._pos_to_indexes:
            # Find position in the token list.
            position = mouse_event.position
            index = self._pos_to_indexes.get((position.x, position.y))

            if index is not None:
                # Find mouse handler for this character.
                count = 0
                for item in self._tokens:
                    count += len(item[1])
                    if count >= index:
                        if len(item) >= 3:
                            # Handler found. Call it.
                            handler = item[2]
                            handler(cli, mouse_event)
                            return
                        else:
                            break

        # Otherwise, don't handle here.
        return NotImplemented
コード例 #5
0
class BufferControl(UIControl):
    """
    Control for visualising the content of a `Buffer`.

    :param input_processors: list of :class:`~prompt_toolkit.layout.processors.Processor`.
    :param lexer: :class:`~prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting.
    :param preview_search: `bool` or `CLIFilter`: Show search while typing.
    :param get_search_state: Callable that takes a CommandLineInterface and
        returns the SearchState to be used. (If not CommandLineInterface.search_state.)
    :param wrap_lines: `bool` or `CLIFilter`: Wrap long lines.
    :param buffer_name: String representing the name of the buffer to display.
    :param default_char: :class:`.Char` instance to use to fill the background. This is
        transparent by default.
    :param focus_on_click: Focus this buffer when it's click, but not yet focussed.
    """
    def __init__(self,
                 buffer_name=DEFAULT_BUFFER,
                 input_processors=None,
                 highlighters=None,
                 lexer=None,
                 preview_search=False,
                 search_buffer_name=SEARCH_BUFFER,
                 get_search_state=None,
                 wrap_lines=True,
                 menu_position=None,
                 default_char=None,
                 focus_on_click=False):
        assert input_processors is None or all(isinstance(i, Processor) for i in input_processors)
        assert highlighters is None or all(isinstance(i, Highlighter) for i in highlighters)
        assert menu_position is None or callable(menu_position)
        assert lexer is None or isinstance(lexer, Lexer)
        assert get_search_state is None or callable(get_search_state)

        self.preview_search = to_cli_filter(preview_search)
        self.get_search_state = get_search_state
        self.wrap_lines = to_cli_filter(wrap_lines)
        self.focus_on_click = to_cli_filter(focus_on_click)

        self.input_processors = input_processors or []
        self.highlighters = highlighters or []
        self.buffer_name = buffer_name
        self.menu_position = menu_position
        self.lexer = lexer or SimpleLexer()
        self.default_char = default_char or Char(token=Token.Transparent)
        self.search_buffer_name = search_buffer_name

        #: LRU cache for the lexer.
        #: Often, due to cursor movement, undo/redo and window resizing
        #: operations, it happens that a short time, the same document has to be
        #: lexed. This is a faily easy way to cache such an expensive operation.
        self._token_lru_cache = SimpleLRUCache(maxsize=8)

        #: Keep a similar cache for rendered screens. (when we scroll up/down
        #: through the screen, or when we change another buffer, we don't want
        #: to recreate the same screen again.)
        self._screen_lru_cache = SimpleLRUCache(maxsize=8)

        #: Highlight Cache.
        #: When nothing of the buffer content or processors has changed, but
        #: the highlighting of the selection/search changes,
        self._highlight_lru_cache = SimpleLRUCache(maxsize=8)

        self._xy_to_cursor_position = None
        self._last_click_timestamp = None

    def _buffer(self, cli):
        """
        The buffer object that contains the 'main' content.
        """
        return cli.buffers[self.buffer_name]

    def has_focus(self, cli):
        # This control gets the focussed if the actual `Buffer` instance has the
        # focus or when any of the `InputProcessor` classes tells us that it
        # wants the focus. (E.g. in case of a reverse-search, where the actual
        # search buffer may not be displayed, but the "reverse-i-search" text
        # should get the focus.)
        return cli.focus_stack.current == self.buffer_name or \
            any(i.has_focus(cli) for i in self.input_processors)

    def preferred_width(self, cli, max_available_width):
        # Return the length of the longest line.
        return max(map(len, self._buffer(cli).document.lines))

    def preferred_height(self, cli, width):
        # Draw content on a screen using this width. Measure the height of the
        # result.
        screen, highlighters = self.create_screen(cli, width, None)
        return screen.height

    def _get_input_tokens(self, cli, document):
        """
        Tokenize input text for highlighting.
        Return (tokens, source_to_display, display_to_source) tuple.

        :param document: The document to be shown. This can be `buffer.document`
                         but could as well be a different one, in case we are
                         searching through the history. (Buffer.document_for_search)
        """
        def get():
            # Call lexer.
            tokens = list(self.lexer.get_tokens(cli, document.text))

            # 'Explode' tokens in characters.
            # (Some input processors -- like search/selection highlighter --
            # rely on that each item in the tokens array only contains one
            # character.)
            tokens = [(token, c) for token, text in tokens for c in text]

            # Run all processors over the input.
            # (They can transform both the tokens and the cursor position.)
            source_to_display_functions = []
            display_to_source_functions = []

            d_ = document  # Each processor receives the document of the previous one.

            for p in self.input_processors:
                transformation  = p.apply_transformation(cli, d_, tokens)
                d_ = transformation.document
                assert isinstance(transformation, Transformation)

                tokens = transformation.tokens
                source_to_display_functions.append(transformation.source_to_display)
                display_to_source_functions.append(transformation.display_to_source)

            # Chain cursor transformation (movement) functions.

            def source_to_display(cursor_position):
                " Chained source_to_display. "
                for f in source_to_display_functions:
                    cursor_position = f(cursor_position)
                return cursor_position

            def display_to_source(cursor_position):
                " Chained display_to_source. "
                for f in reversed(display_to_source_functions):
                    cursor_position = f(cursor_position)
                return cursor_position

            return tokens, source_to_display, display_to_source

        key = (
            document.text,

            # Include invalidation_hashes from all processors.
            tuple(p.invalidation_hash(cli, document) for p in self.input_processors),
        )

        return self._token_lru_cache.get(key, get)

    def create_screen(self, cli, width, height):
        buffer = self._buffer(cli)

        # Get the document to be shown. If we are currently searching (the
        # search buffer has focus, and the preview_search filter is enabled),
        # then use the search document, which has possibly a different
        # text/cursor position.)
        def preview_now():
            """ True when we should preview a search. """
            return bool(self.preview_search(cli) and
                        cli.buffers[self.search_buffer_name].text)

        if preview_now():
            if self.get_search_state:
                ss = self.get_search_state(cli)
            else:
                ss = cli.search_state

            document = buffer.document_for_search(SearchState(
                text=cli.current_buffer.text,
                direction=ss.direction,
                ignore_case=ss.ignore_case))
        else:
            document = buffer.document

        # Wrap.
        wrap_width = width if self.wrap_lines(cli) else None

        def _create_screen():
            screen = Screen(self.default_char, initial_width=width)

            # Get tokens
            # Note: we add the space character at the end, because that's where
            #       the cursor can also be.
            input_tokens, source_to_display, display_to_source = self._get_input_tokens(cli, document)
            input_tokens += [(self.default_char.token, ' ')]

            write_data_result = screen.write_data(input_tokens, width=wrap_width)
            indexes_to_pos = write_data_result.indexes_to_pos
            line_lengths = write_data_result.line_lengths

            pos_to_indexes = dict((v, k) for k, v in indexes_to_pos.items())

            def cursor_position_to_xy(cursor_position):
                """ Turn a cursor position in the buffer into x/y coordinates
                on the screen. """
                cursor_position = min(len(document.text), cursor_position)

                # First get the real token position by applying all transformations.
                cursor_position = source_to_display(cursor_position)

                # Then look up into the table.
                try:
                    return indexes_to_pos[cursor_position]
                except KeyError:
                    # This can fail with KeyError, but only if one of the
                    # processors is returning invalid key locations.
                    raise
                    # return 0, 0

            def xy_to_cursor_position(x, y):
                """ Turn x/y screen coordinates back to the original cursor
                position in the buffer. """
                # Look up reverse in table.
                while x > 0 or y > 0:
                    try:
                        index = pos_to_indexes[x, y]
                        break
                    except KeyError:
                        # No match found -> mouse click outside of region
                        # containing text. Look to the left or up.
                        if x: x -= 1
                        elif y: y -=1
                else:
                    # Nobreak.
                    index = 0

                # Transform.
                return display_to_source(index)

            return screen, cursor_position_to_xy, xy_to_cursor_position, line_lengths

        # Build a key for the caching. If any of these parameters changes, we
        # have to recreate a new screen.
        key = (
            # When the text changes, we obviously have to recreate a new screen.
            document.text,

            # When the width changes, line wrapping will be different.
            # (None when disabled.)
            wrap_width,

            # Include invalidation_hashes from all processors.
            tuple(p.invalidation_hash(cli, document) for p in self.input_processors),
        )

        # Get from cache, or create if this doesn't exist yet.
        screen, cursor_position_to_xy, self._xy_to_cursor_position, line_lengths = \
            self._screen_lru_cache.get(key, _create_screen)

        x, y = cursor_position_to_xy(document.cursor_position)
        screen.cursor_position = Point(y=y, x=x)

        # If there is an auto completion going on, use that start point for a
        # pop-up menu position. (But only when this buffer has the focus --
        # there is only one place for a menu, determined by the focussed buffer.)
        if cli.current_buffer_name == self.buffer_name:
            menu_position = self.menu_position(cli) if self.menu_position else None
            if menu_position is not None:
                assert isinstance(menu_position, int)
                x, y = cursor_position_to_xy(menu_position)
                screen.menu_position = Point(y=y, x=x)
            elif buffer.complete_state:
                # Position for completion menu.
                # Note: We use 'min', because the original cursor position could be
                #       behind the input string when the actual completion is for
                #       some reason shorter than the text we had before. (A completion
                #       can change and shorten the input.)
                x, y = cursor_position_to_xy(
                    min(buffer.cursor_position,
                        buffer.complete_state.original_document.cursor_position))
                screen.menu_position = Point(y=y, x=x)
            else:
                screen.menu_position = None

        # Add highlighting.
        highlight_key = (
            key,  # Includes everything from the 'key' above. (E.g. when the
                     # document changes, we have to recalculate highlighting.)

            # Include invalidation_hashes from all highlighters.
            tuple(h.invalidation_hash(cli, document) for h in self.highlighters)
        )

        highlighting = self._highlight_lru_cache.get(highlight_key, lambda:
            self._get_highlighting(cli, document, cursor_position_to_xy, line_lengths))

        return screen, highlighting

    def _get_highlighting(self, cli, document, cursor_position_to_xy, line_lengths):
        """
        Return a _HighlightDict for the highlighting. (This is a lazy dict of dicts.)

        The Window class will apply this for the visible regions. - That way,
        we don't have to recalculate the screen again for each selection/search
        change.

        :param line_lengths: Maps line numbers to the length of these lines.
        """
        def get_row_size(y):
            " Return the max 'x' value for a given row in the screen. "
            return max(1, line_lengths.get(y, 0))

        # Get list of fragments.
        row_to_fragments = defaultdict(list)

        for h in self.highlighters:
            for fragment in h.get_fragments(cli, document):
                # Expand fragments.
                start_column, start_row = cursor_position_to_xy(fragment.start)
                end_column, end_row = cursor_position_to_xy(fragment.end)
                token = fragment.token

                if start_row == end_row:
                    # Single line highlighting.
                    row_to_fragments[start_row].append(
                        _HighlightFragment(start_column, end_column, token))
                else:
                    # Multi line highlighting.
                    # (First line.)
                    row_to_fragments[start_row].append(
                        _HighlightFragment(start_column, get_row_size(start_row), token))

                    # (Middle lines.)
                    for y in range(start_row + 1, end_row):
                        row_to_fragments[y].append(_HighlightFragment(0, get_row_size(y), token))

                    # (Last line.)
                    row_to_fragments[end_row].append(_HighlightFragment(0, end_column, token))

        # Create dict to return.
        return _HighlightDict(row_to_fragments)

    def mouse_handler(self, cli, mouse_event):
        """
        Mouse handler for this control.
        """
        buffer = self._buffer(cli)
        position = mouse_event.position

        # Focus buffer when clicked.
        if self.has_focus(cli):
            if self._xy_to_cursor_position:
                # Translate coordinates back to the cursor position of the
                # original input.
                pos = self._xy_to_cursor_position(position.x, position.y)

                # Set the cursor position.
                if pos <= len(buffer.text):
                    if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN:
                        buffer.exit_selection()
                        buffer.cursor_position = pos

                    elif mouse_event.event_type == MouseEventTypes.MOUSE_UP:
                        # When the cursor was moved to another place, select the text.
                        # (The >1 is actually a small but acceptable workaround for
                        # selecting text in Vi navigation mode. In navigation mode,
                        # the cursor can never be after the text, so the cursor
                        # will be repositioned automatically.)
                        if abs(buffer.cursor_position - pos) > 1:
                            buffer.start_selection(selection_type=SelectionType.CHARACTERS)
                            buffer.cursor_position = pos

                        # Select word around cursor on double click.
                        # Two MOUSE_UP events in a short timespan are considered a double click.
                        double_click = self._last_click_timestamp and time.time() - self._last_click_timestamp < .3
                        self._last_click_timestamp = time.time()

                        if double_click:
                            start, end = buffer.document.find_boundaries_of_current_word()
                            buffer.cursor_position += start
                            buffer.start_selection(selection_type=SelectionType.CHARACTERS)
                            buffer.cursor_position += end - start
                    else:
                        # Don't handle scroll events here.
                        return NotImplemented

        # Not focussed, but focussing on click events.
        else:
            if self.focus_on_click(cli) and mouse_event.event_type == MouseEventTypes.MOUSE_UP:
                # Focus happens on mouseup. (If we did this on mousedown, the
                # up event will be received at the point where this widget is
                # focussed and be handled anyway.)
                cli.focus_stack.replace(self.buffer_name)
            else:
                return NotImplemented

    def move_cursor_down(self, cli):
        b = self._buffer(cli)
        b.cursor_position += b.document.get_cursor_down_position()

    def move_cursor_up(self, cli):
        b = self._buffer(cli)
        b.cursor_position += b.document.get_cursor_up_position()
コード例 #6
0
class Window(Container):
    """
    Container that holds a control.

    :param content: :class:`~prompt_toolkit.layout.controls.UIControl` instance.
    :param width: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance.
    :param height: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance.
    :param get_width: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`.
    :param get_height: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`.
    :param dont_extend_width: When `True`, don't take up more width then the
                              preferred width reported by the control.
    :param dont_extend_height: When `True`, don't take up more width then the
                               preferred height reported by the control.
    :param left_margins: A list of :class:`~prompt_toolkit.layout.margins.Margin`
        instance to be displayed on the left. For instance:
        :class:`~prompt_toolkit.layout.margins.NumberredMargin` can be one of
        them in order to show line numbers.
    :param right_margins: Like `left_margins`, but on the other side.
    :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the
        preferred amount of lines/columns to be always visible before/after the
        cursor. When both top and bottom are a very high number, the cursor
        will be centered vertically most of the time.
    :param allow_scroll_beyond_bottom: A `bool` or
        :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, allow
        scrolling so far, that the top part of the content is not visible
        anymore, while there is still empty space available at the bottom of
        the window. In the Vi editor for instance, this is possible. You will
        see tildes while the top part of the body is hidden.
    :param get_vertical_scroll: Callable that takes this window
        instance as input and returns a preferred vertical scroll.
        (When this is `None`, the scroll is only determined by the last and
        current cursor position.)
    :param get_horizontal_scroll: Callable that takes this window
        instance as input and returns a preferred vertical scroll.
    :param always_hide_cursor: A `bool` or
        :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, never
        display the cursor, even when the user control specifies a cursor
        position.
    """
    def __init__(self,
                 content,
                 width=None,
                 height=None,
                 get_width=None,
                 get_height=None,
                 dont_extend_width=False,
                 dont_extend_height=False,
                 left_margins=None,
                 right_margins=None,
                 scroll_offsets=None,
                 allow_scroll_beyond_bottom=False,
                 get_vertical_scroll=None,
                 get_horizontal_scroll=None,
                 always_hide_cursor=False):
        assert isinstance(content, UIControl)
        assert width is None or isinstance(width, LayoutDimension)
        assert height is None or isinstance(height, LayoutDimension)
        assert get_width is None or callable(get_width)
        assert get_height is None or callable(get_height)
        assert width is None or get_width is None
        assert height is None or get_height is None
        assert scroll_offsets is None or isinstance(scroll_offsets,
                                                    ScrollOffsets)
        assert left_margins is None or all(
            isinstance(m, Margin) for m in left_margins)
        assert right_margins is None or all(
            isinstance(m, Margin) for m in right_margins)
        assert get_vertical_scroll is None or callable(get_vertical_scroll)
        assert get_horizontal_scroll is None or callable(get_horizontal_scroll)

        self.allow_scroll_beyond_bottom = to_cli_filter(
            allow_scroll_beyond_bottom)
        self.always_hide_cursor = to_cli_filter(always_hide_cursor)

        self.content = content
        self.dont_extend_width = dont_extend_width
        self.dont_extend_height = dont_extend_height
        self.left_margins = left_margins or []
        self.right_margins = right_margins or []
        self.scroll_offsets = scroll_offsets or ScrollOffsets()
        self.get_vertical_scroll = get_vertical_scroll
        self.get_horizontal_scroll = get_horizontal_scroll
        self._width = get_width or (lambda cli: width)
        self._height = get_height or (lambda cli: height)

        # Cache for the screens generated by the margin.
        self._margin_cache = SimpleLRUCache(maxsize=8)

        self.reset()

    def __repr__(self):
        return 'Window(content=%r)' % self.content

    def reset(self):
        self.content.reset()

        #: Scrolling position of the main content.
        self.vertical_scroll = 0
        self.horizontal_scroll = 0

        #: Keep render information (mappings between buffer input and render
        #: output.)
        self.render_info = None

    def preferred_width(self, cli, max_available_width):
        # Width of the margins.
        total_margin_width = sum(
            m.get_width(cli) for m in self.left_margins + self.right_margins)

        # Window of the content.
        preferred_width = self.content.preferred_width(
            cli, max_available_width - total_margin_width)

        if preferred_width is not None:
            preferred_width += total_margin_width

        # Merge.
        return self._merge_dimensions(dimension=self._width(cli),
                                      preferred=preferred_width,
                                      dont_extend=self.dont_extend_width)

    def preferred_height(self, cli, width):
        return self._merge_dimensions(dimension=self._height(cli),
                                      preferred=self.content.preferred_height(
                                          cli, width),
                                      dont_extend=self.dont_extend_height)

    @staticmethod
    def _merge_dimensions(dimension, preferred=None, dont_extend=False):
        """
        Take the LayoutDimension from this `Window` class and the received
        preferred size from the `UIControl` and return a `LayoutDimension` to
        report to the parent container.
        """
        dimension = dimension or LayoutDimension()

        # When a preferred dimension was explicitly given to the Window,
        # ignore the UIControl.
        if dimension.preferred_specified:
            preferred = dimension.preferred

        # When a 'preferred' dimension is given by the UIControl, make sure
        # that it stays within the bounds of the Window.
        if preferred is not None:
            if dimension.max:
                preferred = min(preferred, dimension.max)

            if dimension.min:
                preferred = max(preferred, dimension.min)

        # When a `dont_extend` flag has been given, use the preferred dimension
        # also as the max dimension.
        if dont_extend and preferred is not None:
            max_ = min(dimension.max, preferred)
        else:
            max_ = dimension.max

        return LayoutDimension(min=dimension.min,
                               max=max_,
                               preferred=preferred)

    def write_to_screen(self, cli, screen, mouse_handlers, write_position):
        """
        Write window to screen. This renders the user control, the margins and
        copies everything over to the absolute position at the given screen.
        """
        # Calculate margin sizes.
        left_margin_widths = [m.get_width(cli) for m in self.left_margins]
        right_margin_widths = [m.get_width(cli) for m in self.right_margins]
        total_margin_width = sum(left_margin_widths + right_margin_widths)

        # Render UserControl.
        tpl = self.content.create_screen(
            cli, write_position.width - total_margin_width,
            write_position.height)
        if isinstance(tpl, tuple):
            temp_screen, highlighting = tpl
        else:
            # For backwards, compatibility.
            temp_screen, highlighting = tpl, defaultdict(
                lambda: defaultdict(lambda: None))

        # Scroll content.
        applied_scroll_offsets = self._scroll(
            temp_screen, write_position.width - total_margin_width,
            write_position.height, cli)

        # Write body to screen.
        self._copy_body(cli, temp_screen, highlighting, screen, write_position,
                        sum(left_margin_widths),
                        write_position.width - total_margin_width,
                        applied_scroll_offsets)

        # Remember render info. (Set before generating the margins. They need this.)
        self.render_info = WindowRenderInfo(
            original_screen=temp_screen,
            horizontal_scroll=self.horizontal_scroll,
            vertical_scroll=self.vertical_scroll,
            window_width=write_position.width,
            window_height=write_position.height,
            cursor_position=Point(
                y=temp_screen.cursor_position.y - self.vertical_scroll,
                x=temp_screen.cursor_position.x - self.horizontal_scroll),
            configured_scroll_offsets=self.scroll_offsets,
            applied_scroll_offsets=applied_scroll_offsets)

        # Set mouse handlers.
        def mouse_handler(cli, mouse_event):
            """ Wrapper around the mouse_handler of the `UIControl` that turns
            absolute coordinates into relative coordinates. """
            position = mouse_event.position

            # Call the mouse handler of the UIControl first.
            result = self.content.mouse_handler(
                cli,
                MouseEvent(position=Point(x=position.x - write_position.xpos -
                                          sum(left_margin_widths),
                                          y=position.y - write_position.ypos +
                                          self.vertical_scroll),
                           event_type=mouse_event.event_type))

            # If it returns NotImplemented, handle it here.
            if result == NotImplemented:
                return self._mouse_handler(cli, mouse_event)

            return result

        mouse_handlers.set_mouse_handler_for_range(
            x_min=write_position.xpos + sum(left_margin_widths),
            x_max=write_position.xpos + write_position.width -
            total_margin_width,
            y_min=write_position.ypos,
            y_max=write_position.ypos + write_position.height,
            handler=mouse_handler)

        # Render and copy margins.
        move_x = 0

        def render_margin(m, width):
            " Render margin. Return `Screen`. "
            # Retrieve margin tokens.
            tokens = m.create_margin(cli, self.render_info, width,
                                     write_position.height)

            # Turn it into a screen. (Take a screen from the cache if we
            # already rendered those tokens using this size.)
            def create_screen():
                return TokenListControl.static(tokens).create_screen(
                    cli, width + 1, write_position.height)

            key = (tokens, width, write_position.height)
            return self._margin_cache.get(key, create_screen)

        for m, width in zip(self.left_margins, left_margin_widths):
            # Create screen for margin.
            margin_screen = render_margin(m, width)

            # Copy and shift X.
            self._copy_margin(margin_screen, screen, write_position, move_x,
                              width)
            move_x += width

        move_x = write_position.width - sum(right_margin_widths)

        for m, width in zip(self.right_margins, right_margin_widths):
            # Create screen for margin.
            margin_screen = render_margin(m, width)

            # Copy and shift X.
            self._copy_margin(margin_screen, screen, write_position, move_x,
                              width)
            move_x += width

    def _copy_body(self, cli, temp_screen, highlighting, new_screen,
                   write_position, move_x, width, applied_scroll_offsets):
        """
        Copy characters from the temp screen that we got from the `UIControl`
        to the real screen.
        """
        xpos = write_position.xpos + move_x
        ypos = write_position.ypos
        height = write_position.height

        temp_buffer = temp_screen.data_buffer
        new_buffer = new_screen.data_buffer
        temp_screen_height = temp_screen.height

        vertical_scroll = self.vertical_scroll
        horizontal_scroll = self.horizontal_scroll
        y = 0

        # Now copy the region we need to the real screen.
        for y in range(0, height):
            # We keep local row variables. (Don't look up the row in the dict
            # for each iteration of the nested loop.)
            new_row = new_buffer[y + ypos]

            if y >= temp_screen_height and y >= write_position.height:
                # Break out of for loop when we pass after the last row of the
                # temp screen. (We use the 'y' position for calculation of new
                # screen's height.)
                break
            else:
                temp_row = temp_buffer[y + vertical_scroll]
                highlighting_row = highlighting[y + vertical_scroll]

                # Copy row content, except for transparent tokens.
                # (This is useful in case of floats.)
                # Also apply highlighting.
                for x in range(0, width):
                    cell = temp_row[x + horizontal_scroll]
                    highlighting_token = highlighting_row[x]

                    if highlighting_token:
                        new_row[x + xpos] = Char(cell.char, highlighting_token)
                    elif cell.token != Transparent:
                        new_row[x + xpos] = cell

        if self.content.has_focus(cli):
            new_screen.cursor_position = Point(
                y=temp_screen.cursor_position.y + ypos - vertical_scroll,
                x=temp_screen.cursor_position.x + xpos - horizontal_scroll)

            if not self.always_hide_cursor(cli):
                new_screen.show_cursor = temp_screen.show_cursor

        if not new_screen.menu_position and temp_screen.menu_position:
            new_screen.menu_position = Point(
                y=temp_screen.menu_position.y + ypos - vertical_scroll,
                x=temp_screen.menu_position.x + xpos - horizontal_scroll)

        # Update height of the output screen. (new_screen.write_data is not
        # called, so the screen is not aware of its height.)
        new_screen.height = max(new_screen.height, ypos + y + 1)

    def _copy_margin(self, temp_screen, new_screen, write_position, move_x,
                     width):
        """
        Copy characters from the margin screen to the real screen.
        """
        xpos = write_position.xpos + move_x
        ypos = write_position.ypos

        temp_buffer = temp_screen.data_buffer
        new_buffer = new_screen.data_buffer

        # Now copy the region we need to the real screen.
        for y in range(0, write_position.height):
            new_row = new_buffer[y + ypos]
            temp_row = temp_buffer[y]

            # Copy row content, except for transparent tokens.
            # (This is useful in case of floats.)
            for x in range(0, width):
                cell = temp_row[x]
                if cell.token != Transparent:
                    new_row[x + xpos] = cell

    def _scroll(self, temp_screen, width, height, cli):
        """
        Scroll to make sure the cursor position is visible and that we maintain the
        requested scroll offset.
        Return the applied scroll offsets.
        """
        def do_scroll(current_scroll, scroll_offset_start, scroll_offset_end,
                      cursor_pos, window_size, content_size):
            " Scrolling algorithm. Used for both horizontal and vertical scrolling. "
            # Calculate the scroll offset to apply.
            # This can obviously never be more than have the screen size. Also, when the
            # cursor appears at the top or bottom, we don't apply the offset.
            scroll_offset_start = int(
                min(scroll_offset_start, window_size / 2, cursor_pos))
            scroll_offset_end = int(
                min(scroll_offset_end, window_size / 2,
                    content_size - 1 - cursor_pos))

            # Prevent negative scroll offsets.
            if current_scroll < 0:
                current_scroll = 0

            # Scroll back if we scrolled to much and there's still space to show more of the document.
            if (not self.allow_scroll_beyond_bottom(cli)
                    and current_scroll > content_size - window_size):
                current_scroll = max(0, content_size - window_size)

            # Scroll up if cursor is before visible part.
            if current_scroll > cursor_pos - scroll_offset_start:
                current_scroll = max(0, cursor_pos - scroll_offset_start)

            # Scroll down if cursor is after visible part.
            if current_scroll < (cursor_pos +
                                 1) - window_size + scroll_offset_end:
                current_scroll = (cursor_pos +
                                  1) - window_size + scroll_offset_end

            # Calculate the applied scroll offset. This value can be lower than what we had.
            scroll_offset_start = max(0,
                                      min(current_scroll, scroll_offset_start))
            scroll_offset_end = max(
                0,
                min(content_size - current_scroll - window_size,
                    scroll_offset_end))

            return current_scroll, scroll_offset_start, scroll_offset_end

        # When a preferred scroll is given, take that first into account.
        if self.get_vertical_scroll:
            self.vertical_scroll = self.get_vertical_scroll(self)
            assert isinstance(self.vertical_scroll, int)
        if self.get_horizontal_scroll:
            self.horizontal_scroll = self.get_horizontal_scroll(self)
            assert isinstance(self.horizontal_scroll, int)

        # Update horizontal/vertical scroll to make sure that the cursor
        # remains visible.
        offsets = self.scroll_offsets

        self.vertical_scroll, scroll_offset_top, scroll_offset_bottom = do_scroll(
            current_scroll=self.vertical_scroll,
            scroll_offset_start=offsets.top,
            scroll_offset_end=offsets.bottom,
            cursor_pos=temp_screen.cursor_position.y,
            window_size=height,
            content_size=temp_screen.height)

        self.horizontal_scroll, scroll_offset_left, scroll_offset_right = do_scroll(
            current_scroll=self.horizontal_scroll,
            scroll_offset_start=offsets.left,
            scroll_offset_end=offsets.right,
            cursor_pos=temp_screen.cursor_position.x,
            window_size=width,
            content_size=temp_screen.width)

        applied_scroll_offsets = ScrollOffsets(top=scroll_offset_top,
                                               bottom=scroll_offset_bottom,
                                               left=scroll_offset_left,
                                               right=scroll_offset_right)

        return applied_scroll_offsets

    def _mouse_handler(self, cli, mouse_event):
        """
        Mouse handler. Called when the UI control doesn't handle this
        particular event.
        """
        if mouse_event.event_type == MouseEventTypes.SCROLL_DOWN:
            self._scroll_down(cli)
        elif mouse_event.event_type == MouseEventTypes.SCROLL_UP:
            self._scroll_up(cli)

    def _scroll_down(self, cli):
        " Scroll window down. "
        info = self.render_info

        if self.vertical_scroll < info.content_height - info.window_height:
            if info.cursor_position.y <= info.configured_scroll_offsets.top:
                self.content.move_cursor_down(cli)

            self.vertical_scroll += 1

    def _scroll_up(self, cli):
        " Scroll window up. "
        info = self.render_info

        if info.vertical_scroll > 0:
            if info.cursor_position.y >= info.window_height - 1 - info.configured_scroll_offsets.bottom:
                self.content.move_cursor_up(cli)

            self.vertical_scroll -= 1

    def walk(self, cli):
        # Only yield self. A window doesn't have children.
        yield self