Support clicking to toggle checkboxes in QTextEdit

Add QAbstractTextDocumentLayout::markerAt(pos) for hit testing.
(Qt Quick TextEdit needs it too.) Finds out whether the position
corresponds to a marker on a paragraph.  I.e. it finds checkboxes in
GitHub-flavored markdown.  This enables editor classes to toggle
checkboxes by clicking them.

Use it in QTextEdit to add the checkbox toggling feature.  Also show the
"pointing finger" cursor when hovering a toggleable checkbox.

Change-Id: I036c967ab45e14c836272eac2cc7c7d652543c89
Reviewed-by: Gatis Paeglis <gatis.paeglis@qt.io>
Shawn Rutledge 2019-05-10 20:20:00 +02:00
parent 12e0c7cbe0
commit 2c8fa9700c
8 changed files with 96 additions and 0 deletions

View File

@ -41,6 +41,7 @@
#include <qtextformat.h>
#include "qtextdocument_p.h"
#include "qtextengine_p.h"
#include "qtextlist.h"
#include "qabstracttextdocumentlayout_p.h"
@ -649,6 +650,36 @@ QTextFormat QAbstractTextDocumentLayout::formatAt(const QPointF &pos) const
return pieceTable->formatCollection()->format(it->format);
}
/*!
\since 5.14
Returns the block (probably a list item) whose \l{QTextBlockFormat::marker()}{marker}
is found at the given position \a pos.
*/
QTextBlock QAbstractTextDocumentLayout::blockWithMarkerAt(const QPointF &pos) const
{
QTextBlock block = document()->firstBlock();
while (block.isValid()) {
if (block.blockFormat().marker() != QTextBlockFormat::NoMarker) {
QRectF blockBr = blockBoundingRect(block);
QTextBlockFormat blockFmt = block.blockFormat();
QFontMetrics fm(block.charFormat().font());
qreal totalIndent = blockFmt.indent() + blockFmt.leftMargin() + blockFmt.textIndent();
if (block.textList())
totalIndent += block.textList()->format().indent() * 40;
QRectF adjustedBr = blockBr.adjusted(totalIndent - fm.height(), 0, totalIndent - blockBr.width(), fm.height() - blockBr.height());
if (adjustedBr.contains(pos)) {
//qDebug() << "hit block" << block.text() << blockBr << adjustedBr << "marker" << block.blockFormat().marker()
// << "font" << block.charFormat().font() << "adj" << lineHeight << totalIndent;
if (block.blockFormat().hasProperty(QTextFormat::BlockMarker))
return block;
}
}
block = block.next();
}
return QTextBlock();
}
/*!
\fn QRectF QAbstractTextDocumentLayout::frameBoundingRect(QTextFrame *frame) const

View File

@ -87,6 +87,7 @@ public:
QString anchorAt(const QPointF& pos) const;
QString imageAt(const QPointF &pos) const;
QTextFormat formatAt(const QPointF &pos) const;
QTextBlock blockWithMarkerAt(const QPointF &pos) const;
virtual int pageCount() const = 0;
virtual QSizeF documentSize() const = 0;

View File

@ -167,6 +167,7 @@ void QTextEditPrivate::init(const QString &html)
QObject::connect(control, SIGNAL(copyAvailable(bool)), q, SIGNAL(copyAvailable(bool)));
QObject::connect(control, SIGNAL(selectionChanged()), q, SIGNAL(selectionChanged()));
QObject::connect(control, SIGNAL(cursorPositionChanged()), q, SLOT(_q_cursorPositionChanged()));
QObject::connect(control, SIGNAL(blockMarkerHovered(QTextBlock)), q, SLOT(_q_hoveredBlockWithMarkerChanged(QTextBlock)));
QObject::connect(control, SIGNAL(textChanged()), q, SLOT(updateMicroFocus()));
@ -187,6 +188,7 @@ void QTextEditPrivate::init(const QString &html)
vbar->setSingleStep(20);
viewport->setBackgroundRole(QPalette::Base);
q->setMouseTracking(true);
q->setAcceptDrops(true);
q->setFocusPolicy(Qt::StrongFocus);
q->setAttribute(Qt::WA_KeyCompression);
@ -228,6 +230,21 @@ void QTextEditPrivate::_q_cursorPositionChanged()
#endif
}
void QTextEditPrivate::_q_hoveredBlockWithMarkerChanged(const QTextBlock &block)
{
Q_Q(QTextEdit);
Qt::CursorShape cursor = cursorToRestoreAfterHover;
if (block.isValid() && !q->isReadOnly()) {
QTextBlockFormat::MarkerType marker = block.blockFormat().marker();
if (marker != QTextBlockFormat::NoMarker) {
if (viewport->cursor().shape() != Qt::PointingHandCursor)
cursorToRestoreAfterHover = viewport->cursor().shape();
cursor = Qt::PointingHandCursor;
}
}
viewport->setCursor(cursor);
}
void QTextEditPrivate::pageUpDown(QTextCursor::MoveOperation op, QTextCursor::MoveMode moveMode)
{
QTextCursor cursor = control->textCursor();

View File

@ -331,6 +331,7 @@ private:
Q_PRIVATE_SLOT(d_func(), void _q_adjustScrollbars())
Q_PRIVATE_SLOT(d_func(), void _q_ensureVisible(const QRectF &))
Q_PRIVATE_SLOT(d_func(), void _q_cursorPositionChanged())
Q_PRIVATE_SLOT(d_func(), void _q_hoveredBlockWithMarkerChanged(const QTextBlock &))
friend class QTextEditControl;
friend class QTextDocument;
friend class QWidgetTextControl;

View File

@ -104,6 +104,7 @@ public:
void _q_currentCharFormatChanged(const QTextCharFormat &format);
void _q_cursorPositionChanged();
void _q_hoveredBlockWithMarkerChanged(const QTextBlock &block);
void updateDefaultTextOption();
@ -136,6 +137,8 @@ public:
QString placeholderText;
Qt::CursorShape cursorToRestoreAfterHover = Qt::IBeamCursor;
#ifdef QT_KEYPAD_NAVIGATION
QBasicTimer deleteAllTimer;
#endif

View File

@ -1581,6 +1581,11 @@ void QWidgetTextControlPrivate::mousePressEvent(QEvent *e, Qt::MouseButton butto
e->ignore();
return;
}
bool wasValid = blockWithMarkerUnderMouse.isValid();
blockWithMarkerUnderMouse = q->blockWithMarkerAt(pos);
if (wasValid != blockWithMarkerUnderMouse.isValid())
emit q->blockMarkerHovered(blockWithMarkerUnderMouse);
cursorIsFocusIndicator = false;
const QTextCursor oldSelection = cursor;
@ -1599,6 +1604,8 @@ void QWidgetTextControlPrivate::mousePressEvent(QEvent *e, Qt::MouseButton butto
selectedBlockOnTrippleClick = cursor;
anchorOnMousePress = QString();
blockWithMarkerUnderMouse = QTextBlock();
emit q->blockMarkerHovered(blockWithMarkerUnderMouse);
trippleClickTimer.stop();
} else {
@ -1738,6 +1745,11 @@ void QWidgetTextControlPrivate::mouseMoveEvent(QEvent *e, Qt::MouseButton button
}
selectionChanged(true);
repaintOldAndNewSelection(oldSelection);
} else {
bool wasValid = blockWithMarkerUnderMouse.isValid();
blockWithMarkerUnderMouse = q->blockWithMarkerAt(mousePos);
if (wasValid != blockWithMarkerUnderMouse.isValid())
emit q->blockMarkerHovered(blockWithMarkerUnderMouse);
}
sendMouseEventToInputContext(e, QEvent::MouseMove, button, mousePos, modifiers, buttons, globalPos);
@ -1787,6 +1799,26 @@ void QWidgetTextControlPrivate::mouseReleaseEvent(QEvent *e, Qt::MouseButton but
emit q->microFocusChanged();
}
// toggle any checkbox that the user clicks
if ((interactionFlags & Qt::TextEditable) && (button & Qt::LeftButton) &&
(blockWithMarkerUnderMouse.isValid()) && !cursor.hasSelection()) {
QTextBlock markerBlock = q->blockWithMarkerAt(pos);
if (markerBlock == blockWithMarkerUnderMouse) {
auto fmt = blockWithMarkerUnderMouse.blockFormat();
switch (fmt.marker()) {
case QTextBlockFormat::Unchecked :
fmt.setMarker(QTextBlockFormat::Checked);
break;
case QTextBlockFormat::Checked:
fmt.setMarker(QTextBlockFormat::Unchecked);
break;
default:
break;
}
cursor.setBlockFormat(fmt);
}
}
if (interactionFlags & Qt::LinksAccessibleByMouse) {
if (!(button & Qt::LeftButton))
return;
@ -2385,6 +2417,12 @@ QString QWidgetTextControl::anchorAtCursor() const
return d->anchorForCursor(d->cursor);
}
QTextBlock QWidgetTextControl::blockWithMarkerAt(const QPointF &pos) const
{
Q_D(const QWidgetTextControl);
return d->doc->documentLayout()->blockWithMarkerAt(pos);
}
bool QWidgetTextControl::overwriteMode() const
{
Q_D(const QWidgetTextControl);

View File

@ -150,6 +150,8 @@ public:
QString anchorAtCursor() const;
QTextBlock blockWithMarkerAt(const QPointF &pos) const;
bool overwriteMode() const;
void setOverwriteMode(bool overwrite);
@ -242,6 +244,7 @@ Q_SIGNALS:
void microFocusChanged();
void linkActivated(const QString &link);
void linkHovered(const QString &);
void blockMarkerHovered(const QTextBlock &block);
void modificationChanged(bool m);
public:

View File

@ -55,6 +55,7 @@
#include "QtGui/qtextdocumentfragment.h"
#include "QtGui/qtextcursor.h"
#include "QtGui/qtextformat.h"
#include "QtGui/qtextobject.h"
#if QT_CONFIG(menu)
#include "QtWidgets/qmenu.h"
#endif
@ -227,6 +228,7 @@ public:
QString highlightedAnchor; // Anchor below cursor
QString anchorOnMousePress;
QTextBlock blockWithMarkerUnderMouse;
bool hadSelectionOnMousePress;
bool ignoreUnusedNavigationEvents;