From d86e2c28a02955135759fe3835676c1a16b4396f Mon Sep 17 00:00:00 2001 From: blue Date: Wed, 27 Apr 2022 01:17:53 +0300 Subject: [PATCH] an attempt to display text in a better way with QTextDocument + QTextBrowser --- shared/utils.cpp | 2 +- ui/utils/CMakeLists.txt | 2 - ui/utils/textmeter.cpp | 233 --------------------- ui/utils/textmeter.h | 68 ------ ui/widgets/messageline/feedview.cpp | 6 +- ui/widgets/messageline/messagedelegate.cpp | 104 ++++++--- ui/widgets/messageline/messagedelegate.h | 11 +- 7 files changed, 88 insertions(+), 338 deletions(-) delete mode 100644 ui/utils/textmeter.cpp delete mode 100644 ui/utils/textmeter.h diff --git a/shared/utils.cpp b/shared/utils.cpp index a7a4ecb..518d288 100644 --- a/shared/utils.cpp +++ b/shared/utils.cpp @@ -40,5 +40,5 @@ QString Shared::processMessageBody(const QString& msg) { QString processed = msg.toHtmlEscaped(); processed.replace(urlReg, "\\1"); - return "

" + processed + "

"; + return "

" + processed + "

"; } diff --git a/ui/utils/CMakeLists.txt b/ui/utils/CMakeLists.txt index 823287d..b46d30d 100644 --- a/ui/utils/CMakeLists.txt +++ b/ui/utils/CMakeLists.txt @@ -15,6 +15,4 @@ target_sources(squawk PRIVATE resizer.h shadowoverlay.cpp shadowoverlay.h - textmeter.cpp - textmeter.h ) diff --git a/ui/utils/textmeter.cpp b/ui/utils/textmeter.cpp deleted file mode 100644 index 51c6d54..0000000 --- a/ui/utils/textmeter.cpp +++ /dev/null @@ -1,233 +0,0 @@ -// Squawk messenger. -// Copyright (C) 2019 Yury Gubich -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#include "textmeter.h" -#include -#include - -TextMeter::TextMeter(): - base(), - fonts() -{ -} - -TextMeter::~TextMeter() -{ -} - -void TextMeter::initializeFonts(const QFont& font) -{ - fonts.clear(); - QList supported = base.writingSystems(font.family()); - std::set sup; - std::set added({font.family()}); - for (const QFontDatabase::WritingSystem& system : supported) { - sup.insert(system); - } - fonts.push_back(QFontMetrics(font)); - QString style = base.styleString(font); - - QList systems = base.writingSystems(); - for (const QFontDatabase::WritingSystem& system : systems) { - if (sup.count(system) == 0) { - QStringList families = base.families(system); - if (!families.empty() && added.count(families.first()) == 0) { - QString family(families.first()); - QFont nfont = base.font(family, style, font.pointSize()); - if (added.count(nfont.family()) == 0) { - added.insert(family); - fonts.push_back(QFontMetrics(nfont)); - qDebug() << "Added font" << nfont.family() << "for" << system; - } - } - } - } -} - -QSize TextMeter::boundingSize(const QString& text, const QSize& limits) const -{ -// QString str("ridiculus mus. Suspendisse potenti. Cras pretium venenatis enim, faucibus accumsan ex"); -// bool first = true; -// int width = 0; -// QStringList list = str.split(" "); -// QFontMetrics m = fonts.front(); -// for (const QString& word : list) { -// if (first) { -// first = false; -// } else { -// width += m.horizontalAdvance(QChar::Space); -// } -// width += m.horizontalAdvance(word); -// for (const QChar& ch : word) { -// width += m.horizontalAdvance(ch); -// } -// } -// qDebug() << "together:" << m.horizontalAdvance(str); -// qDebug() << "apart:" << width; -// I cant measure or wrap text this way, this simple example shows that even this gives differen result -// The Qt implementation under it is thousands and thousands lines of code in QTextEngine -// I simply can't get though it - - if (text.size() == 0) { - return QSize (0, 0); - } - Helper current(limits.width()); - for (const QChar& ch : text) { - if (newLine(ch)) { - current.computeNewWord(); - if (current.height == 0) { - current.height = fonts.front().lineSpacing(); - } - current.beginNewLine(); - } else if (visible(ch)) { - bool found = false; - for (const QFontMetrics& metrics : fonts) { - if (metrics.inFont(ch)) { - current.computeChar(ch, metrics); - found = true; - break; - } - } - - if (!found) { - current.computeChar(ch, fonts.front()); - } - } - } - current.computeNewWord(); - current.beginNewLine(); - - int& height = current.size.rheight(); - if (height > 0) { - height -= fonts.front().leading(); - } - - return current.size; -} - -void TextMeter::Helper::computeChar(const QChar& ch, const QFontMetrics& metrics) -{ - int ha = metrics.horizontalAdvance(ch); - if (newWord(ch)) { - if (printOnLineBreak(ch)) { - if (!lineOverflow(metrics, ha, ch)){ - computeNewWord(); - } - } else { - computeNewWord(); - delayedSpaceWidth = ha; - lastSpace = ch; - } - } else { - lineOverflow(metrics, ha, ch); - } -} - -void TextMeter::Helper::computeNewLine(const QFontMetrics& metrics, int horizontalAdvance, const QChar& ch) -{ - if (wordBeganWithTheLine) { - text = word.chopped(1); - width = wordWidth - horizontalAdvance; - height = wordHeight; - } - if (width != metrics.horizontalAdvance(text)) { - qDebug() << "Kerning Error" << width - metrics.horizontalAdvance(text); - } - beginNewLine(); - if (wordBeganWithTheLine) { - word = ch; - wordWidth = horizontalAdvance; - wordHeight = metrics.lineSpacing(); - } - - wordBeganWithTheLine = true; - delayedSpaceWidth = 0; - lastSpace = QChar::Null; -} - -void TextMeter::Helper::beginNewLine() -{ - size.rheight() += height; - size.rwidth() = qMax(size.width(), width); - qDebug() << text; - text = ""; - width = 0; - height = 0; -} - -void TextMeter::Helper::computeNewWord() -{ - width += wordWidth + delayedSpaceWidth; - height = qMax(height, wordHeight); - if (lastSpace != QChar::Null) { - text += lastSpace; - } - text += word; - word = ""; - wordWidth = 0; - wordHeight = 0; - delayedSpaceWidth = 0; - lastSpace = QChar::Null; - wordBeganWithTheLine = false; -} - -bool TextMeter::Helper::lineOverflow(const QFontMetrics& metrics, int horizontalAdvance, const QChar& ch) -{ - wordHeight = qMax(wordHeight, metrics.lineSpacing()); - wordWidth += horizontalAdvance; - word += ch; - if (width + delayedSpaceWidth + wordWidth > maxWidth) { - computeNewLine(metrics, horizontalAdvance, ch); - return true; - } - return false; -} - - -bool TextMeter::newLine(const QChar& ch) -{ - return ch == QChar::LineFeed; -} - -bool TextMeter::newWord(const QChar& ch) -{ - return ch.isSpace() || ch.isPunct(); -} - -bool TextMeter::printOnLineBreak(const QChar& ch) -{ - return ch != QChar::Space; -} - -bool TextMeter::visible(const QChar& ch) -{ - return true; -} - -TextMeter::Helper::Helper(int p_maxWidth): - width(0), - height(0), - wordWidth(0), - wordHeight(0), - delayedSpaceWidth(0), - maxWidth(p_maxWidth), - wordBeganWithTheLine(true), - text(""), - word(""), - lastSpace(QChar::Null), - size(0, 0) -{ -} diff --git a/ui/utils/textmeter.h b/ui/utils/textmeter.h deleted file mode 100644 index 243d989..0000000 --- a/ui/utils/textmeter.h +++ /dev/null @@ -1,68 +0,0 @@ -// Squawk messenger. -// Copyright (C) 2019 Yury Gubich -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#ifndef TEXTMETER_H -#define TEXTMETER_H - -#include -#include - -#include -#include -#include -#include - -class TextMeter -{ -public: - TextMeter(); - ~TextMeter(); - void initializeFonts(const QFont& font); - QSize boundingSize(const QString& text, const QSize& limits) const; - -private: - QFontDatabase base; - std::list fonts; - - struct Helper { - Helper(int maxWidth); - int width; - int height; - int wordWidth; - int wordHeight; - int delayedSpaceWidth; - int maxWidth; - bool wordBeganWithTheLine; - QString text; - QString word; - QChar lastSpace; - QSize size; - - void computeNewLine(const QFontMetrics& metrics, int horizontalAdvance, const QChar& ch); - void computeChar(const QChar& ch, const QFontMetrics& metrics); - void computeNewWord(); - void beginNewLine(); - bool lineOverflow(const QFontMetrics& metrics, int horizontalAdvance, const QChar& ch); - }; - - static bool newLine(const QChar& ch); - static bool newWord(const QChar& ch); - static bool visible(const QChar& ch); - static bool printOnLineBreak(const QChar& ch); - -}; - -#endif // TEXTMETER_H diff --git a/ui/widgets/messageline/feedview.cpp b/ui/widgets/messageline/feedview.cpp index de7f56f..e0c1477 100644 --- a/ui/widgets/messageline/feedview.cpp +++ b/ui/widgets/messageline/feedview.cpp @@ -343,6 +343,7 @@ void FeedView::paintEvent(QPaintEvent* event) QDateTime lastDate; bool first = true; + QRect viewportRect = vp->rect(); for (const QModelIndex& index : toRener) { QDateTime currentDate = index.data(Models::MessageFeed::Date).toDateTime(); option.rect = visualRect(index); @@ -356,7 +357,10 @@ void FeedView::paintEvent(QPaintEvent* event) } first = false; } - bool mouseOver = option.rect.contains(cursor) && vp->rect().contains(cursor); + QRect stripe = option.rect; + stripe.setLeft(0); + stripe.setWidth(viewportRect.width()); + bool mouseOver = stripe.contains(cursor) && viewportRect.contains(cursor); option.state.setFlag(QStyle::State_MouseOver, mouseOver); itemDelegate(index)->paint(&painter, option, index); diff --git a/ui/widgets/messageline/messagedelegate.cpp b/ui/widgets/messageline/messagedelegate.cpp index 4ddecee..1d094fa 100644 --- a/ui/widgets/messageline/messagedelegate.cpp +++ b/ui/widgets/messageline/messagedelegate.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "messagedelegate.h" #include "messagefeed.h" @@ -42,7 +43,7 @@ MessageDelegate::MessageDelegate(QObject* parent): nickFont(), dateFont(), bodyMetrics(bodyFont), - bodyMeter(), + bodyRenderer(new QTextDocument()), nickMetrics(nickFont), dateMetrics(dateFont), buttonHeight(0), @@ -52,11 +53,13 @@ MessageDelegate::MessageDelegate(QObject* parent): bars(new std::map()), statusIcons(new std::map()), pencilIcons(new std::map()), - bodies(new std::map()), + bodies(new std::map()), previews(new std::map()), idsToKeep(new std::set()), clearingWidgets(false) { + bodyRenderer->setDocumentMargin(0); + QPushButton btn(QCoreApplication::translate("MessageLine", "Download")); buttonHeight = btn.sizeHint().height(); buttonWidth = btn.sizeHint().width(); @@ -83,7 +86,7 @@ MessageDelegate::~MessageDelegate() delete pair.second; } - for (const std::pair& pair: *bodies){ + for (const std::pair& pair: *bodies){ delete pair.second; } @@ -98,6 +101,7 @@ MessageDelegate::~MessageDelegate() delete bars; delete bodies; delete previews; + delete bodyRenderer; } void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const @@ -124,8 +128,6 @@ void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& optio opt.displayAlignment = Qt::AlignRight | Qt::AlignTop; } - QSize bodySize = bodyMeter.boundingSize(data.text, opt.rect.size()); - QRect rect; if (ntds) { painter->setFont(nickFont); @@ -168,15 +170,7 @@ void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& optio painter->restore(); QWidget* vp = static_cast(painter->device()); - if (data.text.size() > 0) { - QLabel* body = getBody(data); - body->setParent(vp); - body->setMinimumSize(bodySize); - body->setMaximumSize(bodySize); - body->move(opt.rect.left(), opt.rect.y()); - body->show(); - opt.rect.adjust(0, bodySize.height() + textMargin, 0, 0); - } + paintBody(data, painter, opt); painter->setFont(dateFont); QColor q = painter->pen().color(); QString dateString = data.date.toLocalTime().toString("hh:mm"); @@ -304,7 +298,12 @@ QSize MessageDelegate::sizeHint(const QStyleOptionViewItem& option, const QModel Models::FeedItem data = qvariant_cast(vi); QSize messageSize(0, 0); if (data.text.size() > 0) { - messageSize = bodyMeter.boundingSize(data.text, messageRect.size()); + bodyRenderer->setPlainText(data.text); + bodyRenderer->setTextWidth(messageRect.size().width()); + + QSizeF size = bodyRenderer->size(); + size.setWidth(bodyRenderer->idealWidth()); + messageSize = QSize(qCeil(size.width()), qCeil(size.height())); messageSize.rheight() += textMargin; } @@ -393,7 +392,7 @@ void MessageDelegate::initializeFonts(const QFont& font) nickMetrics = QFontMetrics(nickFont); dateMetrics = QFontMetrics(dateFont); - bodyMeter.initializeFonts(bodyFont); + bodyRenderer->setDefaultFont(bodyFont); Preview::initializeFont(bodyFont); } @@ -573,34 +572,36 @@ QLabel * MessageDelegate::getPencilIcon(const Models::FeedItem& data) const return result; } -QLabel * MessageDelegate::getBody(const Models::FeedItem& data) const +QTextBrowser * MessageDelegate::getBody(const Models::FeedItem& data) const { - std::map::const_iterator itr = bodies->find(data.id); - QLabel* result = 0; + std::map::const_iterator itr = bodies->find(data.id); + QTextBrowser* result = 0; if (itr != bodies->end()) { result = itr->second; } else { - result = new QLabel(); + result = new QTextBrowser(); result->setFont(bodyFont); result->setContextMenuPolicy(Qt::NoContextMenu); - result->setWordWrap(true); + result->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + result->setContentsMargins(0, 0, 0, 0); + //result->viewport()->setAutoFillBackground(false); + result->document()->setDocumentMargin(0); + result->setFrameStyle(0); + result->setLineWidth(0); + //result->setAutoFillBackground(false); + //->setWordWrap(true); + result->setOpenExternalLinks(true); + //result->setTextInteractionFlags(result->textInteractionFlags() | Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse); result->setOpenExternalLinks(true); - result->setTextInteractionFlags(result->textInteractionFlags() | Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse); bodies->insert(std::make_pair(data.id, result)); } - result->setText(Shared::processMessageBody(data.text)); + result->setHtml(Shared::processMessageBody(data.text)); return result; } -void MessageDelegate::beginClearWidgets() -{ - idsToKeep->clear(); - clearingWidgets = true; -} - template void removeElements(std::map* elements, std::set* idsToKeep) { std::set toRemove; @@ -615,6 +616,51 @@ void removeElements(std::map* elements, std::set* idsToKee } } +int MessageDelegate::paintBody(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const +{ + if (data.text.size() > 0) { + bodyRenderer->setHtml(Shared::processMessageBody(data.text)); + bodyRenderer->setTextWidth(option.rect.size().width()); + painter->save(); + painter->translate(option.rect.topLeft()); + bodyRenderer->drawContents(painter); + painter->restore(); + QSize bodySize(qCeil(bodyRenderer->idealWidth()), qCeil(bodyRenderer->size().height())); + + + QTextBrowser* editor = nullptr; + if (option.state.testFlag(QStyle::State_MouseOver)) { + std::set ids({data.id}); + removeElements(bodies, &ids); + editor = getBody(data); + editor->setParent(static_cast(painter->device())); + } else { + std::map::const_iterator itr = bodies->find(data.id); + if (itr != bodies->end()) { + editor = itr->second; + } + } + if (editor != nullptr) { + editor->setMinimumSize(bodySize); + editor->setMaximumSize(bodySize); + editor->move(option.rect.left(), option.rect.y()); + editor->show(); + } + + option.rect.adjust(0, bodySize.height() + textMargin, 0, 0); + return bodySize.width(); + } + return 0; +} + +void MessageDelegate::beginClearWidgets() +{ + idsToKeep->clear(); + clearingWidgets = true; +} + + + void MessageDelegate::endClearWidgets() { if (clearingWidgets) { diff --git a/ui/widgets/messageline/messagedelegate.h b/ui/widgets/messageline/messagedelegate.h index 38ec0ee..b49410f 100644 --- a/ui/widgets/messageline/messagedelegate.h +++ b/ui/widgets/messageline/messagedelegate.h @@ -29,12 +29,13 @@ #include #include #include +#include +#include #include "shared/icons.h" #include "shared/global.h" #include "shared/utils.h" #include "shared/pathcheck.h" -#include "ui/utils/textmeter.h" #include "preview.h" @@ -70,13 +71,15 @@ protected: int paintBar(QProgressBar* bar, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const; int paintPreview(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const; int paintComment(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const; + int paintBody(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const; void paintAvatar(const Models::FeedItem& data, const QModelIndex& index, const QStyleOptionViewItem& option, QPainter* painter) const; void paintBubble(const Models::FeedItem& data, QPainter* painter, const QStyleOptionViewItem& option) const; + QPushButton* getButton(const Models::FeedItem& data) const; QProgressBar* getBar(const Models::FeedItem& data) const; QLabel* getStatusIcon(const Models::FeedItem& data) const; QLabel* getPencilIcon(const Models::FeedItem& data) const; - QLabel* getBody(const Models::FeedItem& data) const; + QTextBrowser* getBody(const Models::FeedItem& data) const; void clearHelperWidget(const Models::FeedItem& data) const; bool needToDrawAvatar(const QModelIndex& index, const Models::FeedItem& data, const QStyleOptionViewItem& option) const; @@ -95,7 +98,7 @@ private: QFont nickFont; QFont dateFont; QFontMetrics bodyMetrics; - TextMeter bodyMeter; + QTextDocument* bodyRenderer; QFontMetrics nickMetrics; QFontMetrics dateMetrics; @@ -107,7 +110,7 @@ private: std::map* bars; std::map* statusIcons; std::map* pencilIcons; - std::map* bodies; + std::map* bodies; std::map* previews; std::set* idsToKeep; bool clearingWidgets;