diff options
author | Friedemann Kleint <Friedemann.Kleint@digia.com> | 2013-06-13 16:23:44 +0200 |
---|---|---|
committer | The Qt Project <gerrit-noreply@qt-project.org> | 2013-08-06 08:19:38 +0200 |
commit | 9ce12cc8de940cdd450a28f4bd079acfc3621aa3 (patch) | |
tree | ca07f5b57179b58caae43f0297be4f4d9834d8e5 | |
parent | c207724c9bb8e205e756086950063cc91656e401 (diff) |
Add side widgets to QLineEdit via QAction.
Add QLineEdit::addAction() overloads,
allowing for a variable number of icons or user-defined
widgets.
Change-Id: Id298f18c2f47cc998170357e65cc6098df851aab
Done-with: Kevin.Ottens@kdab.com
Reviewed-by: Thomas Zander <zander@kde.org>
Reviewed-by: Richard J. Moore <rich@kde.org>
Reviewed-by: Frederik Gladhorn <frederik.gladhorn@digia.com>
-rw-r--r-- | src/widgets/widgets/qlineedit.cpp | 70 | ||||
-rw-r--r-- | src/widgets/widgets/qlineedit.h | 21 | ||||
-rw-r--r-- | src/widgets/widgets/qlineedit_p.cpp | 161 | ||||
-rw-r--r-- | src/widgets/widgets/qlineedit_p.h | 80 | ||||
-rw-r--r-- | tests/auto/widgets/widgets/qlineedit/tst_qlineedit.cpp | 43 |
5 files changed, 363 insertions, 12 deletions
diff --git a/src/widgets/widgets/qlineedit.cpp b/src/widgets/widgets/qlineedit.cpp index 9efae802c6..c704eb5c35 100644 --- a/src/widgets/widgets/qlineedit.cpp +++ b/src/widgets/widgets/qlineedit.cpp @@ -420,6 +420,60 @@ bool QLineEdit::hasFrame() const return d->frame; } +/*! + \enum QLineEdit::ActionPosition + + This enum type describes how a line edit should display the action widgets to be + added. + + \value LeadingPosition The widget is displayed to the left of the text + when using layout direction \c Qt::LeftToRight or to + the right when using \c Qt::RightToLeft, respectively. + + \value TrailingPosition The widget is displayed to the right of the text + when using layout direction \c Qt::LeftToRight or to + the left when using \c Qt::RightToLeft, respectively. + + \sa addAction(), removeAction(), QWidget::layoutDirection + + \since 5.2 +*/ + +/*! + \fn void QLineEdit::addAction(QAction *action) + \overload + \internal +*/ + +/*! + \overload + + Adds the \a action to the list of actions at the \a position. + + \since 5.2 +*/ + +void QLineEdit::addAction(QAction *action, ActionPosition position) +{ + Q_D(QLineEdit); + QWidget::addAction(action); + d->addAction(action, 0, position); +} + +/*! + \overload + + Creates a new action with the given \a icon at the \a position. + + \since 5.2 +*/ + +QAction *QLineEdit::addAction(const QIcon &icon, ActionPosition position) +{ + QAction *result = new QAction(icon, QString(), this); + addAction(result, position); + return result; +} void QLineEdit::setFrame(bool enable) { @@ -606,7 +660,7 @@ QSize QLineEdit::sizeHint() const + d->topTextMargin + d->bottomTextMargin + d->topmargin + d->bottommargin; int w = fm.width(QLatin1Char('x')) * 17 + 2*d->horizontalMargin - + d->leftTextMargin + d->rightTextMargin + + d->effectiveLeftTextMargin() + d->effectiveRightTextMargin() + d->leftmargin + d->rightmargin; // "some" QStyleOptionFrameV2 opt; initStyleOption(&opt); @@ -967,7 +1021,6 @@ void QLineEdit::setDragEnabled(bool b) d->dragEnabled = b; } - /*! \property QLineEdit::cursorMoveStyle \brief the movement style of cursor in this line edit @@ -1350,8 +1403,11 @@ bool QLineEdit::event(QEvent * e) || style()->styleHint(QStyle::SH_BlinkCursorWhenTextSelected, &opt, this)) d->setCursorVisible(true); } + } else if (e->type() == QEvent::ActionRemoved) { + d->removeAction(static_cast<QActionEvent *>(e)); + } else if (e->type() == QEvent::Resize) { + d->positionSideWidgets(); } - #ifdef QT_KEYPAD_NAVIGATION if (QApplication::keypadNavigationEnabled()) { if (e->type() == QEvent::EnterEditFocus) { @@ -1777,9 +1833,9 @@ void QLineEdit::paintEvent(QPaintEvent *) initStyleOption(&panel); style()->drawPrimitive(QStyle::PE_PanelLineEdit, &panel, &p, this); r = style()->subElementRect(QStyle::SE_LineEditContents, &panel, this); - r.setX(r.x() + d->leftTextMargin); + r.setX(r.x() + d->effectiveLeftTextMargin()); r.setY(r.y() + d->topTextMargin); - r.setRight(r.right() - d->rightTextMargin); + r.setRight(r.right() - d->effectiveRightTextMargin()); r.setBottom(r.bottom() - d->bottomTextMargin); p.setClipRect(r); @@ -2083,8 +2139,12 @@ void QLineEdit::changeEvent(QEvent *ev) initStyleOption(&opt); d->control->setPasswordCharacter(style()->styleHint(QStyle::SH_LineEdit_PasswordCharacter, &opt, this)); } + d->m_iconSize = QSize(); update(); break; + case QEvent::LayoutDirectionChange: + d->positionSideWidgets(); + break; default: break; } diff --git a/src/widgets/widgets/qlineedit.h b/src/widgets/widgets/qlineedit.h index e2b944314b..3d16e527b6 100644 --- a/src/widgets/widgets/qlineedit.h +++ b/src/widgets/widgets/qlineedit.h @@ -59,12 +59,14 @@ class QCompleter; class QStyleOptionFrame; class QAbstractSpinBox; class QDateTimeEdit; +class QIcon; +class QToolButton; class Q_WIDGETS_EXPORT QLineEdit : public QWidget { Q_OBJECT - Q_ENUMS(EchoMode) + Q_ENUMS(ActionPosition EchoMode) Q_PROPERTY(QString inputMask READ inputMask WRITE setInputMask) Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged USER true) Q_PROPERTY(int maxLength READ maxLength WRITE setMaxLength) @@ -83,8 +85,12 @@ class Q_WIDGETS_EXPORT QLineEdit : public QWidget Q_PROPERTY(bool acceptableInput READ hasAcceptableInput) Q_PROPERTY(QString placeholderText READ placeholderText WRITE setPlaceholderText) Q_PROPERTY(Qt::CursorMoveStyle cursorMoveStyle READ cursorMoveStyle WRITE setCursorMoveStyle) - public: + enum ActionPosition { + LeadingPosition, + TrailingPosition + }; + explicit QLineEdit(QWidget* parent=0); explicit QLineEdit(const QString &, QWidget* parent=0); ~QLineEdit(); @@ -164,6 +170,16 @@ public: void getTextMargins(int *left, int *top, int *right, int *bottom) const; QMargins textMargins() const; +#ifdef Q_NO_USING_KEYWORD + inline void addAction(QAction *action) + { QWidget::addAction(action); } +#else + using QWidget::addAction; +#endif + + void addAction(QAction *action, ActionPosition position); + QAction *addAction(const QIcon &icon, ActionPosition position); + public Q_SLOTS: void setText(const QString &); void clear(); @@ -240,6 +256,7 @@ private: #endif Q_PRIVATE_SLOT(d_func(), void _q_selectionChanged()) Q_PRIVATE_SLOT(d_func(), void _q_updateNeeded(const QRect &)) + Q_PRIVATE_SLOT(d_func(), void _q_textChanged(const QString &)) }; #endif // QT_NO_LINEEDIT diff --git a/src/widgets/widgets/qlineedit_p.cpp b/src/widgets/widgets/qlineedit_p.cpp index 1999216e65..028675f845 100644 --- a/src/widgets/widgets/qlineedit_p.cpp +++ b/src/widgets/widgets/qlineedit_p.cpp @@ -44,8 +44,10 @@ #ifndef QT_NO_LINEEDIT +#include "qvariant.h" #include "qabstractitemview.h" #include "qdrag.h" +#include "qwidgetaction.h" #include "qclipboard.h" #ifndef QT_NO_ACCESSIBILITY #include "qaccessible.h" @@ -53,6 +55,7 @@ #ifndef QT_NO_IM #include "qinputmethod.h" #include "qlist.h" +#include <qpropertyanimation.h> #endif QT_BEGIN_NAMESPACE @@ -219,9 +222,9 @@ QRect QLineEditPrivate::adjustedContentsRect() const QStyleOptionFrameV2 opt; q->initStyleOption(&opt); QRect r = q->style()->subElementRect(QStyle::SE_LineEditContents, &opt, q); - r.setX(r.x() + leftTextMargin); + r.setX(r.x() + effectiveLeftTextMargin()); r.setY(r.y() + topTextMargin); - r.setRight(r.right() - rightTextMargin); + r.setRight(r.right() - effectiveRightTextMargin()); r.setBottom(r.bottom() - bottomTextMargin); return r; } @@ -297,6 +300,160 @@ void QLineEditPrivate::drag() #endif // QT_NO_DRAGANDDROP +QLineEditIconButton::QLineEditIconButton(QWidget *parent) + : QToolButton(parent) + , m_opacity(0) +{ +#ifndef QT_NO_CURSOR + setCursor(Qt::ArrowCursor); +#endif + setFocusPolicy(Qt::NoFocus); +} + +void QLineEditIconButton::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + // Note isDown should really use the active state but in most styles + // this has no proper feedback + QIcon::Mode state = QIcon::Disabled; + if (isEnabled()) + state = isDown() ? QIcon::Selected : QIcon::Normal; + const QPixmap iconPixmap = icon().pixmap(QSize(IconButtonSize, IconButtonSize), + state, QIcon::Off); + QRect pixmapRect = QRect(0, 0, iconPixmap.width(), iconPixmap.height()); + pixmapRect.moveCenter(rect().center()); + painter.setOpacity(m_opacity); + painter.drawPixmap(pixmapRect, iconPixmap); +} + +void QLineEditIconButton::setOpacity(qreal value) +{ + if (!qFuzzyCompare(m_opacity, value)) { + m_opacity = value; + update(); + } +} + +void QLineEditIconButton::startOpacityAnimation(qreal endValue) +{ + QPropertyAnimation *animation = new QPropertyAnimation(this, QByteArrayLiteral("opacity")); + animation->setDuration(160); + animation->setEndValue(endValue); + animation->start(QAbstractAnimation::DeleteWhenStopped); +} + +void QLineEditPrivate::_q_textChanged(const QString &text) +{ + if (hasSideWidgets()) { + const int newTextSize = text.size(); + if (!newTextSize || !lastTextSize) { + lastTextSize = newTextSize; + const bool fadeIn = newTextSize > 0; + foreach (const SideWidgetEntry &e, leadingSideWidgets) { + if (e.flags & SideWidgetFadeInWithText) + static_cast<QLineEditIconButton *>(e.widget)->animateShow(fadeIn); + } + foreach (const SideWidgetEntry &e, trailingSideWidgets) { + if (e.flags & SideWidgetFadeInWithText) + static_cast<QLineEditIconButton *>(e.widget)->animateShow(fadeIn); + } + } + } +} + +QSize QLineEditPrivate::iconSize() const +{ + if (!m_iconSize.isValid()) // This might require style-specific handling (pixel metric). + m_iconSize = QSize(QLineEditIconButton::IconButtonSize + 6, QLineEditIconButton::IconButtonSize + 2); + return m_iconSize; +} + +void QLineEditPrivate::positionSideWidgets() +{ + Q_Q(QLineEdit); + if (hasSideWidgets()) { + const QRect contentRect = q->rect(); + const QSize iconSize = QLineEditPrivate::iconSize(); + const int delta = QLineEditIconButton::IconMargin + iconSize.width(); + QRect widgetGeometry(QPoint(QLineEditIconButton::IconMargin, (contentRect.height() - iconSize.height()) / 2), iconSize); + foreach (const SideWidgetEntry &e, leftSideWidgetList()) { + e.widget->setGeometry(widgetGeometry); + widgetGeometry.moveLeft(widgetGeometry.left() + delta); + } + widgetGeometry.moveLeft(contentRect.width() - iconSize.width() - QLineEditIconButton::IconMargin); + foreach (const SideWidgetEntry &e, rightSideWidgetList()) { + e.widget->setGeometry(widgetGeometry); + widgetGeometry.moveLeft(widgetGeometry.left() - delta); + } + } +} + +QLineEditPrivate::PositionIndexPair QLineEditPrivate::findSideWidget(const QAction *a) const +{ + for (int i = 0; i < leadingSideWidgets.size(); ++i) { + if (a == leadingSideWidgets.at(i).action) + return PositionIndexPair(QLineEdit::LeadingPosition, i); + } + for (int i = 0; i < trailingSideWidgets.size(); ++i) { + if (a == trailingSideWidgets.at(i).action) + return PositionIndexPair(QLineEdit::TrailingPosition, i); + } + return PositionIndexPair(QLineEdit::LeadingPosition, -1); +} + +QWidget *QLineEditPrivate::addAction(QAction *newAction, QAction *before, QLineEdit::ActionPosition position, int flags) +{ + Q_Q(QLineEdit); + if (!newAction) + return 0; + QWidget *w = 0; + // Store flags about QWidgetAction here since removeAction() may be called from ~QAction, + // in which a qobject_cast<> no longer works. + if (QWidgetAction *widgetAction = qobject_cast<QWidgetAction *>(newAction)) { + if ((w = widgetAction->requestWidget(q))) + flags |= SideWidgetCreatedByWidgetAction; + } + if (!w) { + QLineEditIconButton *toolButton = new QLineEditIconButton(q); + toolButton->setIcon(newAction->icon()); + toolButton->setOpacity(lastTextSize > 0 || !(flags & SideWidgetFadeInWithText) ? 1 : 0); + toolButton->setDefaultAction(newAction); + w = toolButton; + } + if (!hasSideWidgets()) { // initial setup. + QObject::connect(q, SIGNAL(textChanged(QString)), q, SLOT(_q_textChanged(QString))); + lastTextSize = q->text().size(); + } + // If there is a 'before' action, it takes preference + PositionIndexPair positionIndex = before ? findSideWidget(before) : PositionIndexPair(position, -1); + SideWidgetEntryList &list = positionIndex.first == QLineEdit::TrailingPosition ? trailingSideWidgets : leadingSideWidgets; + if (positionIndex.second < 0) + positionIndex.second = list.size(); + list.insert(positionIndex.second, SideWidgetEntry(w, newAction, flags)); + positionSideWidgets(); + w->show(); + return w; +} + +void QLineEditPrivate::removeAction(const QActionEvent *e) +{ + Q_Q(QLineEdit); + QAction *action = e->action(); + const PositionIndexPair positionIndex = findSideWidget(action); + if (positionIndex.second == -1) + return; + SideWidgetEntryList &list = positionIndex.first == QLineEdit::TrailingPosition ? trailingSideWidgets : leadingSideWidgets; + SideWidgetEntry entry = list.takeAt(positionIndex.second); + if (entry.flags & SideWidgetCreatedByWidgetAction) + static_cast<QWidgetAction *>(entry.action)->releaseWidget(entry.widget); + else + delete entry.widget; + positionSideWidgets(); + if (!hasSideWidgets()) // Last widget, remove connection + QObject::disconnect(q, SIGNAL(textChanged(QString)), q, SLOT(_q_textChanged(QString))); + q->update(); +} + QT_END_NAMESPACE #endif diff --git a/src/widgets/widgets/qlineedit_p.h b/src/widgets/widgets/qlineedit_p.h index 4eb35b7dc6..19dbde6558 100644 --- a/src/widgets/widgets/qlineedit_p.h +++ b/src/widgets/widgets/qlineedit_p.h @@ -58,7 +58,9 @@ #ifndef QT_NO_LINEEDIT #include "private/qwidget_p.h" #include "QtWidgets/qlineedit.h" +#include "QtWidgets/qtoolbutton.h" #include "QtGui/qtextlayout.h" +#include "QtGui/qicon.h" #include "QtWidgets/qstyleoption.h" #include "QtCore/qbasictimer.h" #include "QtWidgets/qcompleter.h" @@ -69,16 +71,54 @@ QT_BEGIN_NAMESPACE +// QLineEditIconButton: This is a simple helper class that represents clickable icons that fade in with text + +class QLineEditIconButton : public QToolButton +{ + Q_OBJECT + Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity) +public: + enum { IconMargin = 4, IconButtonSize = 16 }; + + explicit QLineEditIconButton(QWidget *parent = 0); + + qreal opacity() const { return m_opacity; } + void setOpacity(qreal value); + void animateShow(bool visible) { startOpacityAnimation(visible ? 1.0 : 0.0); } + +protected: + void paintEvent(QPaintEvent *event); + +private: + void startOpacityAnimation(qreal endValue); + + qreal m_opacity; +}; + class Q_AUTOTEST_EXPORT QLineEditPrivate : public QWidgetPrivate { Q_DECLARE_PUBLIC(QLineEdit) public: + enum SideWidgetFlag { + SideWidgetFadeInWithText = 0x1, + SideWidgetCreatedByWidgetAction = 0x2 + }; + + struct SideWidgetEntry { + SideWidgetEntry(QWidget *w = 0, QAction *a = 0, int _flags = 0) : widget(w), action(a), flags(_flags) {} + + QWidget *widget; + QAction *action; + int flags; + }; + typedef QList<SideWidgetEntry> SideWidgetEntryList; QLineEditPrivate() : control(0), frame(1), contextMenuEnabled(1), cursorVisible(0), dragEnabled(0), clickCausedFocus(0), hscroll(0), vscroll(0), alignment(Qt::AlignLeading | Qt::AlignVCenter), - leftTextMargin(0), topTextMargin(0), rightTextMargin(0), bottomTextMargin(0) + leftTextMargin(0), topTextMargin(0), rightTextMargin(0), bottomTextMargin(0), + lastTextSize(0) { } @@ -145,15 +185,49 @@ public: QBasicTimer dndTimer; void drag(); #endif + void _q_textChanged(const QString &); - int leftTextMargin; + int leftTextMargin; // use effectiveLeftTextMargin() in case of icon. int topTextMargin; - int rightTextMargin; + int rightTextMargin; // use effectiveRightTextMargin() in case of icon. int bottomTextMargin; QString placeholderText; + + QWidget *addAction(QAction *newAction, QAction *before, QLineEdit::ActionPosition, int flags = 0); + void removeAction(const QActionEvent *e); + QSize iconSize() const; + void positionSideWidgets(); + inline bool hasSideWidgets() const { return !leadingSideWidgets.isEmpty() || !trailingSideWidgets.isEmpty(); } + inline const SideWidgetEntryList &leftSideWidgetList() const + { return q_func()->layoutDirection() == Qt::LeftToRight ? leadingSideWidgets : trailingSideWidgets; } + inline const SideWidgetEntryList &rightSideWidgetList() const + { return q_func()->layoutDirection() == Qt::LeftToRight ? trailingSideWidgets : leadingSideWidgets; } + + int effectiveLeftTextMargin() const; + int effectiveRightTextMargin() const; + +private: + typedef QPair<QLineEdit::ActionPosition, int> PositionIndexPair; + + PositionIndexPair findSideWidget(const QAction *a) const; + + SideWidgetEntryList leadingSideWidgets; + SideWidgetEntryList trailingSideWidgets; + int lastTextSize; + mutable QSize m_iconSize; }; +inline int QLineEditPrivate::effectiveLeftTextMargin() const +{ + return leftTextMargin + leftSideWidgetList().size() * (QLineEditIconButton::IconMargin + iconSize().width()); +} + +inline int QLineEditPrivate::effectiveRightTextMargin() const +{ + return rightTextMargin + rightSideWidgetList().size() * (QLineEditIconButton::IconMargin + iconSize().width()); +} + #endif // QT_NO_LINEEDIT QT_END_NAMESPACE diff --git a/tests/auto/widgets/widgets/qlineedit/tst_qlineedit.cpp b/tests/auto/widgets/widgets/qlineedit/tst_qlineedit.cpp index ae3fefc45a..cd715b7b06 100644 --- a/tests/auto/widgets/widgets/qlineedit/tst_qlineedit.cpp +++ b/tests/auto/widgets/widgets/qlineedit/tst_qlineedit.cpp @@ -46,6 +46,9 @@ #include "qstringlist.h" #include "qstyle.h" #include "qvalidator.h" +#include "qwidgetaction.h" +#include "qimage.h" +#include "qicon.h" #include "qcompleter.h" #include "qstandarditemmodel.h" #include <qpa/qplatformtheme.h> @@ -61,8 +64,12 @@ #include <private/qlineedit_p.h> #include <private/qwidgetlinecontrol_p.h> #include <qmenu.h> +#include <qlabel.h> #include <qlayout.h> #include <qspinbox.h> +#include <qlistview.h> +#include <qstringlistmodel.h> +#include <qsortfilterproxymodel.h> #include <qdebug.h> #include "qcommonstyle.h" @@ -290,6 +297,8 @@ private slots: void undoRedoAndEchoModes_data(); void undoRedoAndEchoModes(); + void sideWidgets(); + protected slots: void editingFinished(); @@ -4048,5 +4057,39 @@ void tst_QLineEdit::undoRedoAndEchoModes() QCOMPARE(testWidget->text(), expected.at(2)); } +void tst_QLineEdit::sideWidgets() +{ + QWidget testWidget; + QVBoxLayout *l = new QVBoxLayout(&testWidget); + QLineEdit *lineEdit = new QLineEdit(&testWidget); + l->addWidget(lineEdit); + l->addSpacerItem(new QSpacerItem(0, 50, QSizePolicy::Ignored, QSizePolicy::Fixed)); + QImage image(QSize(20, 20), QImage::Format_ARGB32); + image.fill(Qt::yellow); + QAction *iconAction = new QAction(QIcon(QPixmap::fromImage(image)), QString(), lineEdit); + QWidgetAction *label1Action = new QWidgetAction(lineEdit); + label1Action->setDefaultWidget(new QLabel(QStringLiteral("l1"))); + QWidgetAction *label2Action = new QWidgetAction(lineEdit); + label2Action->setDefaultWidget(new QLabel(QStringLiteral("l2"))); + QWidgetAction *label3Action = new QWidgetAction(lineEdit); + label3Action->setDefaultWidget(new QLabel(QStringLiteral("l3"))); + lineEdit->addAction(iconAction, QLineEdit::LeadingPosition); + lineEdit->addAction(label2Action, QLineEdit::LeadingPosition); + lineEdit->addAction(label1Action, QLineEdit::TrailingPosition); + lineEdit->addAction(label3Action, QLineEdit::TrailingPosition); + testWidget.move(300, 300); + testWidget.show(); + QVERIFY(QTest::qWaitForWindowExposed(&testWidget)); + // Arbitrarily add/remove actions, trying to detect crashes. Add QTRY_VERIFY(false) to view the result. + delete label3Action; + lineEdit->removeAction(label2Action); + lineEdit->removeAction(iconAction); + lineEdit->removeAction(label1Action); + lineEdit->removeAction(iconAction); + lineEdit->removeAction(label1Action); + lineEdit->addAction(iconAction); + lineEdit->addAction(iconAction); +} + QTEST_MAIN(tst_QLineEdit) #include "tst_qlineedit.moc" |