QTextLayout::drawCursor() does not work when subclassing QAbstractTextDocumentLayout - qt

currently QTextEdit and QPlainTextEdit does not meet my requirements, so I need to subclass QAbstractTextDocumentLayout to provide custom layout of the document. I reference QPlainTextDocumentLayout and QTextDocumentLayout a lot, and finally got a simple layout to display the text. However, I couldn't see the cursor in QTextEdit, which should be blinking. I need help to figure this out.
I am using Qt 5.9.1. The simple project is here. The draw() function of VTextDocumentLayout looks like this:
void VTextDocumentLayout::draw(QPainter *p_painter, const PaintContext &p_context)
{
qDebug() << "VTextDocumentLayout draw()" << p_context.clip << p_context.cursorPosition << p_context.selections.size();
// Find out the blocks.
int first, last;
blockRangeFromRect(p_context.clip, first, last);
if (first == -1) {
return;
}
QTextDocument *doc = document();
QPointF offset(m_margin, m_blocks[first].m_offset);
QTextBlock block = doc->findBlockByNumber(first);
QTextBlock lastBlock = doc->findBlockByNumber(last);
qreal maximumWidth = m_width;
while (block.isValid()) {
const BlockInfo &info = m_blocks[block.blockNumber()];
const QRectF &rect = info.m_rect;
QTextLayout *layout = block.layout();
if (!block.isVisible()) {
offset.ry() += rect.height();
if (block == lastBlock) {
break;
}
block = block.next();
continue;
}
QTextBlockFormat blockFormat = block.blockFormat();
QBrush bg = blockFormat.background();
if (bg != Qt::NoBrush) {
fillBackground(p_painter, rect, bg);
}
auto selections = formatRangeFromSelection(block, p_context.selections);
QPen oldPen = p_painter->pen();
p_painter->setPen(p_context.palette.color(QPalette::Text));
layout->draw(p_painter,
offset,
selections,
p_context.clip.isValid() ? p_context.clip : QRectF());
// Draw the cursor.
int blpos = block.position();
int bllen = block.length();
bool drawCursor = p_context.cursorPosition >= blpos
&& p_context.cursorPosition < blpos + bllen;
Q_ASSERT(p_context.cursorPosition >= -1);
if (drawCursor) {
int cpos = p_context.cursorPosition;
cpos -= blpos;
qDebug() << "draw cursor" << block.blockNumber() << blpos << bllen << p_context.cursorPosition << cpos;
layout->drawCursor(p_painter, offset, cpos);
}
p_painter->setPen(oldPen);
offset.ry() += rect.height();
if (block == lastBlock) {
break;
}
block = block.next();
}
}
I called layout->drawCursor() to draw the cursor but this function seems to do nothing.
Any help is appreciated! Thanks!
Update:
Add the log as following:
VTextDocumentLayout draw() QRectF(67,0 9x13) 19 0
block range 0 1
draw cursor 1 5 15 19 14
blockBoundingRect() 1 13 QRectF(0,0 75x17)
VTextDocumentLayout draw() QRectF(67,0 9x13) -1 0
block range 0 1
blockBoundingRect() 1 13 QRectF(0,0 75x17)
VTextDocumentLayout draw() QRectF(67,0 9x13) 19 0
When running this project in Linux, I coundn't see the cursor. However, when running it in Windows, I could see the cursor but not blinking.
Update:
It seems that QTextEdit pass a wrong clip rect to the layout. If I just inserted one line of text (only one block within the document), I could see the blinking cursor. Quite strange!!!

The default value of PaintContext class cursorPosition is -1.
http://doc.qt.io/qt-4.8/qabstracttextdocumentlayout-paintcontext.html#cursorPosition-var
If any default value is not provided to the cursor position ever (it is a public variable), then my guess is value remains -1, and drawCursor is always false (as the block position index minimum value is zero in worst case).
Try setting some default value to PaintContext::cursorPosition, which may display the cursor.

When subclassing QAbstractTextDocumentLayout, blockBoundingRect() should return the geometry of the block, not just the rect as what QPlainTextDocumentLayout does.
In one word, QPlainTextDocumentLayout provides a BAD example for subsclassing QAbstractTextDocumentLayout.

Related

Reading Rich Text Character and Block Formatting

I have rich text items implemented using QGraphicsTextItem
To set font size, for example:
void set (int fontSize) {
QTextCursor _cursor = textCursor();
QTextCharFormat _format;
_format.setFontPointSize(fontSize);
_cursor.mergeCharFormat(_format);
setTextCursor(_cursor); }
A lot more complicated is to read the font size.
Assuming I have a selection, I must iterate through the document, through all QTextBlock, QTextFragment, reading the QTextCharFormat ...
But the simple option, if there is no selection, just reading the font size at cursor:
int get () {
return textCursor().charFormat().fontPointSize(); }
This works, but I found 3 issues:
1) Setting font size by QGraphicsTextItem properties:
QFont f = font();
f.setPointSize(20);
setFont(f);
this returns 0 by my get function above. To set the font size for the entire item, I have to use the same method as in the set function.
Shouldn't the setFont method set a font that can be read from the QTextCursor ?
2) setHtml can set formatting - but I don't see any way to read that formatting
How can I read the rich text formatting from an html fragment ? Is the only posiblity, parsing the html ?
3) (my current stumbling block)
Copy formatted text from an outside source and paste in the QGraphicsTextItem seems to maintain the formatting of the source - but how can I read that formatting ?
The get method above reads font size 0 if the text was pasted from outside.
font().pointSize() always returns 8. (I have not set it so I imagine that is a default)
Is there another method to read the text format ?
is the clipboard text formatted using html ?
How can I find the font size (or any other formatting) from the pasted text ?
(The same questions apply to block formatting, like alignment).
I think most of your problems could be solved by getting the QTextDocument for your QGraphicsTextItem object and work with it. QTextDocument and its methods (like QTextFormat::property(int propertyId)) can help you to get a lot of properties for your text.
1) If you set the size using the QFont object, you should get the size using the same way.
2) When you set the text using html, QGraphicsTextItem::font() is not useful so you need to get the QTextDocument and use their functions instead.
3) Same as 2. I think... because I don't have your code to test it :)
Well, here you have a code as an example. I hope this answer helps you.
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsTextItem>
#include <QTextCursor>
#include <QTextCharFormat>
#include <QFont>
#include <QDebug>
#include <QTextDocument>
#include <QTextBlock>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsScene scene;
QGraphicsView view(&scene);
/* ITEM 1 */
QGraphicsTextItem* item_1 = new QGraphicsTextItem("QGraphicsTextItem 1");
item_1->setTextInteractionFlags(Qt::TextEditorInteraction);
QFont f = item_1->font();
f.setPointSize(30);
item_1->setFont(f);
qDebug() << "textCursor().position() (returns 0): " <<
item_1->textCursor().position();
qDebug() << "textCursor().charFormat().fontPointSize() (returns 0): " <<
item_1->textCursor().charFormat().fontPointSize();
qDebug() << "font().pointSize() (returns 30 - OK!): " <<
item_1->font().pointSize();
QTextDocument* doc = item_1->document();
f = doc->defaultFont();
qDebug() << "pointSize (returns 30 - OK!): " << f.pointSize();
scene.addItem(item_1);
/* ITEM 2 */
QGraphicsTextItem* item_2 = new QGraphicsTextItem();
item_2->setPos(0, 50);
item_2->setHtml("<html><head/><body><p>"
"<span style=\"font-size:14pt; font-weight:600;\">QGraphics</span>"
"<span style=\"font-size:24pt; font-weight:600;\">TextItem 2</span>"
"</p></body></html>");
qDebug() << "font().pointSize() (returns 8, the default value): "
<< item_2->font().pointSize();
doc = item_2->document();
f = doc->defaultFont();
qDebug() << "pointSize (returns 8, the default value): " << f.pointSize();
QVector<QTextFormat> formats = doc->allFormats();
QVectorIterator<QTextFormat> i(formats);
while (i.hasNext()) {
QTextFormat format = i.next();
if (format.property(QTextFormat::FontPointSize).isValid())
qDebug() << "format.property (returns 14 or 24): " <<
format.property(QTextFormat::FontPointSize).toInt();
}
/*
* Get the block of text. In this example, we only have one block, but
* two text fragments (see below)
*/
QTextBlock text_block = item_2->document()->findBlock(1);
QTextBlock::iterator it;
for (it = text_block.begin(); !(it.atEnd()); ++it) {
QTextFragment currentFragment = it.fragment();
if (currentFragment.isValid())
qDebug() << "currentFragment.text(): " << currentFragment.text();
qDebug() << "currentFragment.charFormat().font().pointSize() "
"(returns 14 or 24, depending on"
"the current text fragment): " <<
currentFragment.charFormat().font().pointSize();
}
scene.addItem(item_2);
view.setFixedSize(640, 480);
view.show();
return a.exec();
}

QTreeView::dropMimeData - setting values for a new child

I am trying to drop some mime encoded text onto a tree view. It is working - the dropMimeData() method is called, I can decode the mime data into the strings that were dropped, I can insert a child into the model which shows up in the view, but ... I can't find a way to set the text value of the new item/row to the string dragged and dropped (or any string for that matter).
Here is some of the code i've tried inside the dropMimeData() method:
if ( ( row == -1) && (column == -1) && parent.isValid() ) {
int mdlidx = this->data(parent, Qt::DisplayRole).ModelIndex;
qDebug() << "mdlidx: " << mdlidx;
// treet text - the text of the cell that gets dropped onto
QString tt = this->data(parent, Qt::DisplayRole).toString();
qDebug() << "tree text: " << tt;
TreeItem *item = this->getItem(parent);
int ccnt = item->childCount();
qDebug() << "ccnt: " << ccnt ;
if ( item->insertChildren(0, 1, 0) ) {
qDebug() << "Child Inserted";
// how do I access the new child item here ???
} else {
qDebug() << "Failed";
}
How do I access the new child item in order to set the text that would be visible in the view?
I'm using the QStandardItemModel, if that makes any difference.
My solution to this is to create a signal and slot - I emot the signal in the dropMimeData() method and the slot is in a part of the code that has the view and model, so can easily update the model.
I send the mime data and the parent across using the signal.
I'm not sure if this is the correct way of doing this, but it is working.

Drawing QImage on a QPainter which has inverted y-axis

I have a scene with an inverted y-axis. Everything is correctly drawn except QImages.
I use drawIage() as:
QRectF aWorldRect = ...
QRectF anImageRect = QRectF(0, 0, theQImage.width(), theQImage.height())
thePainter->drawImage(aWorldRect, theQImage, anImageRect;
I get undefined graphics outside (to the top of) where the image should be displayed. This is normal because y-axis is inverted. So I expected something like that may fix the issue:
QRectF anImageRect = QRectF(0, 0, imgWidth, -imgHeight)
It has the same effect. If I do aWorldRect = aWorldRect.noralized() before calling drawImage(), I get the image in the correct rectangle but mirrored so I did aQImage = aQImage.mirrored(). Now the image is correctly displayed in the correct rectangle
I consider this a workaround which I don't like to keep. So, can someone tell me what should be done to get the image displayed, the right way?
Update
Here I put a minimal sample of my problem that is ready to compile:
Update 2014-04-09 10:05 EET
Updated the sample code little bit to make really work using the workaround
#include <QtGui>
const int WIDTH = 640;
const int HEIGHT = 480;
class View : public QGraphicsView
{
protected:
void drawBackground(QPainter *p, const QRectF & rect)
{
QImage img = QImage("/usr/share/backgrounds/images/stone_bird.jpg"); // or any other
/* The next three lines makes everything displayed correctly but
should be considered a workaround */
/* I ignore the rect that is passed to the function on purpose */
QRectF imageRect = QRectF(QPointF(0, 0), QPointF(img.width(), img.height()));
QRectF theSceneRect = sceneRect().normalized();
p->drawImage(theSceneRect, img.mirrored(), imageRect);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
View w;
/* I don't want to change the code below */
w.setScene(new QGraphicsScene(QRectF(QPointF(0, HEIGHT), QPointF(WIDTH, 0))));
w.scale(1, -1);
w.scene()->addLine(0, HEIGHT, WIDTH, 0);
w.showMaximized();
return a.exec();
}
The approach of reversing the Y coordinate value is right but the implementation was faulty.
QRectF's documentation shows that it takes (x, y, width, height). Giving height as negative makes little sense. Instead try the other constructor which takes topLeft and bottomRight.
QRectF anImageRect(QPointF(0.0f, -imgHeight), QPointF(imageWidth, 0.0f));
EDIT:
It seems that the only drawings like line, arc, etc. are affected by the scale (1, -1) transform you set on the view. drawImage continues to render upside down due to the scale set. The simple fix is to set the scale back to (1, -1). Here's the updated code:
void drawBackground(QPainter *p, const QRectF & rect)
{
QImage img = QImage("/usr/share/backgrounds/images/stone_bird.jpg");
// backup the current transform set (which has the earlier scale of (1, -1))
const QTransform oldTransform = p->transform();
// set the transform back to identity to make the Y axis go from top to bottom
p->setTransform(QTransform());
// draw
QRectF theSceneRect = sceneRect().normalized();
p->drawImage(theSceneRect, img);
// revert back to the earlier transform
p->setTransform(oldTransform);
}
Updated on 2014-04-14 14:35 EET
I could finally solve the problem reliably by replacing the two lines
QRectF theSceneRect = sceneRect().normalized();
p->drawImage(theSceneRect, img.mirrored(), imageRect);
of my question to
QRectF theSceneRect = sceneRect(); // Not normalized. It is no more workaround :)
qreal x = theSceneRect.x();
qreal y = theSceneRect.y();
qreal w = theSceneRect.width();
qreal h = theSceneRect.height();
qreal sx = imageRect.x();
qreal sy = imageRect.y();
qreal sw = imageRect.width();
qreal sh = imageRect.height();
p->translate(x, y);
p->scale(w / sw, h / sh);
p->setBackgroundMode(Qt::TransparentMode);
p->setRenderHint(QPainter::Antialiasing, p->renderHints() &
QPainter::SmoothPixmapTransform);
QBrush brush(img);
p->setBrush(brush);
p->setPen(Qt::NoPen);
p->setBrushOrigin(QPointF(-sx, -sy));
p->drawRect(QRectF(0, 0, sw, sh));
p->restore();
This is inspired by the implementation of the QPainter::drawImage() which is not reliable in such cases due to many if statements handling rectangles with negative values of width or height.
It would be better if I made the solution in another function but I kept it this way to be more compatible with the code in my question.

QGraphicsItem: Why no `stackAfter` method?

I'm having an annoying time trying to get around the 'recommended' way of doing something.
So, I have a stack of cards. I want to make it so that when I deal a card, it becomes the last-drawn object of the entire scene (typical bring_to_front functionality).
The recommended way to do this is just adding to the object's zValue until it is larger than all the rest, but I was hoping to do away with rather "lazy" integers running around all over the place with judicious use of the stackBefore method, which simulates reorganizing the order in which objects were added to the scene.
This works perfectly fine when I shuffle my cards in a limited set (get list of selected items, random.shuffle, for item do item.stackBefore(next item)), but it is certainly not working when it comes to bubbling the card to the top of the entire scene.
I considered adding a copy of the object to the scene and then removing the original, but it just seems like I should be able to do stackAfter like I would when using a Python list (or insertAt or something).
Sample code:
def deal_items(self):
if not self.selection_is_stack():
self.statusBar().showMessage("You must deal from a stack")
return
item_list = self.scene.sorted_selection()
for i,item in enumerate(item_list[::-1]):
width = item.boundingRect().width()
item.moveBy(width+i*width*0.6,0)
another_list = self.scene.items()[::-1]
idx = another_list.index(item)
for another_item in another_list[idx+1:]:
another_item.stackBefore(item)
This works. It just seems somewhat... ugly.
self.scene.items returns the items in the stacking order (link). So if you want to stackAfter an item, you can just query the z value of the current topmost item and then set the z value of the new topmost card to a value one larger.
item.setZValue(self.scene.items().first().zValue() + 1)
Hope that helps.
Edit added src for stackBefore and setZValue from http://gitorious.org/qt/
src/gui/graphicsview/qgraphicsitem.cpp
void QGraphicsItem::stackBefore(const QGraphicsItem *sibling)
{
if (sibling == this)
return;
if (!sibling || d_ptr->parent != sibling->parentItem()) {
qWarning("QGraphicsItem::stackUnder: cannot stack under %p, which must be a sibling", sibling);
return;
}
QList<QGraphicsItem *> *siblings = d_ptr->parent
? &d_ptr->parent->d_ptr->children
: (d_ptr->scene ? &d_ptr->scene->d_func()->topLevelItems : 0);
if (!siblings) {
qWarning("QGraphicsItem::stackUnder: cannot stack under %p, which must be a sibling", sibling);
return;
}
// First, make sure that the sibling indexes have no holes. This also
// marks the children list for sorting.
if (d_ptr->parent)
d_ptr->parent->d_ptr->ensureSequentialSiblingIndex();
else
d_ptr->scene->d_func()->ensureSequentialTopLevelSiblingIndexes();
// Only move items with the same Z value, and that need moving.
int siblingIndex = sibling->d_ptr->siblingIndex;
int myIndex = d_ptr->siblingIndex;
if (myIndex >= siblingIndex) {
siblings->move(myIndex, siblingIndex);
// Fixup the insertion ordering.
for (int i = 0; i < siblings->size(); ++i) {
int &index = siblings->at(i)->d_ptr->siblingIndex;
if (i != siblingIndex && index >= siblingIndex && index <= myIndex)
++index;
}
d_ptr->siblingIndex = siblingIndex;
for (int i = 0; i < siblings->size(); ++i) {
int &index = siblings->at(i)->d_ptr->siblingIndex;
if (i != siblingIndex && index >= siblingIndex && index <= myIndex)
siblings->at(i)->d_ptr->siblingOrderChange();
}
d_ptr->siblingOrderChange();
}
}
void QGraphicsItem::setZValue(qreal z)
{
const QVariant newZVariant(itemChange(ItemZValueChange, z));
qreal newZ = newZVariant.toReal();
if (newZ == d_ptr->z)
return;
if (d_ptr->scene && d_ptr->scene->d_func()->indexMethod != QGraphicsScene::NoIndex) {
// Z Value has changed, we have to notify the index.
d_ptr->scene->d_func()->index->itemChange(this, ItemZValueChange, &newZ);
}
d_ptr->z = newZ;
if (d_ptr->parent)
d_ptr->parent->d_ptr->needSortChildren = 1;
else if (d_ptr->scene)
d_ptr->scene->d_func()->needSortTopLevelItems = 1;
if (d_ptr->scene)
d_ptr->scene->d_func()->markDirty(this, QRectF(), /*invalidateChildren=*/true);
itemChange(ItemZValueHasChanged, newZVariant);
if (d_ptr->flags & ItemNegativeZStacksBehindParent)
setFlag(QGraphicsItem::ItemStacksBehindParent, z < qreal(0.0));
if (d_ptr->isObject)
emit static_cast<QGraphicsObject *>(this)->zChanged();
}

Does Qt have a way to find bounding box of an image?

Given a .png image with a transparent background, I want to find the bounding box of the non-transparent data. Using nested for loops with QImage.pixel() is painfully slow. Is there a built-in method of doing this in Qt?
There is one option that involves using a QGraphicsPixmapItem and querying for the bounding box of the opaque area (QGraphicsPixmapItem::opaqueArea().boundingRect()). Not sure if it is the best way but it works :) It might be worth digging into Qt's source code to see what code is at the heart of it.
The following code will print out the width and height of the image followed by the width and height of the opaque portions of the image:
QPixmap p("image.png");
QGraphicsPixmapItem *item = new QGraphicsPixmapItem(p);
std::cout << item->boundingRect().width() << "," << item->boundingRect().height() << std::endl;
std::cout << item->opaqueArea().boundingRect().width() << "," << item->opaqueArea().boundingRect().height() << std::endl;
If pixel() is too slow for you, consider more efficient row-wise data adressing, given a QImage p:
int l =p.width(), r = 0, t = p.height(), b = 0;
for (int y = 0; y < p.height(); ++y) {
QRgb *row = (QRgb*)p.scanLine(y);
bool rowFilled = false;
for (int x = 0; x < p.width(); ++x) {
if (qAlpha(row[x])) {
rowFilled = true;
r = std::max(r, x);
if (l > x) {
l = x;
x = r; // shortcut to only search for new right bound from here
}
}
}
if (rowFilled) {
t = std::min(t, y);
b = y;
}
}
I doubt it will get any faster than this.
The easiest and also relatively fast solution is to do as follows:
QRegion(QBitmap::fromImage(image.createMaskFromColor(0x00000000))).boundingRect()
If you have a QPixmap rather than QImage, then you can use:
QRegion(pixmap.createMaskFromColor(Qt::transparent)).boundingRect()
QPixmap::createMaskFromColor internally will convert the pixmap to an image and do the same as above. An even shorter solution for QPixmap is:
QRegion(pixmap.mask()).boundingRect()
In this case, a QPixmap without alpha channel will result in an empty region, so you may need to check for that explicitly. Incidentally, this is also what QGraphicsPixmapItem::opaqueArea mentioned by #Arnold Spence is based on.
You may also want to try QImage::createAlphaMask, though the cutoff point will not be at 0 alpha but rather somewhere at half opacity.

Resources