/****************************************************************************
**
** 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 HbWidgets 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.
**
****************************************************************************/
//
// W A R N I N G
// -------------
//
// This file is not part of the Hb API. It exists purely as an
// implementation detail. This file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "hbselectioncontrol_p.h"
#include "hbstyleoption_p.h"
#include "hbeffect.h"
#include "hbdialog_p.h"
#include "hbabstractedit.h"
#include "hbabstractedit_p.h"
#include "hbtoucharea.h"
#include "hbpangesture.h"
#include "hbtapgesture.h"
#include "hbevent.h"
#include "hbpopup.h"
#include "hbnamespace_p.h"
#include "hbmainwindow.h"
#include <QTextCursor>
#include <QGraphicsItem>
#include <QAbstractTextDocumentLayout>
#include <QGraphicsSceneMouseEvent>
#include <QBasicTimer>
#include <QSizeF>
#include <QPointF>
#include <QHash>
#include <QGraphicsScene>
#include <hbwidgetfeedback.h>
typedef QHash<HbMainWindow*,HbSelectionControl*> HbSelectionControlHash;
Q_GLOBAL_STATIC(HbSelectionControlHash, globalSelectionControlHash)
namespace {
static const int SNAP_DELAY = 300;
}
class HbSelectionControlPrivate :public HbDialogPrivate
{
Q_DECLARE_PUBLIC(HbSelectionControl)
public:
enum HandleType {
DummyHandle,
SelectionStartHandle,
SelectionEndHandle
};
HbSelectionControlPrivate();
void init();
void createPrimitives();
void updateHandle(int newHandlePos,
Qt::AlignmentFlag handleAlignment,
QGraphicsItem *handle,
QGraphicsItem *handleTouchArea,
HbStylePrivate::Primitive handlePrimitive);
QGraphicsItem * reparent(QGraphicsItem *item);
void reparent(QGraphicsItem *item, QGraphicsItem *newParent);
void reparentHandles(QGraphicsItem *newParent);
void panGestureStarted (HbPanGesture *gesture);
void panGestureUpdated (HbPanGesture *gesture);
void panGestureFinished (HbPanGesture *gesture);
void show();
void _q_aboutToChangeView();
void detachEditor(bool updateAtthachedEditorState);
public:
HbAbstractEdit *mEdit;
QGraphicsItem *mTopLevelAncestor;
QPointF mTouchOffset;
QGraphicsItem *mSelectionStartHandle;
QGraphicsItem *mSelectionEndHandle;
HbTouchArea* mSelectionStartTouchArea;
HbTouchArea* mSelectionEndTouchArea;
HandleType mPressed;
bool mPanInProgress;
QBasicTimer mWordSnapTimer;
qreal mLastCursorHeight;
};
HbSelectionControlPrivate::HbSelectionControlPrivate():
mEdit(0),
mTopLevelAncestor(0),
mSelectionStartHandle(0),
mSelectionEndHandle(0),
mSelectionStartTouchArea(0),
mSelectionEndTouchArea(0),
mPressed(HandleType(0)),
mPanInProgress(false),
mLastCursorHeight(0.0)
{
}
void HbSelectionControlPrivate::init()
{
Q_Q(HbSelectionControl);
createPrimitives();
q->setVisible(false);
QGraphicsItem::GraphicsItemFlags itemFlags = q->flags();
#if QT_VERSION >= 0x040600
itemFlags |= QGraphicsItem::ItemSendsGeometryChanges;
#endif
itemFlags &= ~QGraphicsItem::ItemIsFocusable;
q->setFlags(itemFlags);
q->setFocusPolicy(Qt::NoFocus);
// Control will handle all events going to different handlers.
q->setHandlesChildEvents(true);
}
void HbSelectionControlPrivate::createPrimitives()
{
Q_Q(HbSelectionControl);
if (!mSelectionStartHandle) {
mSelectionStartHandle = HbStylePrivate::createPrimitive(HbStylePrivate::P_SelectionControl_selectionstart, q);
mSelectionStartHandle->setFlag(QGraphicsItem::ItemIsFocusable,false);
}
if (!mSelectionEndHandle) {
mSelectionEndHandle = HbStylePrivate::createPrimitive(HbStylePrivate::P_SelectionControl_selectionend, q);
mSelectionEndHandle->setFlag(QGraphicsItem::ItemIsFocusable,false);
}
if (!mSelectionStartTouchArea) {
mSelectionStartTouchArea = new HbTouchArea(q);
mSelectionStartTouchArea->setFlag(QGraphicsItem::ItemIsFocusable,false);
HbStyle::setItemName(mSelectionStartTouchArea, "handle-toucharea");
mSelectionStartTouchArea->grabGesture(Qt::TapGesture);
mSelectionStartTouchArea->grabGesture(Qt::PanGesture);
}
if (!mSelectionEndTouchArea) {
mSelectionEndTouchArea = new HbTouchArea(q);
mSelectionEndTouchArea->setFlag(QGraphicsItem::ItemIsFocusable,false);
HbStyle::setItemName(mSelectionEndTouchArea, "handle-toucharea");
mSelectionEndTouchArea->grabGesture(Qt::TapGesture);
mSelectionEndTouchArea->grabGesture(Qt::PanGesture);
}
}
/*
Updates given handle associated with handleTouchArea to place it
newHandlePos in the selected text.
handlePrimitive identifies handle graphics.
*/
void HbSelectionControlPrivate::updateHandle(int newHandlePos,
Qt::AlignmentFlag handleAlignment,
QGraphicsItem *handle,
QGraphicsItem *handleTouchArea,
HbStylePrivate::Primitive handlePrimitive)
{
Q_Q(HbSelectionControl);
HbStyleOption option;
q->initStyleOption(&option);
HbStylePrivate::updatePrimitive(handle, handlePrimitive, &option);
QRectF rect = mEdit->rectForPosition(newHandlePos,Qt::AlignTop?QTextLine::Leading:QTextLine::Trailing);
mLastCursorHeight = rect.height();
// Convert rect to handle's parent coordinate system
rect = handle->parentItem()->mapRectFromItem(mEdit,rect);
// Center handle around center point of rect
QRectF boundingRect = handle->boundingRect();
boundingRect.moveCenter(rect.center());
if (handleAlignment == Qt::AlignTop) {
boundingRect.moveBottom(rect.top());
} else {
boundingRect.moveTop(rect.bottom());
}
handle->setPos(boundingRect.topLeft());
// Position handle touch area around center-top of handle
QPointF centerPos = boundingRect.center();
rect = boundingRect;
boundingRect = handleTouchArea->boundingRect();
boundingRect.moveCenter(centerPos);
boundingRect.moveTop(rect.top());
handleTouchArea->setPos(boundingRect.topLeft());
if (!mPanInProgress) {
QGraphicsItem * newParent = reparent(handle);
reparent(handleTouchArea, newParent);
}
}
/*
Reparents item to q if item's bounding rect intersects mEdit's viewPort rectangle or otherwise to
HbAbstractEditPrivate::d_ptr(d->mEdit)->canvas.
Returns new parent.
*/
QGraphicsItem * HbSelectionControlPrivate::reparent(QGraphicsItem *item)
{
Q_Q(HbSelectionControl);
QGraphicsItem *newParent = HbAbstractEditPrivate::d_ptr(mEdit)->canvas;
// Convert bounding rect to mEdit's coordinate system
QRectF rect = item->boundingRect();
rect = item->mapRectToItem(mEdit,rect);
QRectF scrollAreaRect = HbAbstractEditPrivate::d_ptr(mEdit)->scrollArea->geometry();
if (rect.intersects(scrollAreaRect)) {
newParent = q;
}
reparent(item, newParent);
return newParent;
}
void HbSelectionControlPrivate::reparent(QGraphicsItem *item, QGraphicsItem *newParent)
{
if (item && newParent && newParent != item->parentItem()) {
// Reparent handle items to newParent
QPointF pos = newParent->mapFromItem(item->parentItem(),item->pos());
// TODO: This is a workaround for a Qt bug when reparenting from a clipping parent to a
// non-clipping parent
item->setParentItem(0);
item->setParentItem(newParent);
item->setPos(pos);
}
}
void HbSelectionControlPrivate::reparentHandles(QGraphicsItem *newParent)
{
// Reparent handle items to newParent
reparent(mSelectionStartHandle, newParent);
reparent(mSelectionStartTouchArea, newParent);
reparent(mSelectionEndHandle, newParent);
reparent(mSelectionEndTouchArea, newParent);
}
void HbSelectionControlPrivate::panGestureStarted(HbPanGesture *gesture)
{
Q_Q(HbSelectionControl);
QPointF point = q->mapFromScene(gesture->sceneStartPos());
mPressed = DummyHandle;
// Find out which handle is being moved
if (mSelectionStartTouchArea->contains(q->mapToItem(mSelectionStartTouchArea, point))) {
mPressed = SelectionStartHandle;
mTouchOffset = mSelectionStartHandle->pos() - point;
}
if (mSelectionEndTouchArea->contains(q->mapToItem(mSelectionEndTouchArea, point))) {
bool useArea = true;
if(mPressed != DummyHandle) {
// The press point was inside in both of the touch areas
// choose the touch area whose center is closer to the press point
QRectF rect = mSelectionStartHandle->boundingRect();
rect.moveTopLeft(mSelectionStartHandle->pos());
QLineF lineEventPosSelStartCenter(point,rect.center());
rect = mSelectionEndHandle->boundingRect();
rect.moveTopLeft(mSelectionEndHandle->pos());
QLineF lineEventPosSelEndCenter(point,rect.center());
if (lineEventPosSelStartCenter.length() < lineEventPosSelEndCenter.length()) {
useArea = false;
}
}
if (useArea) {
mPressed = SelectionEndHandle;
mTouchOffset = mSelectionEndHandle->pos() - point;
}
}
if (mPressed == DummyHandle) {
// Hit is outside touch areas, ignore
return;
}
// Position cursor at the pressed selection handle
QTextCursor cursor = mEdit->textCursor();
int selStartPos = qMin(mEdit->textCursor().anchor(),mEdit->textCursor().position());
int selEndPos = qMax(mEdit->textCursor().anchor(),mEdit->textCursor().position());
if (mPressed == SelectionStartHandle) {
cursor.setPosition(selEndPos);
cursor.setPosition(selStartPos, QTextCursor::KeepAnchor);
} else {
cursor.setPosition(selStartPos);
cursor.setPosition(selEndPos, QTextCursor::KeepAnchor);
}
mEdit->setTextCursor(cursor);
}
void HbSelectionControlPrivate::panGestureFinished(HbPanGesture *gesture)
{
Q_Q(HbSelectionControl);
Q_UNUSED(gesture)
if (mWordSnapTimer.isActive()) {
// Snap selection to word beginning or end
QTextCursor cursor = mEdit->textCursor();
int curPos = mEdit->textCursor().position();
int anchPos = mEdit->textCursor().anchor();
cursor.select(QTextCursor::WordUnderCursor);
// Snap direction depends on cursor position
curPos = ((curPos > anchPos)?cursor.position():cursor.anchor());
cursor.setPosition(anchPos);
cursor.setPosition(curPos, QTextCursor::KeepAnchor);
mEdit->setTextCursor(cursor);
}
mPressed = DummyHandle;
q->updatePrimitives();
}
void HbSelectionControlPrivate::panGestureUpdated(HbPanGesture *gesture)
{
Q_Q(HbSelectionControl);
QRectF docRect = QRectF(mEdit->mapFromItem(HbAbstractEditPrivate::d_ptr(mEdit)->canvas->parentItem(),
HbAbstractEditPrivate::d_ptr(mEdit)->canvas->pos()),
HbAbstractEditPrivate::d_ptr(mEdit)->doc->size());
QPointF editPos = mEdit->mapFromScene(gesture->sceneStartPos() + gesture->sceneOffset());
QPointF origEditPos = editPos;
bool outsideCanvas = !docRect.contains(origEditPos);
// Constrain editPos within docRect
editPos = QPointF(qMin(qMax(editPos.x(),docRect.left()),docRect.right()),
qMin(qMax(editPos.y(),docRect.top()),docRect.bottom()));
QRectF handleRect = mSelectionStartHandle->boundingRect();
handleRect.moveTopLeft(editPos + mTouchOffset);
// Set hitTestPos based on which handle was grabbed
QPointF hitTestPos = handleRect.center();
if (mPressed == SelectionStartHandle) {
hitTestPos.setY(handleRect.bottom()+mLastCursorHeight/2);
} else {
hitTestPos.setY(handleRect.top()-mLastCursorHeight/2);
}
// Override hitTestPos if origEditPos was outside the canvas
if (outsideCanvas) {
if (origEditPos.y() < docRect.top()) {
hitTestPos.setY(handleRect.bottom()+1);
} else if (docRect.bottom() < origEditPos.y()) {
hitTestPos.setY(handleRect.top()-1);
}
}
QTextCursor cursor;
cursor = mEdit->textCursor();
// Hit test for the center of current selection touch area
int hitPos = HbAbstractEditPrivate::d_ptr(mEdit)->hitTest(hitTestPos,Qt::FuzzyHit);
// if no valid hit pos or empty selection return
if (hitPos == -1 || hitPos == cursor.anchor()) {
return;
}
bool handlesMoved(false);
if (hitPos != cursor.position()) {
handlesMoved = true;
}
cursor.setPosition(hitPos, QTextCursor::KeepAnchor);
if (handlesMoved) {
if (mEdit) {
HbWidgetFeedback::triggered(mEdit, Hb::InstantDraggedOver);
}
// Restart timer every time when a selection handle moved
mWordSnapTimer.start(SNAP_DELAY, q);
mEdit->setTextCursor(cursor);
}
// Ensure that the hitPos is visible
HbAbstractEditPrivate::d_ptr(mEdit)->ensurePositionVisible(hitPos);
q->updatePrimitives();
}
void HbSelectionControlPrivate::show() {
Q_Q(HbSelectionControl);
// Set the z-value of the selection control above its top-level ancestor
if (mTopLevelAncestor) {
qreal zValue = mTopLevelAncestor->zValue() + HbPrivate::SelectionControlHandlesValueUnit;
q->setZValue(zValue);
}
if (q->scene() != mEdit->scene() && mEdit->scene()) {
mEdit->scene()->addItem(q);
}
q->show();
q->updatePrimitives();
}
void HbSelectionControlPrivate::_q_aboutToChangeView()
{
Q_Q(HbSelectionControl);
if (mEdit && q->isVisible()) {
mEdit->deselect();
}
}
void HbSelectionControlPrivate::detachEditor(bool updateAtthachedEditorState)
{
Q_Q(HbSelectionControl);
if (mEdit) {
q->hideHandles();
reparentHandles(q);
if (updateAtthachedEditorState) {
mEdit->disconnect(q);
mEdit->d_func()->selectionControl = 0;
mEdit->deselect();
}
mEdit = 0;
mTopLevelAncestor = 0;
}
}
HbSelectionControl::HbSelectionControl() : HbWidget(*new HbSelectionControlPrivate(),0)
{
Q_D(HbSelectionControl);
d->q_ptr = this;
d->init();
//TODO: selection control could be singleton per main window
// since only one selection control is used at a time
}
HbSelectionControl* HbSelectionControl::attachEditor(HbAbstractEdit *edit)
{
if(!edit || !edit->mainWindow()) {
qWarning("HbSelectionControl: attempting to attach to null editor pointer!");
}
HbSelectionControl *control = globalSelectionControlHash()->value(edit->mainWindow());
if (!control) {
control = new HbSelectionControl();
globalSelectionControlHash()->insert(edit->mainWindow(),control);
QObject::connect(edit->mainWindow(), SIGNAL(aboutToChangeView(HbView *, HbView *)), control, SLOT(_q_aboutToChangeView()));
}
HbSelectionControlPrivate *d = control->d_func();
if (edit != d->mEdit) {
control->detachEditor();
d->mEdit = edit;
QObject::connect(d->mEdit, SIGNAL(cursorPositionChanged(int, int)), control, SLOT(updatePrimitives()));
QObject::connect(d->mEdit, SIGNAL(contentsChanged()), control, SLOT(updatePrimitives()));
// find first top-level ancestor of d->mEdit
for(d->mTopLevelAncestor = d->mEdit;
d->mTopLevelAncestor->parentItem();
d->mTopLevelAncestor = d->mTopLevelAncestor->parentItem()){};
}
return control;
}
void HbSelectionControl::detachEditor()
{
Q_D(HbSelectionControl);
d->detachEditor(true);
}
void HbSelectionControl::detachEditorFromDestructor()
{
Q_D(HbSelectionControl);
d->detachEditor(false);
}
void HbSelectionControl::hideHandles()
{
Q_D(HbSelectionControl);
if (isVisible() && d->mEdit) {
hide();
d->reparentHandles(this);
}
}
void HbSelectionControl::showHandles()
{
Q_D(HbSelectionControl);
if (!isVisible() && d->mEdit) {
d->show();
}
}
void HbSelectionControl::scrollStarted()
{
Q_D(HbSelectionControl);
if (isVisible() && d->mEdit) {
d->mPanInProgress = true;
// Reparent handle items to editor canvas on pan start
d->reparentHandles(HbAbstractEditPrivate::d_ptr(d->mEdit)->canvas);
}
}
void HbSelectionControl::scrollFinished()
{
Q_D(HbSelectionControl);
if (isVisible() && d->mEdit) {
d->mPanInProgress = false;
updatePrimitives();
}
}
void HbSelectionControl::timerEvent(QTimerEvent *event)
{
Q_D(HbSelectionControl);
if (event->timerId() == d->mWordSnapTimer.timerId()) {
d->mWordSnapTimer.stop();
}
}
void HbSelectionControl::polish( HbStyleParameters& params )
{
Q_D(HbSelectionControl);
HbWidget::polish(params);
QSizeF size = d->mSelectionStartTouchArea->preferredSize();
d->mSelectionStartTouchArea->resize(size);
d->mSelectionEndTouchArea->resize(size);
updatePrimitives();
}
QVariant HbSelectionControl::itemChange(GraphicsItemChange change, const QVariant &value)
{
if (change == QGraphicsItem::ItemPositionChange) {
return qVariantFromValue(QPointF(0,0));
}
return HbWidget::itemChange(change, value);
}
void HbSelectionControl::gestureEvent(QGestureEvent* event) {
Q_D(HbSelectionControl);
if(HbTapGesture *tap = qobject_cast<HbTapGesture*>(event->gesture(Qt::TapGesture))) {
if (d->mEdit && tap->tapStyleHint() != HbTapGesture::TapAndHold) {
d->mEdit->gestureEvent(event);
// Cancel HbAbstractEdit gesture overriding settings
tap->setProperty(HbPrivate::ThresholdRect.latin1(), QVariant());
scene()->setProperty(HbPrivate::OverridingGesture.latin1(),QVariant());
HbWidgetFeedback::triggered(this, Hb::InstantPressed);
}
}
if(HbPanGesture *pan = qobject_cast<HbPanGesture*>(event->gesture(Qt::PanGesture))) {
switch(pan->state()) {
case Qt::GestureStarted:
if (d->mEdit) {
d->panGestureStarted(pan);
}
break;
case Qt::GestureUpdated:
if (d->mEdit) {
d->panGestureUpdated(pan);
}
break;
case Qt::GestureFinished:
if (d->mEdit) {
d->panGestureFinished(pan);
HbWidgetFeedback::triggered(this, Hb::InstantReleased);
}
break;
case Qt::GestureCanceled:
break;
default:
break;
}
}
}
bool HbSelectionControl::event(QEvent *event)
{
Q_D(HbSelectionControl);
if (event->type() == HbEvent::DeviceProfileChanged && d->mEdit) {
HbDeviceProfileChangedEvent* dpEvent = static_cast<HbDeviceProfileChangedEvent*>(event);
if ( dpEvent->profile().alternateProfileName() == dpEvent->oldProfile().name() ) {
updatePrimitives();
}
}
return HbWidget::event(event);
}
void HbSelectionControl::updatePrimitives()
{
Q_D(HbSelectionControl);
if (isVisible() && d->polished && d->mEdit) {
if (d->mEdit->textCursor().hasSelection() ) {
int selStartPos = qMin(d->mEdit->textCursor().anchor(),d->mEdit->textCursor().position());
int selEndPos = qMax(d->mEdit->textCursor().anchor(),d->mEdit->textCursor().position());
d->updateHandle(selStartPos,Qt::AlignTop,d->mSelectionStartHandle,d->mSelectionStartTouchArea,HbStylePrivate::P_SelectionControl_selectionstart);
d->updateHandle(selEndPos,Qt::AlignBottom,d->mSelectionEndHandle,d->mSelectionEndTouchArea,HbStylePrivate::P_SelectionControl_selectionend);
}
else {
hide();
}
}
}
#include "moc_hbselectioncontrol_p.cpp"