/****************************************************************************
 * apps/graphics/nxwidgets/src/cmultilinetextbox.cxx
 *
 *   Copyright (C) 2012, 2016 Gregory Nutt. All rights reserved.
 *   Author: Gregory Nutt <gnutt@nuttx.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name NuttX, NxWidgets, nor the names of its contributors
 *    me be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************
 *
 * Portions of this package derive from Woopsi (http://woopsi.org/) and
 * portions are original efforts.  It is difficult to determine at this
 * point what parts are original efforts and which parts derive from Woopsi.
 * However, in any event, the work of  Antony Dzeryn will be acknowledged
 * in most NxWidget files.  Thanks Antony!
 *
 *   Copyright (c) 2007-2011, Antony Dzeryn
 *   All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * * Neither the names "Woopsi", "Simian Zombie" nor the
 *   names of its contributors may be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY Antony Dzeryn ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL Antony Dzeryn BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <stdint.h>
#include <stdbool.h>

#include "graphics/nxwidgets/cwidgetcontrol.hxx"
#include "graphics/nxwidgets/cnxfont.hxx"
#include "graphics/nxwidgets/ctext.hxx"
#include "graphics/nxwidgets/cgraphicsport.hxx"
#include "graphics/nxwidgets/singletons.hxx"
#include "graphics/nxwidgets/cstringiterator.hxx"
#include "graphics/nxwidgets/cnxtimer.hxx"
#include "graphics/nxwidgets/cmultilinetextbox.hxx"

/****************************************************************************
 * Pre-Processor Definitions
 ****************************************************************************/

/****************************************************************************
 * Method Implementations
 ****************************************************************************/

using namespace NXWidgets;

/**
 * Constructor.
 *
 * @param pWidgetControl The widget control for the display.
 * @param x The x coordinate of the text box, relative to its parent.
 * @param y The y coordinate of the text box, relative to its parent.
 * @param width The width of the textbox.
 * @param height The height of the textbox.
 * @param text Pointer to a string to display in the textbox.
 * @param flags Standard widget flag options.
 * @param maxRows The maximum number of rows the textbox can track.  Adding
 *   text beyond this number will cause rows at the start of the text to be
 *   forgotten; text is essentially stored as a queue, and adding to the back
 *   of a full queue causes the front items to be popped off.  Setting this to
 *   0 will make the textbox track only the visible rows.
 * @param style The style that the widget should use.  If this is not
 *   specified, the widget will use the values stored in the global
 *   g_defaultWidgetStyle object.  The widget will copy the properties of
 *   the style into its own internal style object.
 */

CMultiLineTextBox::CMultiLineTextBox(CWidgetControl *pWidgetControl,
                                     nxgl_coord_t x, nxgl_coord_t y,
                                     nxgl_coord_t width, nxgl_coord_t height,
                                     const CNxString &text, uint32_t flags,
                                     nxgl_coord_t maxRows, CWidgetStyle *style)
: CScrollingPanel(pWidgetControl, x, y, width, height, flags, style)
{
  m_hAlignment            = TEXT_ALIGNMENT_HORIZ_CENTER;
  m_vAlignment            = TEXT_ALIGNMENT_VERT_CENTER;
  m_topRow                = 0;

  // The border is one line thick

  m_borderSize.top        = 1;
  m_borderSize.right      = 1;
  m_borderSize.bottom     = 1;
  m_borderSize.left       = 1;

  CRect rect;
  getClientRect(rect);
  m_text                  = new CText(getFont(), "", rect.getWidth());
  m_canvasWidth           = rect.getWidth();

  m_flags.draggable       = true;
  m_flags.doubleClickable = true;
  m_maxRows               = maxRows;

  calculateVisibleRows();

  // Set maximum rows if value not set

  if (m_maxRows == 0)
  {
    m_maxRows            = m_visibleRows + 1;
  }

  m_cursorPos            = 0;
  m_showCursor           = SHOW_CURSOR_NEVER;
  m_wrapCursor           = false;

  setText(text);
}

/**
 * Set the horizontal alignment of text within the textbox.
 *
 * @param alignment The horizontal position of the text.
 */

void CMultiLineTextBox::setTextAlignmentHoriz(TextAlignmentHoriz alignment)
{
  m_hAlignment = alignment;
  redraw();
}

/**
 * Set the vertical alignment of text within the textbox.
 *
 * @param alignment The vertical position of the text.
 */

void CMultiLineTextBox::setTextAlignmentVert(TextAlignmentVert alignment)
{
  m_vAlignment = alignment;
  redraw();
}

/**
 * Returns the number of "pages" that the text spans.  A page
 * is defined as the amount of text that can be displayed within
 * the textbox at one time.
 *
 * @return The page count.
 */

const int CMultiLineTextBox::getPageCount(void) const
{
  if (m_visibleRows > 0)
    {
      return (m_text->getLineCount() / m_visibleRows) + 1;
    }
  else
    {
      return 1;
    }
}

/**
 * Returns the current page.
 *
 * @return The current page.
 * @see getPageCount().
 */

const int CMultiLineTextBox::getCurrentPage(void) const
{
  // Calculate the top line of text

  int topRow = -m_canvasY / m_text->getLineHeight();

  // Return the page on which the top row falls

  if (m_visibleRows > 0)
    {
      return topRow / m_visibleRows;
    }
  else
    {
      return 1;
    }
}

/**
 * Set the text displayed in the textbox.
 *
 * @param text String to display.
 */

void CMultiLineTextBox::setText(const CNxString &text)
{
  bool drawingEnabled = m_flags.drawingEnabled;
  disableDrawing();

  m_text->setText(text);

  cullTopLines();
  limitCanvasHeight();
  jumpToTextBottom();

  if (drawingEnabled)
    {
      enableDrawing();
    }
  redraw();

  m_widgetEventHandlers->raiseValueChangeEvent();
}

/**
 * Append new text to the end of the current text
 * displayed in the textbox.
 *
 * @param text String to append.
 */

void CMultiLineTextBox::appendText(const CNxString &text)
{
  bool drawingEnabled = m_flags.drawingEnabled;
  disableDrawing();

  m_text->append(text);

  cullTopLines();
  limitCanvasHeight();
  jumpToTextBottom();

  if (drawingEnabled)
    {
      enableDrawing();
    }
  redraw();

  m_widgetEventHandlers->raiseValueChangeEvent();
}

/**
 * Remove all characters from the string from the start index onwards.
 *
 * @param startIndex Index to remove from.
 */

void CMultiLineTextBox::removeText(const unsigned int startIndex)
{
  removeText(startIndex, m_text->getLength() - startIndex);
}

/**
 * Remove specified number of characters from the string from the
 * start index onwards.
 *
 * @param startIndex Index to remove from.
 * @param count Number of characters to remove.
 */

void CMultiLineTextBox::removeText(const unsigned int startIndex,
                                   const unsigned int count)
{
  bool drawingEnabled = m_flags.drawingEnabled;
  disableDrawing();

  m_text->remove(startIndex, count);

  limitCanvasHeight();
  limitCanvasY();

  moveCursorToPosition(startIndex);

  if (drawingEnabled)
    {
      enableDrawing();
    }
  redraw();

  m_widgetEventHandlers->raiseValueChangeEvent();
}

/**
 * Set the font used in the textbox.
 *
 * @param font Pointer to the new font.
 */

void CMultiLineTextBox::setFont(CNxFont *font)
{
  bool drawingEnabled = m_flags.drawingEnabled;
  disableDrawing();

  m_style.font = font;
  m_text->setFont(font);

  cullTopLines();
  limitCanvasHeight();
  limitCanvasY();

  if (drawingEnabled)
    {
      enableDrawing();
    }
  redraw();

  m_widgetEventHandlers->raiseValueChangeEvent();
}

/**
 * Get the length of the text string.
 *
 * @return The length of the text string.
 */

const int CMultiLineTextBox::getTextLength(void) const
{
  return m_text->getLength();
}

/**
 * Sets the cursor display mode.
 *
 * @param cursorMode Determines cursor display mode
 */

void CMultiLineTextBox::showCursor(EShowCursor cursorMode)
{
  if (m_showCursor != cursorMode)
   {
      m_showCursor = (uint8_t)cursorMode;
      redraw();
    }
}

/**
 * Move the cursor to the text position specified.  0 indicates the start
 * of the string.  If position is greater than the length of the string,
 * the cursor is moved to the end of the string.
 *
 * @param position The new cursor position.
 */

void CMultiLineTextBox::moveCursorToPosition(const int position)
{
  CGraphicsPort *port = m_widgetControl->getGraphicsPort();

  // Erase existing cursor

  drawCursor(port);

  // Force position to within confines of string

  if (position < 0)
    {
      m_cursorPos = 0;
    }
  else
    {
      int len = (int)m_text->getLength();
      m_cursorPos = len > position ? position : len;
    }

  // Draw cursor in new position

  drawCursor(port);
}

/**
 * Insert text at the specified index.
 *
 * @param text The text to insert.
 * @param index Index at which to insert the text.
 */

void CMultiLineTextBox::insertText(const CNxString &text,
                                   const unsigned int index)
{
  bool drawingEnabled = m_flags.drawingEnabled;
  disableDrawing();

  m_text->insert(text, index);

  cullTopLines();
  limitCanvasHeight();

  moveCursorToPosition(index + text.getLength());

  if (drawingEnabled)
    {
      enableDrawing();
    }
  redraw();

  m_widgetEventHandlers->raiseValueChangeEvent();
}

/**
 * Insert text at the current cursor position.
 *
 * @param text The text to insert.
 */

void CMultiLineTextBox::insertTextAtCursor(const CNxString &text)
{
  insertText(text, getCursorPosition());
}

/**
 * Handle a keyboard press event.
 *
 * @param e The event data.
 */

void CMultiLineTextBox::handleKeyPressEvent(const CWidgetEventArgs &e)
{
  nxwidget_char_t key = e.getKey();

  if (key == KEY_CODE_BACKSPACE)
    {
      // Delete character in front of cursor

      if (m_cursorPos > 0)
        {
          removeText(m_cursorPos - 1, 1);
        }
    }
  else if (key != KEY_CODE_NONE)
    {
      // Not modifier; append value

      insertTextAtCursor(key);
    }
}

/**
 * Get the coordinates of the cursor relative to the text.
 *
 * @param x Will be populated with the x coordinate of the cursor.
 * @param y Will be populated with the y coordinate of the cursor.
 */

void CMultiLineTextBox::getCursorCoordinates(nxgl_coord_t &x, nxgl_coord_t &y) const
{
  unsigned int cursorRow = 0;

  x = 0;
  y = 0;

  // Only calculate the cursor position if the cursor isn't at the start
  // of the text

  if (m_cursorPos > 0)
    {
      // Calculate the row in which the cursor appears

      cursorRow = m_text->getLineContainingCharIndex(m_cursorPos);

      // Cursor line offset gives us the distance of the cursor from the
      // start of the line

      uint8_t cursorLineOffset = m_cursorPos - m_text->getLineStartIndex(cursorRow);

      CStringIterator *iterator = m_text->newStringIterator();
      iterator->moveTo(m_text->getLineStartIndex(cursorRow));

      // Sum the width of each char in the row to find the x coordinate

      for (int i = 0; i < cursorLineOffset; ++i)
        {
          x += getFont()->getCharWidth(iterator->getChar());
          iterator->moveToNext();
        }

      delete iterator;
    }

  // Add offset of row to calculated value

  x += getRowX(cursorRow);

  // Calculate y coordinate of the cursor

  y = getRowY(cursorRow);
}

/**
 * Gets the index of the character at the specified x coordinate in the
 * specified row.
 *
 * @param x X coordinate of the character.
 * @param rowIndex Index of the row containing the character.
 * @return The index of the character at the specified coordinate.
 */

int CMultiLineTextBox::getCharIndexAtCoordinate(nxgl_coord_t x, int rowIndex) const
{
  // Locate the character within the row

  int startIndex = m_text->getLineStartIndex(rowIndex);
  int stopIndex  = m_text->getLineLength(rowIndex);
  int width      = getRowX(rowIndex);
  int index      = -1;

  CStringIterator *iterator = m_text->newStringIterator();
  iterator->moveTo(startIndex);

  width += m_text->getFont()->getCharWidth(iterator->getChar());

  for (int i = 0; i < stopIndex; ++i)
    {
      if (width > x)
        {
          if (i == 0)
            {
              // If the coordinate is on the left of the text, we add nothing
              // to the index

              index = startIndex;
            }
          else
            {
              // Character within the row.
              // This is the character that contains the coordinate.

              index = startIndex + i;
            }
          break;
        }

      iterator->moveToNext();
      width += m_text->getFont()->getCharWidth(iterator->getChar());
    }

  delete iterator;

  // If the coordinate is past the last character, index will still be -1.
  // We need to set it to the last character

  if (index == -1)
    {
      if (rowIndex == m_text->getLineCount() - 1)
        {
          // Index past the end point of the text, so return an index
          // just past the text

          index = startIndex + stopIndex;
        }
      else
        {
          // Index at the end of a row, so return the last index of the
          // row

          index = startIndex + stopIndex - 1;
        }
    }

  return index;
}

/**
 * Get the index of the character at the specified coordinates.
 *
 * @param x X coordinate of the character.
 * @param y Y coordinate of the character.
 * @return The index of the character at the specified coordinates.
 */

unsigned int
CMultiLineTextBox::getCharIndexAtCoordinates(nxgl_coord_t x, nxgl_coord_t y) const
{
  int rowIndex = getRowContainingCoordinate(y);
  return getCharIndexAtCoordinate(x, rowIndex);
}

/**
 * Get the row containing the specified Y coordinate.
 *
 * @param y Y coordinate to locate.
 * @return The index of the row containing the specified Y coordinate.
 */

int CMultiLineTextBox::getRowContainingCoordinate(nxgl_coord_t y) const
{
  int row = -1;

  // Locate the row containing the character

  for (int i = 0; i < m_text->getLineCount(); ++i)
    {
      // Abort search if we've found the row below the y coordinate

      if (getRowY(i) > y)
        {
          if (i == 0)
            {
              // If the coordinate is above the text, we return the top
              // row

              row = 0;
            }
          else
            {
              // Row within the text, so return the previous row - this is
              // the row that contains the coordinate.

              row = i - 1;
            }

          break;
        }
    }

  // If the coordinate is below the text, row will still be -1.
  // We need to set it to the last row

  if (row == -1)
    {
      row = m_text->getLineCount() - 1;
    }

  return row;
}

/**
 * Draw the area of this widget that falls within the clipping region.
 * Called by the redraw() function to draw all visible regions.
 *
 * @param port The CGraphicsPort to draw to.
 * @see redraw()
 */

void CMultiLineTextBox::drawContents(CGraphicsPort *port)
{
  drawText(port);

  // Draw the cursor

  drawCursor(port);
}

/**
 * Draw the area of this widget that falls within the clipping region.
 * Called by the redraw() function to draw all visible regions.
 *
 * @param port The CGraphicsPort to draw to.
 * @see redraw()
 */

void CMultiLineTextBox::drawBorder(CGraphicsPort *port)
{
  port->drawFilledRect(getX(), getY(), getWidth(), getHeight(),
                       getBackgroundColor());

  // Stop drawing if the widget indicates it should not have an outline

  if (!isBorderless())
    {
      port->drawBevelledRect(getX(), getY(), getWidth(), getHeight(),
                             getShadowEdgeColor(), getShineEdgeColor());
    }
}

/**
 * Move cursor one character to the left.
 */

void CMultiLineTextBox::moveCursorLeft(void)
{
  if (m_cursorPos > 0)
    {
      moveCursorToPosition(m_cursorPos - 1);
    }

  jumpToCursor();
}

/**
 * Move cursor one character to the right.
 */

void CMultiLineTextBox::moveCursorRight(void)
{
  if (m_cursorPos < (int)m_text->getLength())
    {
      moveCursorToPosition(m_cursorPos + 1);
    }

  jumpToCursor();
}

/**
 * Move cursor one row upwards.
 */

void CMultiLineTextBox::moveCursorUp(void)
{
  nxgl_coord_t cursorX = 0;
  nxgl_coord_t cursorY = 0;

  getCursorCoordinates(cursorX, cursorY);

  // Get the midpoint of the cursor.  We use the midpoint to ensure that
  // the cursor does not drift off to the left as it moves up the text, which
  // is a problem when we use the left edge as the reference point when the
  // font is proportional

  cursorX += m_text->getFont()->getCharWidth(m_text->getCharAt(m_cursorPos)) >> 1;

  // Locate the character above the midpoint

  int index = getCharIndexAtCoordinates(cursorX, cursorY + m_text->getLineHeight());
  moveCursorToPosition(index);
  jumpToCursor();
}

/**
 * Move cursor one row downwards.
 */

void CMultiLineTextBox::moveCursorDown(void)
{
  nxgl_coord_t cursorX = 0;
  nxgl_coord_t cursorY = 0;

  getCursorCoordinates(cursorX, cursorY);

  // Get the midpoint of the cursor.  We use the midpoint to ensure that
  // the cursor does not drift off to the left as it moves up the text, which
  // is a problem when we use the left edge as the reference point when the
  // font is proportional

  cursorX += m_text->getFont()->getCharWidth(m_text->getCharAt(m_cursorPos)) >> 1;

  // Locate the character above the midpoint

  int index = getCharIndexAtCoordinates(cursorX, cursorY - m_text->getLineHeight());
  moveCursorToPosition(index);
  jumpToCursor();
}

/**
 * Ensures that the textbox only contains the maximum allowed
 * number of rows by culling any excess rows from the top of
 * the text.
 *
 * @return True if lines were removed from the text; false if not.
 */

bool CMultiLineTextBox::cullTopLines(void)
{
  // Ensure that we have the correct number of rows

  if (m_text->getLineCount() > m_maxRows)
    {
      m_text->stripTopLines(m_text->getLineCount() - m_maxRows);
      return true;
    }

  return false;
}

/**
 * Ensures that the canvas height is the height of the widget,
 * if the widget exceeds the size of the text, or the height of
 * the text if the text exceeds the size of the widget.
 */

void CMultiLineTextBox::limitCanvasHeight(void)
{
  m_canvasHeight = m_text->getPixelHeight();

  CRect rect;
  getClientRect(rect);
  if (m_canvasHeight < rect.getHeight())
    {
      m_canvasHeight = rect.getHeight();
    }
}

/**
 * Ensures that the canvas cannot scroll beyond its height.
 */

void CMultiLineTextBox::limitCanvasY(void)
{
  CRect rect;
  getClientRect(rect);

  // Ensure that the visible portion of the canvas is not less than the
  // height of the viewer window

  if (m_canvasY + m_canvasHeight < rect.getHeight())
    {
      jumpToTextBottom();
    }
}

/**
 * Jumps to the cursor coordinates of the text.
 */

void CMultiLineTextBox::jumpToCursor(void)
{
  // Get the co-odinates of the cursor

  nxgl_coord_t cursorX;
  nxgl_coord_t cursorY;

  getCursorCoordinates(cursorX, cursorY);

  // Work out which row the cursor falls within

  int cursorRow = m_text->getLineContainingCharIndex(m_cursorPos);
  nxgl_coord_t rowY = getRowY(cursorRow);

  // If the cursor is outside the visible portion of the canvas, jump to it

  CRect rect;
  getClientRect(rect);

  if (rowY + m_text->getLineHeight() + m_canvasY > rect.getHeight())
    {
      // Cursor is below the visible portion of the canvas, so
      // jump down so that the cursor's row is the bottom row of
      // text

      jump(0, -(rowY + m_text->getLineHeight() - rect.getHeight()));
    }
  else if (rowY + m_canvasY < 0)
    {
      // Cursor is above the visible portion of the canvas, so
      // jump up so that the cursor's row is the top row of text

      jump(0, -cursorY);
    }
}

/**
 * Jumps to the bottom of the text.
 */

void CMultiLineTextBox::jumpToTextBottom(void)
{
  CRect rect;
  getClientRect(rect);
  jump(0, -(m_canvasHeight - rect.getHeight()));
}

/**
 * Resize the textbox to the new dimensions.
 *
 * @param width The new width.
 * @param height The new height.
 */

void CMultiLineTextBox::onResize(nxgl_coord_t width, nxgl_coord_t height)
{
  // Ensure the base class resize method is called

  CScrollingPanel::onResize(width, height);

  // Resize the canvas' width

  CRect rect;
  getClientRect(rect);

  m_canvasWidth  = rect.getWidth();
  m_canvasHeight = rect.getHeight();
  m_canvasX      = 0;
  m_canvasY      = 0;

  calculateVisibleRows();

  // Re-wrap the text

  m_text->setWidth(getWidth());
  m_text->wrap();

  bool raiseEvent = cullTopLines();
  limitCanvasHeight();
  limitCanvasY();

  if (raiseEvent)
    {
      m_widgetEventHandlers->raiseValueChangeEvent();
    }
}

/**
 * Starts the dragging system.
 *
 * @param x The x coordinate of the click.
 * @param y The y coordinate of the click.
 */

void CMultiLineTextBox::onClick(nxgl_coord_t x, nxgl_coord_t y)
{
  startDragging(x, y);

  // Move cursor to clicked coordinates

  CRect rect;
  getClientRect(rect);

  // Adjust x and y from screen coordinates to canvas coordinates

  nxgl_coord_t canvasRelativeX = x - getX() - rect.getX() - m_canvasX;
  nxgl_coord_t canvasRelativeY = y - getY() - rect.getY() - m_canvasY;

  moveCursorToPosition(getCharIndexAtCoordinates(canvasRelativeX, canvasRelativeY));
}

/**
 * Opens the keyboard on the bottom display.
 *
 * @param x The x coordinates of the click.
 * @param y The y coordinates of the click.
 */

void CMultiLineTextBox::onDoubleClick(nxgl_coord_t x, nxgl_coord_t y)
{
}

/**
 * Handles cursor control events.  Moves the cursor
 * in the direction selected.
 *
 * @param key The key that was pressed.
 */

void CMultiLineTextBox::handleCursorControlEvent(const CWidgetEventArgs &e)
{
  ECursorControl control = e.getCursorControl();

  switch (control)
    {
      case CURSOR_LEFT:
        moveCursorLeft();
        break;

      case CURSOR_RIGHT:
        moveCursorRight();
        break;

      case CURSOR_UP:
        moveCursorDown();
        break;

      case CURSOR_DOWN:
        moveCursorUp();
        break;

      default:
        // Not interested in other controls

        break;
    }
}

/**
 * Handle a keyboard press event.
 *
 * @param e The event data.
 */

    void handleKeyPressEvent(const CWidgetEventArgs &e);

/**
 * Gets the x position of a row of text based on the width of the row and the
 * type of horizontal alignment currently set.
 *
 * @param row The index of the row.
 * @return The x coordinate of the row.
 */

nxgl_coord_t CMultiLineTextBox::getRowX(int row) const
{
  CRect rect;
  getClientRect(rect);

  uint8_t rowLength = m_text->getLineTrimmedLength(row);
  uint8_t rowPixelWidth = m_text->getFont()->getStringWidth(*m_text, m_text->getLineStartIndex(row), rowLength);

  // Calculate horizontal position

  switch (m_hAlignment)
    {
      case TEXT_ALIGNMENT_HORIZ_CENTER:
        return (rect.getWidth() - rowPixelWidth) >> 1;

      case TEXT_ALIGNMENT_HORIZ_LEFT:
        return 0;

      case TEXT_ALIGNMENT_HORIZ_RIGHT:
        return rect.getWidth() - rowPixelWidth;
    }

  // Will never be reached

  return 0;
}

/**
 * Gets the y position of the specified row of text based on the type of
 * vertical alignment currently set.
 *
 * @param row The row number to find the y coordinate of.
 * @return The y coordinate of the specified row of text.
 */

nxgl_coord_t CMultiLineTextBox::getRowY(int row) const
{
  // If the amount of text exceeds the size of the widget, force
  // the text to be top-aligned

  if (m_visibleRows <= m_text->getLineCount())
    {
      return row * m_text->getLineHeight();
    }

  // All text falls within the textbox, so obey the alignment
  // options

  nxgl_coord_t textY    = 0;
  nxgl_coord_t startPos = 0;

  int canvasRows        = 0;
  int textRows          = 0;

  CRect rect;
  getClientRect(rect);

  // Calculate vertical position

  switch (m_vAlignment)
    {
      case TEXT_ALIGNMENT_VERT_CENTER:

        // Calculate the maximum number of rows

        canvasRows = m_canvasHeight / m_text->getLineHeight();
        textY = row * m_text->getLineHeight();

        // Get the number of rows of text

        textRows = m_text->getLineCount();

        // Ensure there's always one row

        if (textRows == 0)
          {
            textRows = 1;
          }

        // Calculate the start position of the block of text

        startPos = ((canvasRows - textRows) * m_text->getLineHeight()) >> 1;

        // Calculate the row Y coordinate

        textY = startPos + textY;
        break;

      case TEXT_ALIGNMENT_VERT_TOP:
        textY = row * m_text->getLineHeight();
        break;

      case TEXT_ALIGNMENT_VERT_BOTTOM:
        textY = rect.getHeight() - (((m_text->getLineCount() - row) * m_text->getLineHeight()));
        break;
    }

  return textY;
}

/**
 * Return true if the cursor is visible
 */

bool CMultiLineTextBox::isCursorVisible(void) const
{
  return (m_showCursor == SHOW_CURSOR_ALWAYS ||
         (m_showCursor == SHOW_CURSOR_ONFOCUS && hasFocus()));
}

/**
 * Gets the character under the cursor.
 *
 * @return The character under the cursor.
 */

nxwidget_char_t CMultiLineTextBox::getCursorChar(void) const
{
  if (m_cursorPos < (int)m_text->getLength())
    {
      return m_text->getCharAt(m_cursorPos);
    }
  else
    {
      return ' ';
    }
}

/**
 * Works out the number of visible rows within the textbox.
 */

void CMultiLineTextBox::calculateVisibleRows(void)
{
  CRect rect;
  getClientRect(rect);

  m_visibleRows = rect.getHeight() / m_text->getLineHeight();
}

/**
 * Draws text.
 *
 * @param port The CGraphicsPort to draw to.
 */

void CMultiLineTextBox::drawText(CGraphicsPort *port)
{
  // Early exit if there is no text to display

  if (m_text->getLineCount() == 0)
    {
      return;
    }

  // Determine the top and bottom rows within the graphicsport's clip rect.
  // We only draw these rows in order to increase the speed of the routine.

  int regionY   = -m_canvasY;  // Y coord of canvas visible region
  int topRow    = getRowContainingCoordinate(regionY);
  int bottomRow = getRowContainingCoordinate(regionY + m_rect.getHeight());

  // Early exit checks

  if ((topRow < 0) && (bottomRow < 0))
    {
      return;
    }

  if ((bottomRow >= m_text->getLineCount()) && (topRow >= m_text->getLineCount()))
    {
      return;
    }

  // Prevent overflows

  if (topRow < 0)
    {
      topRow = 0;
    }

  if (bottomRow >= m_text->getLineCount())
    {
      bottomRow = m_text->getLineCount() - 1;
    }

  // Draw lines of text

  int currentRow = topRow;

  // Draw all rows in this region

  while (currentRow <= bottomRow)
    {
      drawRow(port, currentRow);
      currentRow++;
    }
}

/**
 * Draws the cursor.
 *
 * @param port The CGraphicsPort to draw to.
 */

void CMultiLineTextBox::drawCursor(CGraphicsPort *port)
{
  // Get the cursor coordinates

  if (isCursorVisible())
    {
      nxgl_coord_t cursorX = 0;
      nxgl_coord_t cursorY = 0;

      getCursorCoordinates(cursorX, cursorY);

      // Adjust for canvas offsets

      cursorX += m_canvasX;
      cursorY += m_canvasY;

      // Draw cursor

      port->invert(cursorX, cursorY,
                   m_text->getFont()->getCharWidth( getCursorChar()),
                   m_text->getFont()->getHeight());
    }
}

/**
 * Draws a single line of text.
 *
 * @param port The CGraphicsPort to draw to.
 * @param row The index of the row to draw.
 */

void CMultiLineTextBox::drawRow(CGraphicsPort *port, int row)
{
  // Get the drawing region

  CRect rect;
  getRect(rect);

  uint8_t rowLength = m_text->getLineTrimmedLength(row);

  struct nxgl_point_s pos;
  pos.x = getRowX(row) + m_canvasX + rect.getX();
  pos.y = getRowY(row) + m_canvasY + rect.getY();

  // Determine the background and text color

  nxgl_mxpixel_t textColor;

  if (!isEnabled())
    {
      textColor = getDisabledTextColor();
    }
  else
    {
      textColor = getEnabledTextColor();
    }

  // And draw the text using the selected color

  port->drawText(&pos, &rect, m_text->getFont(), *m_text,
                 m_text->getLineStartIndex(row), rowLength, textColor);
}
