src/hbcore/primitives/hbrichtextitem.cpp
author Dremov Kirill (Nokia-D-MSW/Tampere) <kirill.dremov@nokia.com>
Fri, 17 Sep 2010 08:32:10 +0300
changeset 28 b7da29130b0e
parent 23 e6ad4ef83b23
permissions -rw-r--r--
Revision: 201035 Kit: 201037

/****************************************************************************
**
** Copyright (C) 2008-2010 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (developer.feedback@nokia.com)
**
** This file is part of the HbCore module of the UI Extensions for Mobile.
**
** GNU Lesser General Public License Usage
** This file may be used under the terms of the GNU Lesser General Public
** License version 2.1 as published by the Free Software Foundation and
** appearing in the file LICENSE.LGPL included in the packaging of this file.
** Please review the following information to ensure the GNU Lesser General
** Public License version 2.1 requirements will be met:
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights.  These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** If you have questions regarding the use of this file, please contact
** Nokia at developer.feedback@nokia.com.
**
****************************************************************************/

#include "hbrichtextitem.h"
#include "hbrichtextitem_p.h"
#include "hbtextutils_p.h"
#include "hbcolorscheme.h"
#include "hbevent.h"

#include <QTextDocument>
#include <QStyle>
#include <QGraphicsSceneResizeEvent>
#include <QTextBlock>
#include <QTextLayout>
#include <QPainter>
#include <QAbstractTextDocumentLayout>
#include <QApplication>
#include <QWidget>
#include <QStyleOptionGraphicsItem>

const qreal KMinimumRichTextWidth = 6.0;

static const QString KDefaultColorThemeName = "qtc_view_normal";

HbRichTextItemPrivate::HbRichTextItemPrivate() :
        mTextOption(Qt::AlignLeft|Qt::AlignVCenter),
        mRtf(0)
{
}

HbRichTextItemPrivate::~HbRichTextItemPrivate()
{
}

/*
 * private constructor
 */
void HbRichTextItemPrivate::init()
{
    Q_Q(HbRichTextItem);

    q->setFlag(QGraphicsItem::ItemClipsToShape, true);
    q->setFlag(QGraphicsItem::ItemIsSelectable, false);
    q->setFlag(QGraphicsItem::ItemIsFocusable,  false);

    mRtf = new QTextDocument(q);
    mRtf->setDocumentMargin(0.0); // no margins needed

    mTextOption.setWrapMode(QTextOption::NoWrap);
    mRtf->setDefaultTextOption(mTextOption);

    mRtf->setDefaultFont(q->font());
}

void HbRichTextItemPrivate::clear()
{
}

void HbRichTextItemPrivate::setDocumentWidth(qreal newWidth)
{
    if (!qFuzzyCompare(mRtf->textWidth(), newWidth)) {
        mRtf->setTextWidth(newWidth);
        calculateOffset();
    }
}

// #define HB_RICH_TEXT_ITEM_ALWAS_SHOW_FIRST_LINE
void HbRichTextItemPrivate::calculateOffset()
{
    Q_Q(HbRichTextItem);

    qreal diff;
    Qt::Alignment align = mTextOption.alignment();
    if (align.testFlag(Qt::AlignTop)) {
        diff = 0.0;
    } else {
        diff = q->geometry().height() - mRtf->size().height();
        if (!align.testFlag(Qt::AlignBottom)) {
            // default align Qt::AlignVCenter if no flags are set
            diff*=0.5;
        }
    }
#ifdef HB_RICH_TEXT_ITEM_ALWAS_SHOW_FIRST_LINE
    diff = qMax(diff, (qreal)0.0);
#endif

    if (diff!=mOffset.y()) {
        mOffset.setY(diff);
        q->prepareGeometryChange();
    }
}

QSizeF HbRichTextItemPrivate::minimumSizeHint(const QSizeF &/*constraint*/) const
{
    QSizeF result(KMinimumRichTextWidth, 0);

    QTextBlock textBlock = mRtf->begin();
    if (textBlock.isValid() && textBlock.layout()->lineCount() > 0) {
        QTextLine line = textBlock.layout()->lineAt(0);
        result.setHeight(line.height());
    } else {
        QFontMetricsF metrics(mRtf->defaultFont());
        result.setHeight(metrics.height());
    }

    qreal doubleDocMargin = mRtf->documentMargin() * (qreal)2.0;
    result.rheight() += doubleDocMargin;
    result.rwidth() += doubleDocMargin;

    return result;
}

void HbRichTextItemPrivate::clearPrefSizeCache()
{
    mPrefSize.setWidth(-1);
    mMinWidthForAdjust = QWIDGETSIZE_MAX;
    mMaxWidthForAdjust = -1;
}

QSizeF HbRichTextItemPrivate::preferredSizeHint(const QSizeF &constraint) const
{
    if (mPrefSizeConstraint==constraint && mPrefSize.isValid()) {
        return mPrefSize;
    }
    mPrefSizeConstraint=constraint;

    QSizeF result;

    if (constraint.width()<=0) {
        mRtf->setTextWidth(QWIDGETSIZE_MAX);
    } else {
        QTextOption::WrapMode wrapMode = mTextOption.wrapMode();
        // optimization when there is no automatic wrap there is no reason
        // to setTextWidth with width constraint (width measure is not needed)
        if (wrapMode!=QTextOption::NoWrap
            && wrapMode!=QTextOption::ManualWrap) {
            mRtf->setTextWidth(constraint.width());
        }
    }
    result = mRtf->size();
    mMaxWidthForAdjust = result.width();
    result.setWidth(mRtf->idealWidth());
    mMinWidthForAdjust = result.width();
    mDefaultPrefHeight = result.height();
    mPrefSize = result;

    return result;
}

bool HbRichTextItemPrivate::isAdjustHeightNeeded(qreal newWidth,
                                                 qreal prefHeight,
                                                 qreal minHeight,
                                                 qreal maxHeight)
{
    // first check if wrapping of text is not active
    QTextOption::WrapMode wrapMode = mTextOption.wrapMode();
    if (wrapMode==QTextOption::NoWrap
        || wrapMode==QTextOption::ManualWrap) {
        return false;
    }

    // preferred height was set from outside of this class so there is mo reason to adjust it
    if (mPrefSizeConstraint.height()>0) {
        return false;
    }

    // check if adjusted size has been already calculated
    // new width is bigger than last estimated range of same height
    if ((mMaxWidthForAdjust>=newWidth
         // new width is smaller than last estimated range of same height
         && newWidth>=mMinWidthForAdjust)) {
        return false;
    }

    if (!mPrefSize.isValid()) {
        // this means that preferred size is set outside of class by setPreferredSize
        // so sizeHint(Qt::Preferredsize) was not called and size adjustment is useless
        return false;
    }

    // new text width was set in setGeometry here it is not needed
    Q_ASSERT_X(qFuzzyCompare(mRtf->textWidth(), newWidth),
               "HbRichTextItemPrivate::isAdjustHeightNeeded",
               QString("mRtf->textWidth()=%1, newWidth=%2")
                    .arg(mRtf->textWidth())
                    .arg(newWidth).toAscii().data());

    // if preconditions are met test if current height is enough
    QSizeF newAdjust = mRtf->size();

    if (qFuzzyCompare(newAdjust.height(), mPrefSize.height())) {
        // height is same as last time update range of same height
        mMaxWidthForAdjust = qMax(mMaxWidthForAdjust, newWidth);
        mMinWidthForAdjust = qMin(mMinWidthForAdjust, newWidth);
        return false;
    }

    // new height was calculated create new range for which
    // current mPrefSize.height is valid
    mMaxWidthForAdjust = newWidth;
    mMinWidthForAdjust = mRtf->idealWidth();

    // store new height, don't change width
    mPrefSize.setHeight(newAdjust.height());

    Q_ASSERT_X(mPrefSizeConstraint.width()>0 || mPrefSize.width()>=mMinWidthForAdjust,
               "HbRichTextItemPrivate::isAdjustHeightNeeded",
               QString("Fail for (%1<%2) string: \"%3\"")
               .arg(mPrefSize.width())
               .arg(mMinWidthForAdjust)
               .arg(mText).toAscii().data());

    if (qFuzzyCompare(qBound(minHeight, mPrefSize.height(), maxHeight),
                      prefHeight)) {
        // updateGeometry has no effect
        return false;
    }

    // all conditions for calling updateGeometry are meet
    return true;
}

/*
    returns true if updateGeometry is needed
 */
bool HbRichTextItemPrivate::restoreDefaultHeightHint()
{
    if (mPrefSizeConstraint.height()>0) {
        return false;
    }

    if (mPrefSizeConstraint.width()>0) {
        clearPrefSizeCache();
        return true;
    }

    if (mPrefSize.height()==mDefaultPrefHeight) {
        return true;
    }

    mPrefSize.setHeight(mDefaultPrefHeight);
    mMinWidthForAdjust = mPrefSize.width();
    mMaxWidthForAdjust = QWIDGETSIZE_MAX;
    return true;
}

/*!
  @proto
  @hbcore
 \class HbRichTextItem
 \brief HbRichTextItem is a item for showing formatted text.


 This is mainly used as a primitive in widgets.
 It derives from HbWidgetBase so it can be layouted.

 */

/*!
    Constructor for the class.
 */

HbRichTextItem::HbRichTextItem(QGraphicsItem *parent) :
        HbWidgetBase(*new HbRichTextItemPrivate, parent)
{
    Q_D(HbRichTextItem);
    d->init();
}

/*!
    Constructor which set content using \a html format.
 */
HbRichTextItem::HbRichTextItem(const QString &html, QGraphicsItem *parent) :
        HbWidgetBase(*new HbRichTextItemPrivate, parent)
{
    Q_D(HbRichTextItem);
    d->init();
    setText(html);
}

/*
    Constructor for internal use only
 */
HbRichTextItem::HbRichTextItem(HbRichTextItemPrivate &dd, QGraphicsItem *parent) :
        HbWidgetBase(dd, parent)
{
    Q_D(HbRichTextItem);
    d->init();
}

/*!
    Destructor for the class.
 */
HbRichTextItem::~HbRichTextItem()
{
    Q_D(HbRichTextItem);
    d->clear();
}

/*!
    Sets the \a text in html format.

    \sa HbRichTextItem::text()
 */
void HbRichTextItem::setText(const QString &text)
{
    Q_D(HbRichTextItem);
    if (d->mText != text) {
        d->mText = text;
        d->mRtf->setHtml(text);
        d->clearPrefSizeCache();
        update();
        updateGeometry();
    }
}

/*!
    Returns the text in html format.

    \sa HbRichTextItem::setText()
 */

QString HbRichTextItem::text() const
{
    Q_D(const HbRichTextItem);
    return d->mText;
}

/*!
    Sets \a alignment for the text from Qt::Alignment enumeration.

    \sa HbRichTextItem::alignment()
 */
void HbRichTextItem::setAlignment(Qt::Alignment alignment)
{
    Q_D(HbRichTextItem);
    d->setApiProtectionFlag(HbWidgetBasePrivate::AC_TextAlign, true);
    alignment = d->combineAlignment(alignment, d->mTextOption.alignment());
    if (d->mTextOption.alignment()!=alignment) {
        prepareGeometryChange();
        d->mTextOption.setAlignment(alignment);
        d->mRtf->setDefaultTextOption(d->mTextOption);
        d->calculateOffset();
        update();
    }
}

/*!
    Returns alignment for the text from Qt::Alignment enumeration.

    \sa HbRichTextItem::setAlignment()
 */
Qt::Alignment HbRichTextItem::alignment() const
{
    Q_D(const HbRichTextItem);
    return d->mTextOption.alignment();
}

/*!
    \reimp
 */
void HbRichTextItem::paint(QPainter *painter, 
                           const QStyleOptionGraphicsItem *option,
                           QWidget *widget)
{
    Q_UNUSED(widget);

    Q_D(HbRichTextItem);

    if (option->exposedRect.isEmpty()) {
        // nothing to paint
        return;
    }

    painter->translate(d->mOffset);

    QAbstractTextDocumentLayout::PaintContext context;
    context.clip = option->exposedRect;
    // painter was translated so it should be compensated
    context.clip.translate(-d->mOffset);

    context.palette.setColor(QPalette::Text, textDefaultColor());

    d->mRtf->documentLayout()->draw(painter, context);

    // restore painter
    painter->translate(-d->mOffset);
}

/*!
    \reimp

    Sets new position and relayouts text according to new size.
 */
void HbRichTextItem::setGeometry(const QRectF & rect)
{
    Q_D(HbRichTextItem);

    HbWidgetBase::setGeometry(rect);

    // this call is needed since there is possible scenario
    // when size was not changed after updateGeometry and sizeHint calls
    d->setDocumentWidth(size().width());

    if (parentLayoutItem() && parentLayoutItem()->isLayout()) {
        // rect.size can't be used here since size can be limited inside of
        // called method HbWidgetBase::setGeometry(rect) so size is used which
        // holds current size
        if (d->isAdjustHeightNeeded(size().width(),
                                    preferredHeight(),
                                    minimumHeight(),
                                    maximumHeight())) {
            updateGeometry();
            return;
        }
    }
}

/*!
    \reimp
 */
QRectF HbRichTextItem::boundingRect () const
{
    Q_D(const HbRichTextItem);

    QRectF result(d->mRtf->documentLayout()->frameBoundingRect(d->mRtf->rootFrame()));
    result.translate(d->mOffset);

    if (flags().testFlag(QGraphicsItem::ItemClipsToShape)) {
        // clip
        result = result.intersect(contentsRect());
    }
    return result;
}

/*!
    \reimp
    Relayouts text according to new size.
 */
void HbRichTextItem::resizeEvent(QGraphicsSceneResizeEvent * /*event*/)
{
    Q_D(HbRichTextItem);

    d->setDocumentWidth(size().width());
    d->calculateOffset();
}

/*!
    \reimp
    This implementation detects layout direction changes, font changes and theme changes.
 */
void HbRichTextItem::changeEvent(QEvent *event)
{
    Q_D(HbRichTextItem);

    switch(event->type()) {
    case QEvent::LayoutDirectionChange: {
            prepareGeometryChange();
            d->mTextOption.setTextDirection(layoutDirection());
            d->mRtf->setDefaultTextOption(d->mTextOption);
            update();
        }
        break;

    case QEvent::FontChange: {
            d->mRtf->setDefaultFont(font());
            d->clearPrefSizeCache();
            updateGeometry();
        }
        break;

    default:
        // Listens theme changed event so that item size hint is
        // update when physical font is changed.
        if (event->type() == HbEvent::ThemeChanged) {
            Q_D(HbRichTextItem);
            d->mDefaultColor = QColor();
            if (!d->mColor.isValid()) {
                update();
            }
        }
    }
    HbWidgetBase::changeEvent(event);
}

/*!
    \reimp
    For which = PreferredSize returns reasonable size (QTextDocument::adjustSize()).
    For which = MinimumSize returns size of 4 first letters
    \a constraint width is taken into account.
 */
QSizeF HbRichTextItem::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const
{
    Q_D(const HbRichTextItem);
    QSizeF result;
    switch(which) {
    case Qt::MinimumSize: {
            result = d->minimumSizeHint(constraint);
        }
        break;

    case Qt::PreferredSize: {
            result = d->preferredSizeHint(constraint);
        }
        break;

    default:
        result = HbWidgetBase::sizeHint(which, constraint);
    }
    return result;
}

/*!
 * @proto
 * Sets the text wrapping mode.
 *
 * \sa HbRichTextItem::textWrapping
 * \sa QTextOption::setWrapMode
 */
void HbRichTextItem::setTextWrapping(Hb::TextWrapping mode)
{
    Q_D(HbRichTextItem);
    d->setApiProtectionFlag(HbWidgetBasePrivate::AC_TextWrapMode, true);
    QTextOption::WrapMode textWrapMode = static_cast<QTextOption::WrapMode>(mode);

    if (d->mTextOption.wrapMode()!=textWrapMode) {
        prepareGeometryChange();
        d->mTextOption.setWrapMode(textWrapMode);
        d->mRtf->setDefaultTextOption(d->mTextOption);
        d->calculateOffset();
        if (d->restoreDefaultHeightHint()) {
            updateGeometry();
        }
    }
}

/*!
 * @proto
 * Returns style of text wrapping.
 *
 * \sa HbRichTextItem::setTextWrapping
 * \sa QTextOption::wrapMode
 */
Hb::TextWrapping HbRichTextItem::textWrapping() const
{
    Q_D(const HbRichTextItem);

    return static_cast<Hb::TextWrapping>(d->mTextOption.wrapMode());
}

/*!
 * Returns color used as a default text color.
 * If invalid color was set color for text is fetch from parent widget.
 * If invalid color was set and no parent widget was set this will return
 * default foreground color.
 *
 * \sa setTextDefaultColor()
 */
QColor HbRichTextItem::textDefaultColor() const
{
    Q_D(const HbRichTextItem);

    if (d->mColor.isValid()) { // Means user has set text color
        return d->mColor;
    } 
    if (!d->mDefaultColor.isValid()) {
        d->mDefaultColor = HbColorScheme::color(KDefaultColorThemeName);
    }
    
    return d->mDefaultColor;
}

/*!
 * Sets color of text.
 * If invalid color was set color for text is fetch from parent widget.
 * If invalid color was set and no parent widget was set default foreground color
 * will be used
 *
 * \sa textDefaultColor()
 */
void HbRichTextItem::setTextDefaultColor(const QColor &color)
{
    Q_D(HbRichTextItem);

    d->setApiProtectionFlag(HbWidgetBasePrivate::AC_TextColor, color.isValid());
    if (d->mColor != color) {
        d->mColor = color;

        if (!color.isValid()) {
            QGraphicsWidget* ccsHandler = parentWidget();
            // check if there is a widget which handles CSS
            if (ccsHandler!=NULL) {
                // this is needed to enforce color fetch from CSS
                HbEvent themeEvent(HbEvent::ThemeChanged);
                QApplication::sendEvent(ccsHandler, &themeEvent);
            }
        }

        if (!d->mText.isEmpty()) {
            update();
        }
    }
}

/*!
 * Shows (default) or hides text. Size hint remains unchanged (same as when text is visible).
 */
void HbRichTextItem::setTextVisible(bool isVisible)
{
    setVisible(isVisible);
}

/*!
 * Returns if text is visible.
 */
bool HbRichTextItem::isTextVisible() const
{
    return isVisible();
}

/*!
 * Enables (default) or disables text clipping when item geometry is too small.
 */
void HbRichTextItem::setTextClip(bool clipping)
{
    setFlag(QGraphicsItem::ItemClipsToShape, clipping);
}

/*!
 * Returns true if text is clipped when item geometry is too small.
 */
bool HbRichTextItem::isTextClip() const
{
    return flags().testFlag(ItemClipsToShape);
}

// end of file