From cd76b6a1d63d21d038dee2db3cfc41c79f080262 Mon Sep 17 00:00:00 2001 From: Laurent Trinques Date: Sat, 30 May 2026 18:48:28 +0200 Subject: [PATCH] This adds native, clickable hyperlinks to PDF exports: cross-references jump directly to the related component on its folio, framing the target element. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a project is exported to PDF, every cross-reference becomes an internal link. Four kinds are covered: - **Master → contact**: the contact list on a coil/relay (`CrossRefItem`) - **Folio report → report**: report element labels (`DynamicElementTextItem`) - **Slave → master**: the `(folio-position)` reference shown on a slave (both standalone `DynamicElementTextItem` and grouped `ElementTextItemGroup`) Clicking a link navigates **inside** the open document (no new viewer instance) and zooms to frame the target element. 1. **Injection** (`printDiagram`, only when the paint engine is a `QPdfEngine`): link rectangles are added with `QPdfEngine::drawHyperlink()`. The scene→page mapping is rebuilt to match exactly what `QGraphicsScene::render()` does (top-left anchored, `KeepAspectRatio`, **no centering**), and rectangles are passed in device pixels — `pageMatrix()` already applies the 72/resolution scale and Y-flip internally. 2. Each link URL encodes the target page and the target element's rectangle, in PDF points on its own page: `#page=N&fitr=L_B_R_T`. 3. **Post-processing** (`pdfConvertUriToGoTo`, run after the painter is closed): the `/S /URI` annotations are rewritten to native `/S /GoTo` actions with a `/D [pageObj 0 R /FitR L B R T]` destination, and the xref table is rebuilt. Pages are enumerated from the `/Pages /Kids` tree (reliable), not by scanning for `/Type /Page` in raw bytes. - `sources/print/projectprintwindow.{cpp,h}` — injection + post-processing - `sources/qetgraphicsitem/crossrefitem.{cpp,h}` — `hoveredContactsMap()` accessor; store text rect for hit area - `sources/qetgraphicsitem/dynamicelementtextitem.h` — `slaveXrefItem()` / `masterElement()` accessors - `sources/qetgraphicsitem/elementtextitemgroup.h` — `slaveXrefItem()` accessor - `qelectrotech.pro`, `cmake/qet_compilation_vars.cmake` — enable Qt gui-private headers (``) - **Fit-to-page mode only.** Links are not injected in tiled mode (multiple pages per folio), which would require a per-tile transform. - Uses Qt private API (`QPdfEngine::drawHyperlink`), stable since Qt 4 but not part of the public API; the build links against `gui-private`. - Page-tree enumeration assumes the flat `/Kids` array Qt produces (no nested page trees). - The frame zoom is controlled by two constants in `destRectPdf` (`pad`, `minSide`) and can be tuned. - Tested on Qt5; the `/Kids` parsing and `pageMatrix` behaviour are identical on Qt6. --- cmake/qet_compilation_vars.cmake | 1 + qelectrotech.pro | 6 +- sources/print/projectprintwindow.cpp | 450 +++++++++++++++++- sources/print/projectprintwindow.h | 3 +- sources/qetgraphicsitem/crossrefitem.cpp | 6 +- sources/qetgraphicsitem/crossrefitem.h | 6 + .../qetgraphicsitem/dynamicelementtextitem.h | 3 + .../qetgraphicsitem/elementtextitemgroup.h | 2 + 8 files changed, 470 insertions(+), 7 deletions(-) diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 0ac2f64f0..777e946d3 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -29,6 +29,7 @@ set(QET_COMPONENTS set(QET_PRIVATE_LIBRARIES Qt::PrintSupport Qt::Gui + Qt::GuiPrivate # Required for QPdfEngine::drawHyperlink (PDF internal links) Qt::Xml Qt::Svg Qt::Sql diff --git a/qelectrotech.pro b/qelectrotech.pro index 41ff33226..fb2fedbec 100644 --- a/qelectrotech.pro +++ b/qelectrotech.pro @@ -230,7 +230,11 @@ RESOURCES += qelectrotech.qrc TRANSLATIONS += lang/*.ts # Modules Qt utilises par l'application -QT += xml svg network sql widgets printsupport concurrent KWidgetsAddons KCoreAddons +QT += xml svg network sql widgets printsupport concurrent KWidgetsAddons KCoreAddons gui-private + +# Private Qt GUI headers (needed for QPdfEngine::drawHyperlink) +# gui-private should add this automatically, but some distros need it explicit +INCLUDEPATH += $$[QT_INSTALL_HEADERS]/QtGui/$$[QT_VERSION]/QtGui # UI DESIGNER FILES AND GENERATION SOURCES FILES FORMS += $$files(sources/richtext/*.ui) \ diff --git a/sources/print/projectprintwindow.cpp b/sources/print/projectprintwindow.cpp index 7eb7a554f..27364d26d 100644 --- a/sources/print/projectprintwindow.cpp +++ b/sources/print/projectprintwindow.cpp @@ -21,9 +21,16 @@ #include "../qeticons.h" #include "../qetproject.h" #include "../qetversion.h" +#include "../qetgraphicsitem/crossrefitem.h" +#include "../qetgraphicsitem/dynamicelementtextitem.h" +#include "../qetgraphicsitem/elementtextitemgroup.h" #include "ui_projectprintwindow.h" +// Private Qt PDF engine for drawHyperlink() — not public API, stable since Qt4 +// Requires QT += gui-private in qelectrotech.pro +#include + #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove # include #else @@ -37,6 +44,10 @@ #include #include #include +#include +#include +#include +#include /** * @brief ProjectPrintWindow::ProjectPrintWindow @@ -188,6 +199,241 @@ ProjectPrintWindow::~ProjectPrintWindow() * @brief ProjectPrintWindow::requestPaint * @param slot called when m_preview emit paintRequested */ +/** + * @brief ProjectPrintWindow::pdfConvertUriToGoTo + * Post-processes a Qt-generated PDF to replace URI link annotations + * (file:///path/to/file.pdf#page=N) with native PDF GoTo actions + * ([pageObj 0 R /Fit]). This makes cross-reference links work in all + * PDF viewers regardless of where the file is stored. + * + * The function: + * 1. Reads the PDF as raw bytes. + * 2. Collects page object numbers in document order by scanning for + * objects that contain "/Type /Page" (but not "/Type /Pages"). + * 3. Replaces every annotation action block + * /S /URI\n/URI (file://...#page=N) + * with + * /S /GoTo\n/D [ 0 R /Fit] + * 4. Rebuilds the cross-reference table (offsets change because the + * replacement strings have different lengths). + * 5. Writes the result back to the same file. + * + * The function is intentionally conservative: if any step fails (file + * not found, malformed PDF, no URI annotations) it returns silently + * without corrupting the file. + */ +static void pdfConvertUriToGoTo(const QString &pdfPath) +{ + // --- 1. Read raw bytes --- + QFile f(pdfPath); + if (!f.open(QIODevice::ReadOnly)) return; + QByteArray data = f.readAll(); + f.close(); + + // --- 2. Collect page object numbers in document order --- + // Read them from the page tree (/Type /Pages -> /Kids [ N 0 R ... ]). + // This is reliable; scanning raw bytes for "/Type /Page" is NOT: that + // marker also occurs inside content streams, and a forward lookahead + // wrongly tags neighbouring objects (it found 280 "pages" for a 137-page + // document). Qt writes a single, flat /Kids array listing every page. + QVector pageObjs; + { + int pagesPos = data.indexOf("/Type /Pages"); + int kidsPos = (pagesPos == -1) ? -1 : data.indexOf("/Kids", pagesPos); + int lb = (kidsPos == -1) ? -1 : data.indexOf('[', kidsPos); + int rb = (lb == -1) ? -1 : data.indexOf(']', lb); + if (lb != -1 && rb != -1 && rb > lb) { + const QString kids = + QString::fromLatin1(data.mid(lb + 1, rb - lb - 1)); + QRegularExpression re(QStringLiteral("(\\d+)\\s+\\d+\\s+R")); + auto it = re.globalMatch(kids); + while (it.hasNext()) { + int objNum = it.next().captured(1).toInt(); + if (objNum > 0) pageObjs.append(objNum); + } + } + } + + if (pageObjs.isEmpty()) return; // nothing to do + + // --- 3. Replace URI annotations with GoTo --- + // Pattern (Qt always writes exactly this): + // /S /URI\n/URI (file:///...#page=N)\n + // or (older patches without file://): + // /S /URI\n/URI (page=N)\n + bool changed = false; + { + // We do a manual scan to handle variable-length replacements. + QByteArray out; + out.reserve(data.size()); + + const QByteArray sUri = "/S /URI\n/URI ("; + const QByteArray sGoTo = "/S /GoTo\n/D ["; + int pos = 0; + + while (pos < data.size()) { + int found = data.indexOf(sUri, pos); + if (found == -1) { + out.append(data.mid(pos)); + break; + } + + // Copy everything up to the match + out.append(data.mid(pos, found - pos)); + + // Find closing ')' of the URI value + int uriStart = found + sUri.size(); + int closeParen = data.indexOf(')', uriStart); + if (closeParen == -1) { + // Malformed — copy rest verbatim + out.append(data.mid(found)); + pos = data.size(); + break; + } + + QByteArray uriVal = data.mid(uriStart, closeParen - uriStart); + + // Extract page number: look for #page=N or bare page=N + int pageNum = -1; + int hashPos = uriVal.lastIndexOf("#page="); + int digitStart = -1; + if (hashPos != -1) { + digitStart = hashPos + 6; + } else if (uriVal.startsWith("page=")) { + digitStart = 5; + } + if (digitStart != -1) { + // Take only the leading digits: the fragment may carry extra + // parameters after the page number (e.g. "22&fitr=15_489_..."), + // and QByteArray::toInt() would fail on the whole remainder. + int e = digitStart; + while (e < uriVal.size() + && uriVal[e] >= '0' && uriVal[e] <= '9') + ++e; + if (e > digitStart) + pageNum = uriVal.mid(digitStart, e - digitStart).toInt(); + } + + if (pageNum >= 1 && pageNum <= pageObjs.size()) { + // Valid page reference — emit GoTo action. + int pageObjNum = pageObjs[pageNum - 1]; + + // Optional precise destination: &fitr=Left_Bottom_Right_Top + // (integer PDF points). If present -> /FitR (frame the element); + // otherwise -> /Fit (whole page, top). + QByteArray dest = " /Fit]"; + int fr = uriVal.indexOf("fitr="); + if (fr != -1) { + QByteArray rest = uriVal.mid(fr + 5); + // stop at first char that is not part of the number list + int end = 0; + while (end < rest.size() + && ((rest[end] >= '0' && rest[end] <= '9') + || rest[end] == '_' || rest[end] == '-')) + ++end; + QList parts = rest.left(end).split('_'); + if (parts.size() == 4) { + dest = " /FitR " + parts[0] + " " + parts[1] + " " + + parts[2] + " " + parts[3] + "]"; + } + } + + QByteArray goTo = sGoTo + + QByteArray::number(pageObjNum) + + " 0 R" + dest; + out.append(goTo); + changed = true; + } else { + // Unknown page — keep original URI + out.append(sUri); + out.append(uriVal); + out.append(')'); + } + + pos = closeParen + 1; // skip past ')' + } + + if (!changed) return; // nothing was replaced + data = out; + } + + // --- 4. Rebuild xref table --- + // Find start of existing xref (last occurrence) + int xrefStart = data.lastIndexOf("\nxref\n"); + if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref "); + if (xrefStart == -1) return; // malformed PDF + ++xrefStart; // skip the leading '\n' + + QByteArray body = data.left(xrefStart); + + // Collect all object offsets from the body + QMap offsets; // objNum -> byte offset + { + const QByteArray objMarker = " 0 obj"; + int pos = 0; + while ((pos = body.indexOf(objMarker, pos)) != -1) { + int numStart = pos - 1; + while (numStart > 0 && body[numStart-1] != '\n' && body[numStart-1] != '\r') + --numStart; + QByteArray numStr = body.mid(numStart, pos - numStart).trimmed(); + bool ok = false; + int objNum = numStr.toInt(&ok); + if (ok && objNum > 0) + offsets[objNum] = numStart; + ++pos; + } + } + + if (offsets.isEmpty()) return; + + int maxObj = offsets.lastKey(); + + // Build xref table + QByteArray xref; + xref += "xref\n"; + xref += "0 " + QByteArray::number(maxObj + 1) + "\n"; + xref += "0000000000 65535 f \n"; + for (int i = 1; i <= maxObj; ++i) { + if (offsets.contains(i)) { + xref += QByteArray::number(offsets[i]).rightJustified(10, '0') + + " 00000 n \n"; + } else { + xref += "0000000000 65535 f \n"; + } + } + + // Find trailer dict from the original xref section + int trailerPos = data.indexOf("trailer", xrefStart); + int trailerEnd = -1; + if (trailerPos != -1) { + trailerEnd = data.indexOf("%%EOF", trailerPos); + if (trailerEnd != -1) trailerEnd += 5; + } + + QByteArray trailer; + if (trailerPos != -1 && trailerEnd != -1) + trailer = data.mid(trailerPos, trailerEnd - trailerPos); + else + trailer = "trailer\n<<>>\n%%EOF"; + + int newXrefOffset = body.size(); + + QByteArray result; + result.reserve(body.size() + xref.size() + trailer.size() + 30); + result += body; + result += xref; + result += trailer; + result += "\nstartxref\n"; + result += QByteArray::number(newXrefOffset); + result += "\n%%EOF\n"; + + // --- 5. Write back --- + QFile out(pdfPath); + if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) return; + out.write(result); + out.close(); +} + void ProjectPrintWindow::requestPaint() { #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) @@ -214,12 +460,47 @@ void ProjectPrintWindow::requestPaint() return; } + // Build diagram -> first physical PDF page number map (1-based) + // Must be done before the print loop since page numbers depend on order + QMap diagramPageMap; + { + int pageNum = 1; + for (auto diagram : selectedDiagram()) { + diagramPageMap.insert(diagram, pageNum); + // Each diagram may span multiple pages if not fit_page + if (!ui->m_fit_in_page_cb->isChecked()) { + auto option = exportProperties(); + bool full_page = m_printer->fullPage(); + int h = horizontalPagesCount(diagram, option, full_page); + int v = verticalPagesCount(diagram, option, full_page); + pageNum += h * v; + } else { + pageNum += 1; + } + } + } + bool first = true; QPainter painter(m_printer); + + // A real PDF export uses the QPdfEngine; the on-screen preview uses a + // preview paint engine. We only post-process when actually writing a PDF. + const bool pdfExport = + (m_printer->outputFormat() == QPrinter::PdfFormat) + && (dynamic_cast(painter.paintEngine()) != nullptr); + for (auto diagram : selectedDiagram()) { first ? first = false : m_printer->newPage(); - printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer); + printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap); + } + + if (pdfExport) { + painter.end(); // flush & close the PDF file on disk + // Convert URI link annotations into native internal GoTo/FitR actions: + // cross-references then jump inside the document (no new viewer + // instance) and frame the target element. + pdfConvertUriToGoTo(m_printer->outputFileName()); } } @@ -230,7 +511,7 @@ void ProjectPrintWindow::requestPaint() * @param fit_page * @param printer */ -void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer) +void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap &diagramPageMap) { ////Prepare the print//// @@ -317,6 +598,171 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter } } + ////Inject PDF cross-reference links//// + if (printer->outputFormat() == QPrinter::PdfFormat && fit_page) { + auto *pdfEngine = dynamic_cast(painter->paintEngine()); + if (pdfEngine) { + + // QGraphicsScene::render() fait save()/restore() : worldTransform() + // est revenu a l'identite ici. On reconstruit DONC explicitement la + // transform appliquee par : + // diagram->render(painter, QRectF(), diagram_rect, KeepAspectRatio) + // cible vide => painter->viewport() ; source = diagram_rect ; centre. + const QRectF target = QRectF(painter->viewport()); + const QRectF source = QRectF(diagram_rect); // meme source que render() + + // render() ANCRE en haut-gauche (pas de centrage) : + // translate(target.topLeft) . scale(s,s) . translate(-source.topLeft) + // On reproduit EXACTEMENT ca — surtout PAS de (target-source*s)/2. + const qreal s = qMin(target.width() / source.width(), + target.height() / source.height()); + + QTransform fit; + fit.translate(target.x(), target.y()); + fit.scale(s, s); + fit.translate(-source.x(), -source.y()); // scene -> pixels device + + // IMPORTANT : QPdfEngine::drawHyperlink() applique lui-meme + // pageMatrix() (echelle 72/resolution + inversion de Y + marges). + // On lui passe donc le rectangle en PIXELS DEVICE, sans aucune + // conversion en points ni flip de notre cote. + const QRectF pageBounds(0, 0, target.width(), target.height()); + + // ---- Device-pixels -> PDF points, replicating QPdfEnginePrivate::pageMatrix() + // (same geometry for every page: same printer, page size and margins). ---- + const qreal pt_scale = 72.0 / printer->resolution(); + const qreal fullH_pt = printer->pageLayout().fullRectPoints().height(); + const bool fullPageMode = + (printer->pageLayout().mode() == QPageLayout::FullPageMode); + const QRect paintPx = + printer->pageLayout().paintRectPixels(printer->resolution()); + auto devToPdf = [=](const QPointF &d) -> QPointF { + qreal dx = d.x(), dy = d.y(); + if (!fullPageMode) { dx += paintPx.left(); dy += paintPx.top(); } + return QPointF(pt_scale * dx, fullH_pt - pt_scale * dy); + }; + + // Compute, in PDF points on its OWN page, the rectangle to frame for a + // target element (used as a /FitR destination so the link zooms onto it). + auto destRectPdf = [&](Element *tgt) -> QRectF { + Diagram *dg = tgt ? tgt->diagram() : nullptr; + if (!dg) return QRectF(); + const QRectF srcT = QRectF(diagramRect(dg, exportProperties())); + if (srcT.width() <= 0.0 || srcT.height() <= 0.0) return QRectF(); + const qreal sT = qMin(target.width() / srcT.width(), + target.height() / srcT.height()); + QTransform fitT; + fitT.translate(target.x(), target.y()); + fitT.scale(sT, sT); + fitT.translate(-srcT.x(), -srcT.y()); + + QRectF elemScene = tgt->mapRectToScene(tgt->boundingRect()); + // Frame the element with a little context, and enforce a minimum + // framed size so tiny contacts don't zoom in extremely. + const qreal pad = 25.0; + elemScene.adjust(-pad, -pad, pad, pad); + const qreal minSide = 160.0; + if (elemScene.width() < minSide) + elemScene.adjust(-(minSide - elemScene.width()) / 2.0, 0, + (minSide - elemScene.width()) / 2.0, 0); + if (elemScene.height() < minSide) + elemScene.adjust(0, -(minSide - elemScene.height()) / 2.0, + 0, (minSide - elemScene.height()) / 2.0); + + const QRectF devT = fitT.mapRect(elemScene); + const QPointF a = devToPdf(devT.topLeft()); + const QPointF b = devToPdf(devT.bottomRight()); + return QRectF(QPointF(qMin(a.x(), b.x()), qMin(a.y(), b.y())), + QPointF(qMax(a.x(), b.x()), qMax(a.y(), b.y()))); + }; + + auto injectLink = [&](const QRectF &sceneRect, Element *targetElmt) { + if (!targetElmt || !targetElmt->diagram()) return; + const int targetPage = + diagramPageMap.value(targetElmt->diagram(), -1); + if (targetPage < 1) return; + const QRectF devRect = fit.mapRect(sceneRect); + if (!devRect.isValid() || !pageBounds.intersects(devRect)) return; + + QString frag = QString("page=%1").arg(targetPage); + const QRectF d = destRectPdf(targetElmt); // /FitR L_B_R_T + if (d.isValid()) + frag += QString("&fitr=%1_%2_%3_%4") + .arg(qRound(d.left())).arg(qRound(d.top())) + .arg(qRound(d.right())).arg(qRound(d.bottom())); + + QUrl url = QUrl::fromLocalFile(printer->outputFileName()); + url.setFragment(frag); + pdfEngine->drawHyperlink(devRect, url); + }; + + for (auto *item : diagram->items()) { + + // --- CrossRefItem links --- + if (auto *xref = dynamic_cast(item)) { + for (auto it = xref->hoveredContactsMap().begin(); + it != xref->hoveredContactsMap().end(); ++it) + { + Element *targetElmt = it.key(); + if (!targetElmt || !targetElmt->diagram()) continue; + // it.value() est en coords LOCALES du CrossRefItem -> scene + injectLink(xref->mapRectToScene(it.value()), targetElmt); + } + continue; + } + + // --- Folio report links (DynamicElementTextItem) --- + if (auto *deti = dynamic_cast(item)) { + Element *parent = deti->parentElement(); + if (!parent) continue; + + // (a) Report element : label -> linked report on another folio + if (parent->linkType() & Element::AllReport) { + if (parent->linkedElements().isEmpty()) continue; + + bool showsLabel = + (deti->textFrom() == DynamicElementTextItem::ElementInfo + && deti->infoName() == QLatin1String("label")) || + (deti->textFrom() == DynamicElementTextItem::CompositeText + && deti->compositeText().contains(QStringLiteral("%{label}"))); + if (!showsLabel) continue; + + Element *targetElmt = parent->linkedElements().first(); + if (!targetElmt || !targetElmt->diagram()) continue; + + injectLink(deti->mapRectToScene(deti->boundingRect()), targetElmt); + continue; + } + + // (b) Slave element : the "(folio-pos)" text -> master element + if (parent->linkType() == Element::Slave) { + QGraphicsTextItem *sx = deti->slaveXrefItem(); + Element *master = deti->masterElement(); + if (sx && master && master->diagram()) { + injectLink(sx->mapRectToScene(sx->boundingRect()), master); + } + continue; + } + continue; + } + + // --- Slave cross-reference carried by a grouped text --- + if (auto *grp = dynamic_cast(item)) { + Element *parent = grp->parentElement(); + if (!parent || parent->linkType() != Element::Slave) continue; + if (parent->linkedElements().isEmpty()) continue; + QGraphicsTextItem *sx = grp->slaveXrefItem(); + if (!sx) continue; + Element *master = parent->linkedElements().first(); + if (!master || !master->diagram()) continue; + injectLink(sx->mapRectToScene(sx->boundingRect()), master); + continue; + } + } + } + } + ////PDF links end//// + ////Print is finished, restore diagram and graphics item properties for (auto view : diagram->views()) { view->setInteractive(true); diff --git a/sources/print/projectprintwindow.h b/sources/print/projectprintwindow.h index f48c01853..8585c2091 100644 --- a/sources/print/projectprintwindow.h +++ b/sources/print/projectprintwindow.h @@ -21,6 +21,7 @@ #include "../exportproperties.h" #include +#include #include namespace Ui { @@ -79,7 +80,7 @@ class ProjectPrintWindow : public QMainWindow private: void requestPaint(); - void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer); + void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap &diagramPageMap = {}); QRect diagramRect(Diagram *diagram, const ExportProperties &option) const; int horizontalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const; int verticalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const; diff --git a/sources/qetgraphicsitem/crossrefitem.cpp b/sources/qetgraphicsitem/crossrefitem.cpp index d1bd2accd..343e4555b 100644 --- a/sources/qetgraphicsitem/crossrefitem.cpp +++ b/sources/qetgraphicsitem/crossrefitem.cpp @@ -928,7 +928,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in bounding_rect = bounding_rect.united(text_rect); if (m_update_map) - m_hovered_contacts_map.insert(elmt, bounding_rect); + m_hovered_contacts_map.insert(elmt, text_rect); ++m_drawed_contacts; } @@ -1012,7 +1012,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in bounding_rect = bounding_rect.united(text_rect); if (m_update_map) - m_hovered_contacts_map.insert(elmt, bounding_rect); + m_hovered_contacts_map.insert(elmt, text_rect); //a switch contact take place of two normal contact m_drawed_contacts += 2; @@ -1044,7 +1044,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in bounding_rect = bounding_rect.united(text_rect); if (m_update_map) - m_hovered_contacts_map.insert(elmt, bounding_rect); + m_hovered_contacts_map.insert(elmt, text_rect); ++m_drawed_contacts; } return bounding_rect; diff --git a/sources/qetgraphicsitem/crossrefitem.h b/sources/qetgraphicsitem/crossrefitem.h index 927e46583..57e1fb22b 100644 --- a/sources/qetgraphicsitem/crossrefitem.h +++ b/sources/qetgraphicsitem/crossrefitem.h @@ -126,6 +126,12 @@ class CrossRefItem : public QGraphicsObject ElementTextItemGroup *m_group = nullptr; QList m_slave_connection; QList m_update_connection; + + public: + /// Returns the map of linked elements and their clickable rects (local coords). + /// Used by the PDF export to inject hyperlink annotations. + const QMultiMap &hoveredContactsMap() const + { return m_hovered_contacts_map; } }; #endif // CROSSREFITEM_H diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.h b/sources/qetgraphicsitem/dynamicelementtextitem.h index 73e49ba85..f31096b53 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.h +++ b/sources/qetgraphicsitem/dynamicelementtextitem.h @@ -86,6 +86,9 @@ class DynamicElementTextItem : public DiagramTextItem void fromXml(const QDomElement &dom_elmt) override; Element *parentElement() const; + /// PDF export: slave cross-reference text item ("(folio-pos)") and its master target. + QGraphicsTextItem *slaveXrefItem() const { return m_slave_Xref_item; } + Element *masterElement() const { return m_master_element.data(); } ElementTextItemGroup *parentGroup() const; Element *elementUseForInfo() const; void refreshLabelConnection(); diff --git a/sources/qetgraphicsitem/elementtextitemgroup.h b/sources/qetgraphicsitem/elementtextitemgroup.h index e9e51cc66..842ba50dd 100644 --- a/sources/qetgraphicsitem/elementtextitemgroup.h +++ b/sources/qetgraphicsitem/elementtextitemgroup.h @@ -76,6 +76,8 @@ class ElementTextItemGroup : public QObject, public QGraphicsItemGroup QList texts() const; Diagram *diagram() const; Element *parentElement() const; + /// PDF export: slave cross-reference text item of the group, if any. + QGraphicsTextItem *slaveXrefItem() const { return m_slave_Xref_item; } QDomElement toXml(QDomDocument &dom_document) const; void fromXml(QDomElement &dom_element);