text: # -*- coding: utf-8 -*-
  
  ######
  # This file is a copy of a future version of asciimatics's TextBox, one the
  # package gets upgraded this can be removed
  ######
  
  """This module implements a multi line editing text box"""
  from __future__ import division
  from __future__ import absolute_import
  from __future__ import print_function
  from __future__ import unicode_literals
  from builtins import chr
  from builtins import str
  from copy import copy
  from logging import getLogger
  from asciimatics.event import KeyboardEvent, MouseEvent
  from asciimatics.screen import Screen
  from asciimatics.strings import ColouredText
  from asciimatics.widgets.widget import Widget
  from asciimatics.widgets.utilities import _find_min_start, _enforce_width, _get_offset
  
  # Logging
  logger = getLogger(__name__)
  
  
  class TextBox(Widget):
      """
      A TextBox is a widget for multi-line text editing.
  
      It consists of a framed box with option label.
      """
  
      __slots__ = ["_label", "_line", "_column", "_start_line", "_start_column", "_required_height",
                   "_as_string", "_line_wrap", "_on_change", "_reflowed_text_cache", "_parser",
                   "_readonly", "_hide_cursor", "_auto_scroll"]
  
      def __init__(self, height, label=None, name=None, as_string=False, line_wrap=False, parser=None,
                   on_change=None, readonly=False, **kwargs):
          """
          :param height: The required number of input lines for this TextBox.
          :param label: An optional label for the widget.
          :param name: The name for the TextBox.
          :param as_string: Use string with newline separator instead of a list
              for the value of this widget.
          :param line_wrap: Whether to wrap at the end of the line.
          :param parser: Optional parser to colour text.
          :param on_change: Optional function to call when text changes.
          :param readonly: Whether the widget prevents user input to change values.  Default is False.
  
          Also see the common keyword arguments in :py:obj:`.Widget`.
          """
          super(TextBox, self).__init__(name, **kwargs)
          self._label = label
          self._line = 0
          self._column = 0
          self._start_line = 0
          self._start_column = 0
          self._required_height = height
          self._as_string = as_string
          self._line_wrap = line_wrap
          self._parser = parser
          self._on_change = on_change
          self._reflowed_text_cache = None
          self._readonly = readonly
          self._hide_cursor = False
          self._auto_scroll = True
  
      def update(self, frame_no):
          self._draw_label()
  
          # Calculate new visible limits if needed.
          height = self._h
          if not self._line_wrap:
              self._start_column = min(self._start_column, self._column)
              self._start_column += _find_min_start(
                  str(self._value[self._line][self._start_column:self._column + 1]),
                  self.width,
                  self._frame.canvas.unicode_aware,
                  self._column >= self.string_len(str(self._value[self._line])))
  
          # Clear out the existing box content
          (colour, attr, background) = self._pick_colours("readonly" if self._readonly else "edit_text")
          self._frame.canvas.clear_buffer(
              colour, attr, background, self._x + self._offset, self._y, self.width, height)
  
          # Convert value offset to display offsets
          # NOTE: _start_column is always in display coordinates.
          display_text = self._reflowed_text
          display_start_column = self._start_column
          display_line, display_column = 0, 0
          for i, (_, line, col) in enumerate(display_text):
              if line < self._line or (line == self._line and col <= self._column):
                  display_line = i
                  display_column = self._column - col
  
          # Restrict to visible/valid content.
          self._start_line = max(0, max(display_line - height + 1,
                                        min(self._start_line, display_line)))
  
          # Render visible portion of the text.
          for line, (text, _, _) in enumerate(display_text):
              if self._start_line <= line < self._start_line + height:
                  paint_text = _enforce_width(
                      text[display_start_column:], self.width, self._frame.canvas.unicode_aware)
                  self._frame.canvas.paint(
                      str(paint_text),
                      self._x + self._offset,
                      self._y + line - self._start_line,
                      colour, attr, background,
                      colour_map=paint_text.colour_map if hasattr(paint_text, "colour_map") else None)
  
          # Since we switch off the standard cursor, we need to emulate our own
          # if we have the input focus.
          if self._has_focus and not self._hide_cursor:
              line = str(display_text[display_line][0])
              logger.debug("Cursor: %d,%d", display_start_column, display_column)
              text_width = self.string_len(line[display_start_column:display_column])
  
              self._draw_cursor(
                  " " if display_column >= len(line) else line[display_column],
                  frame_no,
                  self._x + self._offset + text_width,
                  self._y + display_line - self._start_line)
  
      def reset(self):
          # Reset to original data and move to end of the text.
          self._start_line = 0
          self._start_column = 0
          if self._auto_scroll or self._line > len(self._value) - 1:
              self._line = len(self._value) - 1
  
          self._column = 0 if self._is_disabled else len(self._value[self._line])
          self._reflowed_text_cache = None
  
      def _change_line(self, delta):
          """
          Move the cursor up/down the specified number of lines.
  
          :param delta: The number of lines to move (-ve is up, +ve is down).
          """
          # Ensure new line is within limits
          self._line = min(max(0, self._line + delta), len(self._value) - 1)
  
          # Fix up column if the new line is shorter than before.
          if self._column >= len(self._value[self._line]):
              self._column = len(self._value[self._line])
  
      def process_event(self, event):
          def _join(a, b):
              if self._parser:
                  return ColouredText(a, self._parser, colour=b[0].first_colour).join(b)
              return a.join(b)
  
          if isinstance(event, KeyboardEvent):
              old_value = copy(self._value)
              if event.key_code in [10, 13] and not self._readonly:
                  # Split and insert line  on CR or LF.
                  self._value.insert(self._line + 1,
                                     self._value[self._line][self._column:])
                  self._value[self._line] = self._value[self._line][:self._column]
                  self._line += 1
                  self._column = 0
              elif event.key_code == Screen.KEY_BACK and not self._readonly:
                  if self._column > 0:
                      # Delete character in front of cursor.
                      self._value[self._line] = _join(
                          "",
                          [self._value[self._line][:self._column - 1], self._value[self._line][self._column:]])
                      self._column -= 1
                  else:
                      if self._line > 0:
                          # Join this line with previous
                          self._line -= 1
                          self._column = len(self._value[self._line])
                          self._value[self._line] += \
                              self._value.pop(self._line + 1)
              elif event.key_code == Screen.KEY_DELETE and not self._readonly:
                  if self._column < len(self._value[self._line]):
                      self._value[self._line] = _join(
                          "",
                          [self._value[self._line][:self._column], self._value[self._line][self._column + 1:]])
                  else:
                      if self._line < len(self._value) - 1:
                          # Join this line with next
                          self._value[self._line] += \
                              self._value.pop(self._line + 1)
              elif event.key_code == Screen.KEY_PAGE_UP:
                  self._change_line(-self._h)
              elif event.key_code == Screen.KEY_PAGE_DOWN:
                  self._change_line(self._h)
              elif event.key_code == Screen.KEY_UP:
                  self._change_line(-1)
              elif event.key_code == Screen.KEY_DOWN:
                  self._change_line(1)
              elif event.key_code == Screen.KEY_LEFT:
                  # Move left one char, wrapping to previous line if needed.
                  self._column -= 1
                  if self._column < 0:
                      if self._line > 0:
                          self._line -= 1
                          self._column = len(self._value[self._line])
                      else:
                          self._column = 0
              elif event.key_code == Screen.KEY_RIGHT:
                  # Move right one char, wrapping to next line if needed.
                  self._column += 1
                  if self._column > len(self._value[self._line]):
                      if self._line < len(self._value) - 1:
                          self._line += 1
                          self._column = 0
                      else:
                          self._column = len(self._value[self._line])
              elif event.key_code == Screen.KEY_HOME:
                  # Go to the start of this line
                  self._column = 0
              elif event.key_code == Screen.KEY_END:
                  # Go to the end of this line
                  self._column = len(self._value[self._line])
              elif event.key_code >= 32 and not self._readonly:
                  # Insert any visible text at the current cursor position.
                  self._value[self._line] = _join(
                      chr(event.key_code),
                      [self._value[self._line][:self._column], self._value[self._line][self._column:]])
                  self._column += 1
              else:
                  # Ignore any other key press.
                  return event
  
              # If we got here we might have changed the value...
              if old_value != self._value:
                  self._reflowed_text_cache = None
                  if self._on_change:
                      self._on_change()
  
          elif isinstance(event, MouseEvent):
              # Mouse event - rebase coordinates to Frame context.
              if event.buttons != 0:
                  if self.is_mouse_over(event, include_label=False):
                      # Find the line first.
                      clicked_line = event.y - self._y + self._start_line
                      if self._line_wrap:
                          # Line-wrapped text needs to be mapped to visible lines
                          display_text = self._reflowed_text
                          clicked_line = min(clicked_line, len(display_text) - 1)
                          text_line = display_text[clicked_line][1]
                          text_col = display_text[clicked_line][2]
                      else:
                          # non-wrapped just needs a little end protection
                          text_line = max(0, clicked_line)
                          text_col = 0
                      self._line = min(len(self._value) - 1, text_line)
  
                      # Now figure out location in text based on width of each glyph.
                      self._column = (self._start_column + text_col +
                                      _get_offset(
                                          str(self._value[self._line][self._start_column + text_col:]),
                                          event.x - self._x - self._offset,
                                          self._frame.canvas.unicode_aware))
                      self._column = min(len(self._value[self._line]), self._column)
                      self._column = max(0, self._column)
                      return None
              # Ignore other mouse events.
              return event
          else:
              # Ignore other events
              return event
  
          # If we got here, we processed the event - swallow it.
          return None
  
      def required_height(self, offset, width):
          return self._required_height
  
      @property
      def _reflowed_text(self):
          """
          The text as should be formatted on the screen.
  
          This is an array of tuples of the form (text, value line, value column offset) where
          the line and column offsets are indeces into the value (not displayed glyph coordinates).
          """
          if self._reflowed_text_cache is None:
              if self._line_wrap:
                  self._reflowed_text_cache = []
                  limit = self._w - self._offset
                  for i, line in enumerate(self._value):
                      column = 0
                      while self.string_len(str(line)) >= limit:
                          sub_string = _enforce_width(
                              line, limit, self._frame.canvas.unicode_aware)
                          self._reflowed_text_cache.append((sub_string, i, column))
                          line = line[len(sub_string):]
                          column += len(sub_string)
                      self._reflowed_text_cache.append((line, i, column))
              else:
                  self._reflowed_text_cache = [(x, i, 0) for i, x in enumerate(self._value)]
  
          return self._reflowed_text_cache
  
      @property
      def hide_cursor(self):
          """
          Set to True to stop the cursor from showing.  Defaults to False.
          """
          return self._hide_cursor
  
      @hide_cursor.setter
      def hide_cursor(self, new_value):
          self._hide_cursor = new_value
  
      @property
      def auto_scroll(self):
          """
          When set to True the TextBox will scroll to the bottom when created or
          next text is added. When set to False, the current scroll position
          will remain even if the contents are changed.
  
          Defaults to True.
          """
          return self._auto_scroll
  
      @auto_scroll.setter
      def auto_scroll(self, new_value):
          self._auto_scroll = new_value
  
      @property
      def value(self):
          """
          The current value for this TextBox.
          """
          if self._value is None:
              self._value = [""]
          return "\n".join([str(x) for x in self._value]) if self._as_string else self._value
  
      @value.setter
      def value(self, new_value):
          # Convert to the internal format
          old_value = self._value
          if new_value is None:
              new_value = [""]
          elif self._as_string:
              new_value = new_value.split("\n")
          self._value = new_value
  
          # TODO: Sort out speed of this code
          if self._parser:
              new_value = []
              last_colour = None
              for line in self._value:
                  if hasattr(line, "raw_text"):
                      value = line
                  else:
                      value = ColouredText(line, self._parser, colour=last_colour)
                  new_value.append(value)
                  last_colour = value.last_colour
              self._value = new_value
          self.reset()
  
          # Only trigger the notification after we've changed the value.
          if old_value != self._value and self._on_change:
              self._on_change()
  
      @property
      def readonly(self):
          """
          Whether this widget is readonly or not.
          """
          return self._readonly
  
      @readonly.setter
      def readonly(self, new_value):
          self._readonly = new_value
  
      @property
      def frame_update_count(self):
          # Force refresh for cursor if needed.
          if self._has_focus and not self._frame.reduce_cpu and not self._hide_cursor:
              return 5
  
          return 0
  