0.5.0: nuovo motore di lettura PagedTextItem (QQuickPaintedItem + QTextDocument, pattern harbour-books): pagine misurate dal layout, margini e notch su ogni pagina, posizione per carattere stabile a reflow/rotazione
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
#include "pagedtextitem.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QAbstractTextDocumentLayout>
|
||||
#include <QTextBlock>
|
||||
#include <QTextLayout>
|
||||
#include <QFont>
|
||||
#include <QPointF>
|
||||
#include <QRectF>
|
||||
|
||||
PagedTextItem::PagedTextItem(QQuickItem *parent)
|
||||
: QQuickPaintedItem(parent)
|
||||
, m_fontPixelSize(16)
|
||||
, m_page(0)
|
||||
, m_pendingPosition(-1)
|
||||
, m_layoutPageHeight(1)
|
||||
{
|
||||
// nessun margine interno: i margini li impone la geometria dell'item in QML
|
||||
m_doc.setDocumentMargin(0);
|
||||
applyFont();
|
||||
}
|
||||
|
||||
void PagedTextItem::applyFont()
|
||||
{
|
||||
QFont f = m_doc.defaultFont();
|
||||
if (!m_fontFamily.isEmpty())
|
||||
f.setFamily(m_fontFamily);
|
||||
f.setPixelSize(qMax(4, m_fontPixelSize));
|
||||
m_doc.setDefaultFont(f);
|
||||
}
|
||||
|
||||
void PagedTextItem::setHtml(const QString &html)
|
||||
{
|
||||
if (m_html == html)
|
||||
return;
|
||||
m_html = html;
|
||||
m_page = 0;
|
||||
m_doc.setHtml(m_html);
|
||||
if (!m_baseUrl.isEmpty())
|
||||
m_doc.setBaseUrl(QUrl(m_baseUrl));
|
||||
relayout(false);
|
||||
emit htmlChanged();
|
||||
}
|
||||
|
||||
void PagedTextItem::setBaseUrl(const QString &url)
|
||||
{
|
||||
if (m_baseUrl == url)
|
||||
return;
|
||||
m_baseUrl = url;
|
||||
m_doc.setBaseUrl(QUrl(url));
|
||||
emit baseUrlChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
void PagedTextItem::setFontFamily(const QString &family)
|
||||
{
|
||||
if (m_fontFamily == family)
|
||||
return;
|
||||
// posizione letta PRIMA di toccare il font: il layout e' ancora coerente
|
||||
const int keep = positionAtPage(m_page);
|
||||
m_fontFamily = family;
|
||||
applyFont();
|
||||
relayoutKeeping(keep);
|
||||
emit fontFamilyChanged();
|
||||
}
|
||||
|
||||
void PagedTextItem::setFontPixelSize(int px)
|
||||
{
|
||||
px = qMax(4, px);
|
||||
if (m_fontPixelSize == px)
|
||||
return;
|
||||
// posizione letta PRIMA del reflow (vedi setFontFamily)
|
||||
const int keep = positionAtPage(m_page);
|
||||
m_fontPixelSize = px;
|
||||
applyFont();
|
||||
// reflow: mantiene la posizione di lettura per carattere
|
||||
relayoutKeeping(keep);
|
||||
emit fontPixelSizeChanged();
|
||||
}
|
||||
|
||||
void PagedTextItem::setTextColor(const QColor &color)
|
||||
{
|
||||
if (m_textColor == color)
|
||||
return;
|
||||
m_textColor = color;
|
||||
emit textColorChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
void PagedTextItem::setPage(int page)
|
||||
{
|
||||
const int last = pageCount() - 1;
|
||||
page = qBound(0, page, qMax(0, last));
|
||||
if (m_page == page)
|
||||
return;
|
||||
m_page = page;
|
||||
emit pageChanged();
|
||||
emit positionChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
void PagedTextItem::nextPage()
|
||||
{
|
||||
setPage(m_page + 1);
|
||||
}
|
||||
|
||||
void PagedTextItem::previousPage()
|
||||
{
|
||||
setPage(m_page - 1);
|
||||
}
|
||||
|
||||
void PagedTextItem::goToPosition(int pos)
|
||||
{
|
||||
if (width() <= 0 || height() <= 0) {
|
||||
// geometria non ancora risolta: applica al primo layout
|
||||
m_pendingPosition = pos;
|
||||
return;
|
||||
}
|
||||
setPage(pageForPosition(pos));
|
||||
}
|
||||
|
||||
int PagedTextItem::pageCount() const
|
||||
{
|
||||
const int n = m_doc.pageCount();
|
||||
return n > 0 ? n : 1;
|
||||
}
|
||||
|
||||
int PagedTextItem::characterCount() const
|
||||
{
|
||||
return m_doc.characterCount();
|
||||
}
|
||||
|
||||
int PagedTextItem::position() const
|
||||
{
|
||||
return positionAtPage(m_page);
|
||||
}
|
||||
|
||||
qreal PagedTextItem::pageHeight() const
|
||||
{
|
||||
// altezza dell'ULTIMO layout: se la geometria e' appena cambiata ma il
|
||||
// documento non e' ancora stato ri-impaginato, le letture restano coerenti
|
||||
return m_layoutPageHeight > 0 ? m_layoutPageHeight : qMax(qreal(1), height());
|
||||
}
|
||||
|
||||
int PagedTextItem::positionAtPage(int page) const
|
||||
{
|
||||
if (m_doc.isEmpty() || width() <= 0 || height() <= 0)
|
||||
return 0;
|
||||
QAbstractTextDocumentLayout *layout = m_doc.documentLayout();
|
||||
if (!layout)
|
||||
return 0;
|
||||
const qreal ph = pageHeight();
|
||||
// punto appena dentro la pagina: hitTest dà il carattere piu' vicino
|
||||
const QPointF probe(qMin(qreal(4), width() / 2), page * ph + 1);
|
||||
const int pos = layout->hitTest(probe, Qt::FuzzyHit);
|
||||
if (pos < 0)
|
||||
return 0;
|
||||
return qBound(0, pos, qMax(0, m_doc.characterCount() - 1));
|
||||
}
|
||||
|
||||
int PagedTextItem::pageForPosition(int pos) const
|
||||
{
|
||||
if (m_doc.isEmpty() || width() <= 0 || height() <= 0)
|
||||
return 0;
|
||||
const int len = m_doc.characterCount();
|
||||
const int at = qBound(0, pos, qMax(0, len - 1));
|
||||
// findBlock invece di QTextCursor: metodo const (niente cast sul documento)
|
||||
const QTextBlock block = m_doc.findBlock(at);
|
||||
QRectF rect;
|
||||
if (QAbstractTextDocumentLayout *layout = m_doc.documentLayout())
|
||||
rect = layout->blockBoundingRect(block);
|
||||
qreal y = rect.top();
|
||||
if (block.layout()) {
|
||||
// y della riga che contiene il carattere, relativa al blocco: cosi'
|
||||
// anche i paragrafi lunghi (spezzati su piu' pagine) sono precisi
|
||||
const int inBlock = qBound(0, at - block.position(),
|
||||
qMax(0, block.length() - 1));
|
||||
const QTextLine line = block.layout()->lineForTextPosition(inBlock);
|
||||
if (line.isValid())
|
||||
y += line.y();
|
||||
}
|
||||
return qMax(0, int(y / pageHeight()));
|
||||
}
|
||||
|
||||
void PagedTextItem::clampPage()
|
||||
{
|
||||
const int last = pageCount() - 1;
|
||||
m_page = qBound(0, m_page, qMax(0, last));
|
||||
}
|
||||
|
||||
void PagedTextItem::relayoutKeeping(int pos)
|
||||
{
|
||||
if (width() <= 0 || height() <= 0)
|
||||
return;
|
||||
m_doc.setPageSize(QSizeF(width(), height()));
|
||||
m_layoutPageHeight = qMax(qreal(1), height());
|
||||
if (m_pendingPosition >= 0) {
|
||||
// posizione chiesta prima del layout (ripristino all'apertura)
|
||||
m_page = pageForPosition(m_pendingPosition);
|
||||
m_pendingPosition = -1;
|
||||
} else if (pos >= 0) {
|
||||
m_page = pageForPosition(pos);
|
||||
}
|
||||
clampPage();
|
||||
emit layoutChanged();
|
||||
emit positionChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
void PagedTextItem::relayout(bool keepPosition)
|
||||
{
|
||||
relayoutKeeping(keepPosition ? positionAtPage(m_page) : -1);
|
||||
}
|
||||
|
||||
void PagedTextItem::geometryChanged(const QRectF &newGeometry,
|
||||
const QRectF &oldGeometry)
|
||||
{
|
||||
QQuickPaintedItem::geometryChanged(newGeometry, oldGeometry);
|
||||
if (newGeometry.size() != oldGeometry.size())
|
||||
relayout(true);
|
||||
}
|
||||
|
||||
void PagedTextItem::renderPage(QPainter *painter) const
|
||||
{
|
||||
if (!painter || width() <= 0 || height() <= 0 || m_doc.isEmpty())
|
||||
return;
|
||||
const qreal ph = pageHeight();
|
||||
painter->save();
|
||||
painter->setRenderHint(QPainter::TextAntialiasing, true);
|
||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
||||
painter->setClipRect(QRectF(0, 0, width(), ph));
|
||||
painter->translate(0, -m_page * ph);
|
||||
|
||||
QAbstractTextDocumentLayout::PaintContext ctx;
|
||||
// il clip del contesto e' in coordinate documento
|
||||
ctx.clip = QRectF(0, m_page * ph, width(), ph);
|
||||
if (m_textColor.isValid())
|
||||
ctx.palette.setColor(QPalette::Text, m_textColor);
|
||||
m_doc.documentLayout()->draw(painter, ctx);
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
void PagedTextItem::paint(QPainter *painter)
|
||||
{
|
||||
renderPage(painter);
|
||||
}
|
||||
Reference in New Issue
Block a user