ginebra2/EditorWidget.cpp
author hgs
Fri, 06 Aug 2010 17:23:08 -0400
changeset 9 b39122337a00
parent 7 a1f515018ac1
child 12 afcd8e6d025b
permissions -rw-r--r--
201031

/*
 * Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
 * All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, version 2.1 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not,
 * see "http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html/".
 *
 * Description:
 *
 */
#include <QIcon>

#ifdef ORBIT_UI
#include <hbaction.h>
#include <hbicon.h>
#include <hbinputmethod.h>
#include <hbinputeditorinterface.h>
#include <hbinputsettingproxy.h>
#endif

#include "EditorWidget.h"
#include "Utilities.h"
#include "ChromeEffect.h"

// FIXME ;;; Must address the following issues:
//
// * Clean ORBIT_UI flag by creating new class for orbit ui
// * Replace QGraphicsTextItem with HbTextItem for orbit ui
//

namespace GVA {

  // Methods for class EditorItem

  //GTextLineItem extends QGraphicsTextItem as a single-line editor.
  //Signals horizontal cursor movement, which can be used to implement horizontal scrolling.

  GTextLineItem::GTextLineItem(QGraphicsItem * parent)
  : QGraphicsTextItem(parent)
  , m_maxTextLength(0)
  , m_hints(Qt::ImhNoPredictiveText) // disable the predictive text
  , m_setSpecificBtn(false)
  {
    // Disable wrapping, force text to be stored and displayed
    // as a single line.
    QTextOption textOption = document()->defaultTextOption();
    textOption.setWrapMode(QTextOption::NoWrap);
    document()->setDefaultTextOption(textOption);
    // Enable cursor keys.
    setTextInteractionFlags(Qt::TextEditorInteraction);
    // This is needed to initialize m_textLine.
    setText("");
    setAcceptDrops(false);
    m_defaultStartDragDistance = QApplication::startDragDistance();
  }

  GTextLineItem::~GTextLineItem()
  {
    if (m_maxTextLength > 0)
      disconnect(document(), SIGNAL(contentsChange(int, int, int)), this, SLOT(contentsChange(int, int, int)));
    QApplication::setStartDragDistance(m_defaultStartDragDistance);
  }

  void GTextLineItem::setText(const QString & text)
  {
    QString newText = text;
    if(m_maxTextLength > 0 && text.length() > m_maxTextLength ) {
        newText = newText.left(m_maxTextLength);
    }
    setPlainText(newText);
    m_textLine = document()->begin().layout()->lineForTextPosition(0);
  }

  void GTextLineItem::setMaxTextLength(int length)
  {
    if (m_maxTextLength <= 0 && length > 0) {
        QString text = toPlainText();
        if( text.length() > length ) {
          setPlainText(text.left(length));
        }
        connect(document(), SIGNAL(contentsChange(int, int, int)), this, SLOT(contentsChange(int, int, int)));
    } else if (m_maxTextLength > 0 && length <= 0) {
        disconnect(document(), SIGNAL(contentsChange(int, int, int)), this, SLOT(contentsChange(int, int, int)));
    }
    m_maxTextLength = length;
  }
  
  void GTextLineItem::setSpecificButton(QString& commitString, QString& buttonIconPath) {
    m_spBtnCommitString = commitString;
    m_spBtnIconPath = buttonIconPath;
    m_setSpecificBtn = true;
  }

  void GTextLineItem::contentsChange(int position, int charsRemoved, int charsAdded)
  {
    Q_UNUSED(position);
    Q_UNUSED(charsRemoved);
    Q_UNUSED(charsAdded);
    if( m_maxTextLength > 0 && toPlainText().length() > m_maxTextLength ) {
      QTextCursor cursor = textCursor();
      cursor.deletePreviousChar();
      setTextCursor(cursor);
    }
  }

  void GTextLineItem::specificBtnTriggered(bool checked)
  {
#ifdef ORBIT_UI
    QInputContext *ic = qApp->inputContext();
    QInputMethodEvent *imEvent = new QInputMethodEvent();
    imEvent->setCommitString(QString(m_spBtnCommitString));
    if (ic)
      ic->sendEvent(*imEvent);
    delete imEvent;
#endif
  }

  // Get the pixel offset of the cursor. Needed to implement scrolling.

  qreal GTextLineItem::cursorX()
  {
    return m_textLine.cursorToX(textCursor().position());
  }

  qreal GTextLineItem::anchorX()
  {
    return m_textLine.cursorToX(textCursor().anchor());
  }

  QRectF GTextLineItem::selectionRectF()
  {
    qreal x1 = cursorX();
    qreal x2 = anchorX();
    if (x1 == x2)
      return QRectF();
    return QRectF(x1, 0, x2-x1 + 16, boundingRect().height());
  }

  void GTextLineItem::selectAll()
  {
    QTextCursor tc = textCursor();
    tc.select(QTextCursor::Document);
    setTextCursor(tc);
    emit cursorXChanged(cursorX());
  }

  void GTextLineItem::unselect()
  {
    QTextCursor tc = textCursor();
    tc.setPosition(tc.position());
    setTextCursor(tc);
  }

  void GTextLineItem::setCursorPosition(int pos)
  {
    QTextCursor tc = textCursor();
    tc.setPosition(pos);
    setTextCursor(tc);
  }

  qreal GTextLineItem::textWidth()
  {
    return m_textLine.naturalTextWidth();
  }

  void GTextLineItem::paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget)
  {
    // Paint without ugly selection ants (the dashed line that surrounds
    // the selected text). TODO: It is clearly a bug in QGraphicsTextItem
    // that you cannot explicitly set the style of the selection indicator.  File this.
    QStyleOptionGraphicsItem newOption = *option;
    newOption.state &= (!QStyle::State_Selected | !QStyle::State_HasFocus);
    painter->save();
    painter->setRenderHint(QPainter::Antialiasing);
    QGraphicsTextItem::paint(painter, &newOption, widget);
    painter->restore();
    if(!isEnabled()) {
        ChromeEffect::paintDisabledRect(painter, option->exposedRect);
    }
  }

  void GTextLineItem::mouseMoveEvent(QGraphicsSceneMouseEvent * event)
  {
    QGraphicsTextItem::mouseMoveEvent(event);
    emit cursorXChanged(cursorX());
  }

  void GTextLineItem::mousePressEvent(QGraphicsSceneMouseEvent * event)
  {
    QGraphicsTextItem::mousePressEvent(event);
  }

  void GTextLineItem::mouseReleaseEvent(QGraphicsSceneMouseEvent * event)
  {
    QGraphicsTextItem::mouseReleaseEvent(event);
    QPointF pos = event->pos();
    emit tapped(pos);

    // add specific button
#ifdef ORBIT_UI
    if (m_setSpecificBtn) { 
      HbEditorInterface editorInterface(this);
      QList<HbAction*> actionsList = editorInterface.actions();
      for (int i = 0; i < actionsList.size(); ++i) {
        editorInterface.removeAction(actionsList.at(i));
      }
      QIcon icon(m_spBtnIconPath);
      HbAction* action = new HbAction(HbIcon(icon), QString(""), this);
      connect(action, SIGNAL(triggered(bool)), this, SLOT(specificBtnTriggered(bool)));

      editorInterface.addAction(action);
      m_setSpecificBtn = false;
    }
#endif

    // open vkb by single tap
    QWidget * widget = event->widget();
    QEvent vkbEvent(QEvent::RequestSoftwareInputPanel);
    QApplication::sendEvent(widget, &vkbEvent);

    // disable prediction
#ifdef ORBIT_UI
    HbInputSettingProxy* hbISProxy = HbInputSettingProxy::instance();
    if (hbISProxy->predictiveInputStatusForActiveKeyboard())
      hbISProxy->togglePrediction();
#endif
  }

  void GTextLineItem::keyPressEvent(QKeyEvent * event)
  {
    // Signal horizontal cursor movement so that an editor widget can
    // implement horizontal scrolling.
    qreal oldX = cursorX();
    QGraphicsTextItem::keyPressEvent(event);
    qreal newX = cursorX();
    if (newX != oldX) {
      emit cursorXChanged(newX);
    }
  }

  void GTextLineItem::keyReleaseEvent(QKeyEvent * event)
  {
    QGraphicsTextItem::keyReleaseEvent(event);
    emit textMayChanged();
  }

  void GTextLineItem::focusInEvent(QFocusEvent * event)
  {
    Q_UNUSED(event);
    // disable the drag & drop to fix the auto-delete-all issue
    QApplication::setStartDragDistance(1000);
    QGraphicsTextItem::focusInEvent(event);

#ifndef ORBIT_UI
    QWidget* fw = QApplication::focusWidget();
    Qt::InputMethodHints hints = fw->inputMethodHints();
    if (hints != m_hints)
      fw->setInputMethodHints(m_hints);
#endif

    if (event->reason() != Qt::PopupFocusReason) // to fix the special char issue on VKB
      emit focusChanged(true);
  }

  void GTextLineItem::focusOutEvent(QFocusEvent * event)
  {
    Q_UNUSED(event);
    // restore the drag & drop for other components
    QApplication::setStartDragDistance(m_defaultStartDragDistance);
    QGraphicsTextItem::focusOutEvent(event);
    if (event->reason() != Qt::PopupFocusReason && event->reason() != Qt::ActiveWindowFocusReason) // to fix the special char issue on VKB
      emit focusChanged(false);
  }

  void GTextLineItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
  {
    // ignore context menu event
    event->ignore();
  }

  void GTextLineItem::setInputMethodHints(Qt::InputMethodHints hints)
  { 
    m_hints = hints;
    m_hints |= Qt::ImhNoPredictiveText;  // disable the predictive text
  }

  // Methods for class GLineEditor
  // GLineEditor is a QGraphicsWidget that wraps a GTextLineItem to implement scrolling, 
  // draw a background and set padding 

  GLineEditor::GLineEditor(ChromeSnippet * snippet, ChromeWidget * chrome, QGraphicsItem * parent)
  : QGraphicsWidget(parent)
  , m_chrome(chrome)
  , m_viewPortWidth(0.0)
  , m_viewPortHeight(0.0)
  , m_padding(0.0)
  , m_rightTextMargin(0.0)
  {
    Q_UNUSED(snippet);

    // The viewport clips the editor when text overflows
    // viewport size will be set in resize()
    m_viewPort = new QGraphicsWidget(this);
    m_viewPort->setFlags(QGraphicsItem::ItemClipsChildrenToShape);

    // The actual text editor item
    m_editor = new GTextLineItem(m_viewPort);
    m_editor->setDefaultTextColor(m_textColor);
    m_editor->installEventFilter(this);

    // Monitor editor cursor position changes for horizontal scrolling.
    safe_connect(m_editor, SIGNAL(cursorXChanged(qreal)),
                 this, SLOT(makeVisible(qreal)));

    safe_connect(m_editor, SIGNAL(textMayChanged()),
                 this, SIGNAL(textMayChanged()));

    safe_connect(m_editor, SIGNAL(focusChanged(bool)),
                 this, SIGNAL(focusChanged(bool)));
    
    safe_connect(m_editor, SIGNAL(tapped(QPointF&)),
                 this, SIGNAL(tapped(QPointF&)));

    setAcceptDrops(false);
  }

  GLineEditor::~GLineEditor()
  {
  }

  // TODO: Be a good QGraphicsWidget: update this to use palette color?

  void GLineEditor::setTextColor(QColor & color)
  {
    m_textColor = color;
    m_editor->setDefaultTextColor(m_textColor);
  }

  void GLineEditor::setPadding(qreal padding)
  {
    m_padding = padding;
    //Trigger a resize to adjust component sizes to new padding
    resize(size());
  }

  void GLineEditor::setRightTextMargin(qreal margin)
  {
    m_rightTextMargin = margin;
    resize(size());
  }

  QString GLineEditor::text() const
  {
    return m_editor->toPlainText();
  }

  void GLineEditor::paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget)
  {
    Q_UNUSED(option);
    Q_UNUSED(widget);

    painter->save();
    painter->setRenderHint(QPainter::Antialiasing);
    // First, fill rectangle with background color.
    painter->fillRect(boundingRect(), m_backgroundColor);
    painter->restore();
    if(!isEnabled()) {
        ChromeEffect::paintDisabledRect(painter, option->exposedRect);
    }
    // Make sure any required horizontal scrolling happens
    // before rendering editor widget, which will be drawn on top
    // of the background rectangle.
    makeVisible(m_editor->cursorX());
  }

  void GLineEditor::resizeEvent(QGraphicsSceneResizeEvent * event)
  {
    QSizeF size = event->newSize();
    m_viewPortWidth  = size.width() - m_rightTextMargin  - m_padding * 2;
    m_viewPortHeight = size.height() - m_padding * 2;
    m_viewPort->setGeometry(
                            m_padding,
                            (size.height() - m_editor->boundingRect().height()) / 2,
                            m_viewPortWidth,
                            m_viewPortHeight);
    m_editor->setTextWidth(m_viewPortWidth);
    // move back the m_editor'x to 0
    qreal editorShift = -1 * m_editor->pos().x();
    m_editor->moveBy(editorShift, 0);
    updateEditor();
  }

  void GLineEditor::setText(const QString & text)
  {
    m_editor->setText(text);
    m_editor->setPos(0, m_editor->pos().y());
    updateEditor();
  }

  void GLineEditor::updateEditor()
  {
    makeVisible(m_editor->cursorX());
  }

  // We divide the viewport into 3 distinct regions:
  //
  //
  //        [ left | middle | right ]
  //
  // [ editor, shifted left by editorShift pixels ]
  //
  // When a cursor is in the middle section of the viewport we
  // leave the editor shift unchanged, to preserve stability.
  //
  // When a cursor is in the right section or beyond we shift
  // the editor left until the cursor appears at the border
  // between the middle and right sections.
  //
  // When a cursor is in the left section or beyond we shift
  // the editor right until the cursor appears at the border
  // between the left and middle sections.
  //
  // We never shift the editor right of the viewport.

  void GLineEditor::makeVisible(qreal cursorX)
  {
    qreal leftScrollBorder  = 0;
    qreal rightScrollBorder = m_viewPortWidth - 20;
    qreal editorShift = -1 * m_editor->pos().x();
    qreal localX = cursorX - editorShift;

    if (m_editor->textWidth() < rightScrollBorder) {
      if (editorShift !=0)
        m_editor->moveBy(editorShift, 0);
      return;
    }
    
    if (m_editor->textWidth() - editorShift < rightScrollBorder) {           
      if (editorShift != 0)
        m_editor->moveBy(rightScrollBorder - m_editor->textWidth() + editorShift, 0);
      return;
    }

    if (localX < leftScrollBorder) {
      // Before left section, scroll right.
      // In left section, scroll right.
      qreal shift = qMin(leftScrollBorder - localX, editorShift);
      m_editor->moveBy(shift, 0);
      return;
    }
    if (localX >= rightScrollBorder) {
      // In right section, scroll left.
      // After right section, scroll left.
      qreal shift = localX - rightScrollBorder;
      m_editor->moveBy(-shift, 0);
      return;
    }
    // In middle section, no scroll needed.
    return;
  }

  bool GLineEditor::tappedOnText(qreal x) const
  {
    qreal editorShift = m_editor->pos().x();
    qreal editorWidth = m_editor->textWidth();
    if (editorShift == 0 && editorWidth < x)
      return false;
    return true;
  }

  bool GLineEditor::eventFilter(QObject * object, QEvent * event)
  {
    // Filter editor key events.
    if (object != m_editor)
      return false;

    if (event->type() != QEvent::KeyPress)
      return false;

    QKeyEvent * keyEvent = static_cast<QKeyEvent*>(event);
    switch (keyEvent->key()) {
    case Qt::Key_Select:
    case Qt::Key_Return:
    case Qt::Key_Enter:
      // Signal that a carriage return-like key-press happened.
      emit activated();
      return true;
    case Qt::Key_Down:
    case Qt::Key_Up:
      // Swallow arrow up/down keys, editor has just one line.
      return true;
    default:
      return false;
    }
  }

  //GTextEditor paints a styled frame around a GLineEditor

  GTextEditor::GTextEditor(ChromeSnippet * snippet, ChromeWidget * chrome, QGraphicsItem * parent)
  : GLineEditor(snippet, chrome, parent)
  {
  }

  GTextEditor::~GTextEditor()
  {
  }

  void GTextEditor::paintBorder(QPainter * painter)
  {
    if (m_padding > 0 ) {
      QPainterPath border;
      //qDebug() << "GTextEditor::paintBorder: " << boundingRect().width() << " " << size().width();
      border.addRect(boundingRect());
      border.addRoundedRect(
                            m_padding,
                            m_padding,
                            size().width()-m_padding*2,
                            size().height()-m_padding*2,
                            4,
                            4);
      painter->fillPath(border, m_borderColor);
    }
  }

  void GTextEditor::paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget)
  {
    Q_UNUSED(option);
    Q_UNUSED(widget);

    //updateEditor();
    GLineEditor::paint(painter, option, widget);
    painter->save();
    painter->setRenderHint(QPainter::Antialiasing);
    paintBorder(painter);
    painter->restore();
  }

  //GProgressEditor adds a progress bar to GTextEditor. Joining these into a single QGraphicsWidget makes it
  //possible to draw a text editor with a progress bar with minimal updates

  GProgressEditor::GProgressEditor(ChromeSnippet * snippet, ChromeWidget * chrome, QGraphicsItem * parent)
  : GTextEditor(snippet, chrome, parent)
  , m_percent(0)
  {
  }

  GProgressEditor::~GProgressEditor()
  {
  }

  void GProgressEditor::paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget)
  {
    Q_UNUSED(option);
    Q_UNUSED(widget);

    updateEditor();

    QRectF progressRect = boundingRect();
    progressRect.setWidth(progressRect.width() * m_percent / 100.0);

    painter->save();
    painter->setRenderHint(QPainter::Antialiasing);
    painter->fillRect(boundingRect(), m_backgroundColor);
    painter->fillRect(progressRect, m_progressColor);
    paintBorder(painter);
    painter->restore();
    if(!isEnabled()) {
        ChromeEffect::paintDisabledRect(painter, option->exposedRect);
    }
  }

  void GProgressEditor::setProgress(int percent)
  {
    // Don't bother with small increments, but ...
    //
    // - ALWAYS show the final increment to 99 or 100, because
    //   this tells the user a network request has completed.
    //
    // - ALWAYS show the initial increment from 0, because this
    //   tells the user a new network request has started.
    //
    // - ALWAYS show decrements, because this tells the user
    //   a new network request has started.
    if (percent < 99) {
      if (m_percent > 0) {
        if (percent > m_percent) {
          if (percent - m_percent < 10) {
            //qDebug() << "UrlSearchSnippet::setProgress" << percent << "IGNORE";
            return;
          }
        }
      }
    }

    if (m_percent == percent) {
      //qDebug() << "UrlSearchSnippet::setProgress" << percent << "IGNORE";
      return;
    }

    //qDebug() << "UrlSearchSnippet::setProgress" << percent << "UPDATE";
    m_percent = percent;
    update();
  }

  //A chrome item that displays a GTextEditor. This can be embedded in HTML chrome instead of an input field.

  TextEditItem::TextEditItem(ChromeSnippet * snippet, ChromeWidget * chrome, QGraphicsItem * parent)
    : NativeChromeItem(snippet, parent)
	, m_justFocusIn(false)
  {
    m_textEditor = new GTextEditor(snippet, chrome, this);
  
    //Style via CSS
    QWebElement we = m_snippet->element();

    QColor textColor;
    NativeChromeItem::CSSToQColor(we.styleProperty("color", QWebElement::ComputedStyle),
                                  textColor);
    m_textEditor->setTextColor(textColor);

    //Background of text box 
    QColor backgroundColor;
    NativeChromeItem::CSSToQColor(we.styleProperty("background-color", QWebElement::ComputedStyle),
                                  backgroundColor); 
    m_textEditor->setBackgroundColor(backgroundColor);

    QColor borderColor;
    NativeChromeItem::CSSToQColor(we.styleProperty("border-top-color", QWebElement::ComputedStyle),
                                  borderColor);
    m_textEditor->setBorderColor(borderColor);

    //Padding sets the "border" width
    QString cssPadding = we.styleProperty("padding-top", QWebElement::ComputedStyle);
    m_textEditor->setPadding(cssPadding.remove("px").toInt());

	safe_connect(m_textEditor, SIGNAL(focusChanged(bool)),this, SLOT(focusChanged(bool)));
    safe_connect(m_textEditor, SIGNAL(tapped(QPointF&)),this, SLOT(tapped(QPointF&)));
}
  
  void TextEditItem::tapped(QPointF& pos)
{
    bool hitText = m_textEditor->tappedOnText(pos.x());
    if (!m_justFocusIn && !hitText)
        m_textEditor->unselect();

    if (m_justFocusIn) {
        m_justFocusIn = false;
        if (hitText && !m_textEditor->hasSelection())
            m_textEditor->selectAll();
    }
}

void TextEditItem::focusChanged(bool focusIn)
{
    if (focusIn)
        m_justFocusIn = true;
    else {
        m_justFocusIn = false;
        m_textEditor->unselect();
        m_textEditor->shiftToLeftEnd();
    }
}

  TextEditItem::~TextEditItem()
  {
    delete m_textEditor;
  }

  void TextEditItem::resizeEvent(QGraphicsSceneResizeEvent * ev)
  {
    m_textEditor->resize(ev->newSize());
  }
  
  void TextEditItem::setTextOptions(int flag)
  {
    m_textEditor->setInputMethodHints((Qt::InputMethodHints)flag);
  }

} // namespace GVA