diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.cpp b/sources/qetgraphicsitem/dynamicelementtextitem.cpp index 51a14ed0b..18410e455 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.cpp +++ b/sources/qetgraphicsitem/dynamicelementtextitem.cpp @@ -758,14 +758,11 @@ void DynamicElementTextItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) if(isSelected()) return; - if ((parentElement()->linkType() & Element::AllReport) && m_other_report) + if (linkedElementShown()) { - if( (m_text_from == ElementInfo && m_info_name == "label") || - (m_text_from == CompositeText && m_composite_text.contains("%{label}")) ) - { - m_user_color = color(); - setDefaultTextColor(Qt::blue); - } + m_user_color = color(); + setDefaultTextColor(Qt::blue); + setCursor(Qt::PointingHandCursor); } } @@ -781,6 +778,7 @@ void DynamicElementTextItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) { setDefaultTextColor(m_user_color); m_user_color = QColor(); //m_user_color is now invalid + unsetCursor(); } } @@ -1601,6 +1599,27 @@ QString DynamicElementTextItem::reportReplacedCompositeText() const return string; } +/** + @brief DynamicElementTextItem::linkedElementShown + @return the element this text leads to: the master of a linked slave + or the other end of a linked folio report, when the text shows its + label; nullptr otherwise. Such a text turns blue under the mouse, shows + a pointing hand and jumps there on a double click. +*/ +Element *DynamicElementTextItem::linkedElementShown() const +{ + if(!parentElement()) + return nullptr; + if (!((m_text_from == ElementInfo && m_info_name == "label") || + (m_text_from == CompositeText && m_composite_text.contains("%{label}")))) + return nullptr; + if(parentElement()->linkType() == Element::Slave && m_master_element) + return m_master_element.data(); + if((parentElement()->linkType() & Element::AllReport) && m_other_report) + return m_other_report.data(); + return nullptr; +} + /** @brief DynamicElementTextItem::zoomToLinkedElement If the parent element is a folio report or a slave element, @@ -1608,24 +1627,7 @@ QString DynamicElementTextItem::reportReplacedCompositeText() const */ void DynamicElementTextItem::zoomToLinkedElement() { - if(!parentElement()) - return; - - Element *zoomed_element = nullptr; - - if(parentElement()->linkType() == Element::Slave && m_master_element) - { - if ((m_text_from == ElementInfo && m_info_name == "label") || - (m_text_from == CompositeText && m_composite_text.contains("%{label}"))) - zoomed_element = m_master_element.data(); - } - if((parentElement()->linkType() & Element::AllReport) && m_other_report) - { - if((m_text_from == ElementInfo && m_info_name == "label") || - (m_text_from == CompositeText && m_composite_text.contains("%{label}"))) - zoomed_element = m_other_report.data(); - } - + Element *zoomed_element = linkedElementShown(); if(zoomed_element) { //Unselect and ungrab mouse to prevent unwanted diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.h b/sources/qetgraphicsitem/dynamicelementtextitem.h index 524dd91ad..2a6044890 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.h +++ b/sources/qetgraphicsitem/dynamicelementtextitem.h @@ -105,6 +105,7 @@ class DynamicElementTextItem : public DiagramTextItem static QString xmlTagName() {return QString("dynamic_elmt_text");} void setInfoName(const QString &info_name); QString infoName() const; + Element *linkedElementShown() const; void setCompositeText(const QString &text); QString compositeText() const; void setFrame(const bool frame); diff --git a/sources/qetgraphicsitem/element.cpp b/sources/qetgraphicsitem/element.cpp index c603a1e33..98ab82b48 100644 --- a/sources/qetgraphicsitem/element.cpp +++ b/sources/qetgraphicsitem/element.cpp @@ -49,6 +49,7 @@ #include #include #include +#include static const QString plcTerminalKeys[] = { QETInformation::ELMT_PLC_T1, @@ -253,6 +254,36 @@ void Element::setHighlighted(bool hl) update(); } +/** + @brief Element::showAsJumpTarget + Highlight this element for a moment, so the user sees where a jump + from a folio report (or a cross reference) landed. Independent of + setHighlighted(), which follows the mouse over a linked element and + would be cleared as soon as the mouse leaves the element jumped from. +*/ +void Element::showAsJumpTarget() +{ + ++m_jump_highlight; + update(); + QTimer::singleShot(2000, this, [this]() { + if (m_jump_highlight > 0) + --m_jump_highlight; + update(); + }); +} + +/** + @brief Element::jumpTarget + @return the element a double click on this one jumps to: the other end + of a linked folio report, or nullptr. +*/ +Element *Element::jumpTarget() const +{ + if ((linkType() & AllReport) && !connected_elements.isEmpty()) + return connected_elements.first(); + return nullptr; +} + /** @brief Element::displayHelpLine Display the help line of each terminal if b is true @@ -474,7 +505,7 @@ void Element::paint( const QStyleOptionGraphicsItem *options, QWidget *) { - if (m_must_highlight) { + if (m_must_highlight || m_jump_highlight > 0) { drawHighlight(painter, options); } @@ -2354,6 +2385,29 @@ void Element::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) } } +/** + @brief Element::mouseDoubleClickEvent + A double click on a linked folio report goes to its other end, as a + double click on its text does; on any other element it opens the + properties. A report's properties stay reachable from the context menu + and the properties panel. + @param event +*/ +void Element::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) +{ + if (Element *target = jumpTarget()) + { + event->accept(); + //Unselect and ungrab mouse to prevent unwanted + //move when the target is in the same scene as this. + setSelected(false); + ungrabMouse(); + QetGraphicsItem::showItem(target); + return; + } + QetGraphicsItem::mouseDoubleClickEvent(event); +} + /** When mouse over element change m_mouse_over to true (used in paint() function ) @@ -2367,6 +2421,13 @@ void Element::hoverEnterEvent(QGraphicsSceneHoverEvent *e) foreach (Element *elmt, linkedElements()) elmt -> setHighlighted(true); + //A linked folio report jumps to its other end on a double + //click: the pointing hand says so + if (jumpTarget()) { + setCursor(Qt::PointingHandCursor); + m_jump_cursor = true; + } + m_mouse_over = true; setToolTip( name() ); update(); @@ -2385,6 +2446,11 @@ void Element::hoverLeaveEvent(QGraphicsSceneHoverEvent *e) foreach (Element *elmt, linkedElements()) elmt -> setHighlighted(false); + if (m_jump_cursor) { + unsetCursor(); + m_jump_cursor = false; + } + m_mouse_over = false; update(); } diff --git a/sources/qetgraphicsitem/element.h b/sources/qetgraphicsitem/element.h index b5f6764f5..8a7afb311 100644 --- a/sources/qetgraphicsitem/element.h +++ b/sources/qetgraphicsitem/element.h @@ -176,6 +176,9 @@ class Element : public QetGraphicsItem }; ReloadPictureResult reloadPicture(); virtual void setHighlighted(bool); + void showAsJumpTarget(); + bool isShownAsJumpTarget() const { return m_jump_highlight > 0; } + Element *jumpTarget() const; void displayHelpLine(bool b = true); QSize size() const; QPixmap pixmap(); @@ -316,6 +319,7 @@ class Element : public QetGraphicsItem void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent( QGraphicsSceneMouseEvent *event) override; + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *) override; QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; @@ -359,6 +363,8 @@ class Element : public QetGraphicsItem void storeInformation(); projectDataBase *placedStore() const; bool m_must_highlight = false; + int m_jump_highlight = 0; ///< pending jump-target highlights, see showAsJumpTarget() + bool m_jump_cursor = false; QSize dimensions; QPoint hotspot_coord; bool m_mouse_over = false; diff --git a/sources/qetgraphicsitem/qetgraphicsitem.cpp b/sources/qetgraphicsitem/qetgraphicsitem.cpp index 631ee8d85..45a17fdd6 100644 --- a/sources/qetgraphicsitem/qetgraphicsitem.cpp +++ b/sources/qetgraphicsitem/qetgraphicsitem.cpp @@ -18,6 +18,7 @@ #include "qetgraphicsitem.h" #include "../diagram.h" +#include "element.h" /** @brief QetGraphicsItem::QetGraphicsItem @@ -38,6 +39,10 @@ void QetGraphicsItem::showItem(QetGraphicsItem *item) fit.adjust(-200, -200, 200, 200); view->fitInView(fit, Qt::KeepAspectRatioByExpanding); } + + //Mark where the jump landed + if (Element *element = dynamic_cast(item)) + element->showAsJumpTarget(); } } diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 644cf4725..83249d7a1 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -112,6 +112,16 @@ if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja") --fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet) set_tests_properties(tst_crosshairintegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE ENVIRONMENT "QT_QPA_PLATFORM=offscreen") + # A linked folio report shows a pointing hand and jumps to its other + # end on a double click, on the arrow and on its label text; the end + # jumped to is highlighted for a moment. + add_test(NAME tst_reportjumpintegration + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_report_jump_probe.py + --build ${CMAKE_BINARY_DIR} --binary $ + --source ${CMAKE_CURRENT_SOURCE_DIR}/report_jump_probe.cpp + --fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/linked_reports.qet) + set_tests_properties(tst_reportjumpintegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE + ENVIRONMENT "QT_QPA_PLATFORM=offscreen") endif() if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja") add_test(NAME tst_darkimageintegration diff --git a/tests/qttest/fixtures/linked_reports.qet b/tests/qttest/fixtures/linked_reports.qet new file mode 100644 index 000000000..57ab5bd11 --- /dev/null +++ b/tests/qttest/fixtures/linked_reports.qet @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2-C4 + label + + + + + + + + + + + + + label + + + + + + + + + + + + + + + + + + + + 1-C4 + label + + + + + + + + + + Elements importats + Zavedené prvky + Importerede elementer + Importierte Elemente + Εισηγμένα στοιχεία + Imported elements + Elementos importados + Éléments importés + Uvezeni elementi + Elementi importati + インバートされた要素 + 전기 심볼 요소 + 전기 심볼 요소 + Elementen geïmporteerd + Elementen geïmporteerd + Elementy importowane + Elementos importados + Elementos importados + Elemente importate + Импортированные элементы + Uvoženi elementi + Importerade element + İthal öğeler + Імпортовані елементи + 导入元件 + + + + كهربائي + Elèctric + Elektrotechnika + Elektrisk + Elektrik + Ηλεκτρικά + Electric + Eléctrica + Sähkö + Electrique + Električno + Elektromos + Elettrica + 電気 + 전기 + Цахилгаан + Elektrisk + Elektrotechniek + Elektrisch + Elektrotechnika + Elétrico + Elétrica + Electric + Электротехника + Elektrické + Elektrika + Електрика + El + Elektrik + Електрика + 10_电气 + 电气工程 + + + + متعدد الأقطاب + Multifilar + Vícežilový + Flere ledere + Allpolig + Πολυγραμμικό + All-pole + Multifilar + Yleisnapa + Multifilaire + Univerzalni + Többvonalas szimbólumok + Multifilare + 複線 + 다-극 + Бүх туйл + Alle poler + Veel polig + All-pole + Schematy wieloliniowe + Multipolar + Multifilar + Bipolar + Многополюсные + Všepólové + Vsi poli + Свe-полни + Allpolig + Çok kutuplu + Багатополюсні + 10_全极 + + + + إحالات + Referències als fulls + Odkazování listů + Ark henvisninger + Folienverweise + Αναφορές σελίδας + Folio referencing + Referencias + Lehtiviittaus + Reports de folio + Referenciranje folija + Tervlap hivatkozások + Riferimenti + フォリオ参照 + 도면 간 참조 + Хуудасны холбоос + Folio-referanse + Referenties + Folioverwijzing + Odsyłacze + Renvoi de folio + Referências + Renvoi de folio + Точки разрыва + Odkazy na listy + Sklici na list + Референцирање фолија + Bladhänvisning + Sayfa referansı + Посилання на аркуші + 图页引用 + + + + + + الصفحة التالية + Enllaç sortint + Další list + Går pil + Nächste Folie + Επόμενη σελίδα + Going arrow + Folio siguiente + Lähtevä nuoli + Folio suivant + Odlazna strelica + Elmenő hivatkozás + Foglio successivo + 出矢印 + 나가는 화살표 + Явж буй сум + Går pil + Referentie volgend + Gaande pijl + Wyjście adresowe + Seta de saída + Seta de saída (vai) + Săgeată de ieșire + Следующая страница + Odchádzajúca šípka + Odhodna puščica + Одлазна стрелица + Utgående pil + Giden ok + Стрілка виходу + 02引出箭头 + + Author: The QElectroTech team +License: see http://qelectrotech.org/wiki/doc/elements_license + + + + + + label + + + + + + + + + + الصفحة السابقة + Enllaç entrant + Předchozí list + Kommer pil + Vorherige Folie + Προηγούμενη σελίδα + Coming arrow + folio anterior + Saapuva nuoli + Folio précédent + Dolazna strelica + Beérkező hivatkozás + Foglio precedente + 入り矢印 + 들어오는 화살표 + Ирж буй сум + Kommer pil + Referentie Vorig + Komende pijl + Wejście adresowe + Seta de entrada + Seta de entrada (vem) + Săgeată de intrare + Предыдущая страница + Prichádzajúca šípka + Prihajajoča puščica + Долазна стрелица + Inkommande pil + Gelen ok + Стрілка входу + 01引入箭头 + + Author: The QElectroTech team +License: see http://qelectrotech.org/wiki/doc/elements_license + + + + + + label + + + + + + + + + + + diff --git a/tests/qttest/report_jump_probe.cpp b/tests/qttest/report_jump_probe.cpp new file mode 100644 index 000000000..f6aebe073 --- /dev/null +++ b/tests/qttest/report_jump_probe.cpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Jumping between the two ends of a folio report, linked against the +// application's objects. +// +// A linked folio report shows a pointing hand under the mouse, on its +// arrow and on the text that shows its label, and a double click on +// either goes to the other end. The end jumped to is highlighted for a +// moment, independently of the hover highlight, which the mouse leaving +// the end jumped from would clear at once. An unlinked report shows no +// hand and has nowhere to jump to. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../sources/diagram.h" +#include "../../sources/qetproject.h" +#include "../../sources/qetmessagebox.h" +#include "../../sources/qetgraphicsitem/element.h" +#include "../../sources/qetgraphicsitem/dynamicelementtextitem.h" + +static void check(bool value, const char *message) +{ + if (!value) throw std::runtime_error(message); +} + +static void hover(Diagram *scene, QGraphicsItem *item, QEvent::Type type) +{ + QGraphicsSceneHoverEvent event(type); + event.setScenePos(item->sceneBoundingRect().center()); + event.setPos(item->boundingRect().center()); + scene->sendEvent(item, &event); +} + +static void doubleClick(Diagram *scene, QGraphicsItem *item) +{ + QGraphicsSceneMouseEvent event(QEvent::GraphicsSceneMouseDoubleClick); + event.setButton(Qt::LeftButton); + event.setButtons(Qt::LeftButton); + event.setScenePos(item->sceneBoundingRect().center()); + event.setPos(item->boundingRect().center()); + scene->sendEvent(item, &event); +} + +static void wait(int ms) +{ + QEventLoop loop; + QTimer::singleShot(ms, &loop, &QEventLoop::quit); + loop.exec(); +} + +static bool showsHand(const QGraphicsItem *item) +{ + return item->hasCursor() && item->cursor().shape() == Qt::PointingHandCursor; +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + std::freopen(argv[2], "w", stdout); + + QTemporaryDir settings; + QSettings::setDefaultFormat(QSettings::IniFormat); + QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, settings.path()); + QCoreApplication::setOrganizationName("QETReportJumpRegression"); + QETProject::setBackupEnabled(false); + QET::QetMessageBox::setNonInteractive(true); + QFontDatabase::addApplicationFont(":/fonts/LiberationSans-Regular.ttf"); + + try + { + QETProject project(QString::fromLocal8Bit(argv[1])); + check(project.state() == QETProject::Ok, "fixture opens"); + check(project.diagrams().size() == 2, "fixture has two folios"); + project.buildFolios(project.diagrams()); + Diagram *one = project.diagrams().at(0); + Diagram *two = project.diagrams().at(1); + + // Folio one: a going report linked to folio two, and one alone. + Element *going = nullptr, *alone = nullptr; + for (Element *element : one->elements()) { + if (!(element->linkType() & Element::AllReport)) continue; + (element->linkedElements().isEmpty() ? alone : going) = element; + } + check(going && alone, "a linked and an unlinked report on folio one"); + Element *coming = going->linkedElements().first(); + check(coming->diagram() == two, "the linked end is on folio two"); + + check(going->jumpTarget() == coming, "a linked report jumps to its other end"); + check(alone->jumpTarget() == nullptr, "an unlinked report has nowhere to jump to"); + + // The arrow: a pointing hand while the mouse is over it, only + // when linked. + hover(one, going, QEvent::GraphicsSceneHoverEnter); + check(showsHand(going), "pointing hand over a linked report"); + hover(one, going, QEvent::GraphicsSceneHoverLeave); + check(!going->hasCursor(), "hand gone when the mouse leaves"); + hover(one, alone, QEvent::GraphicsSceneHoverEnter); + check(!showsHand(alone), "no hand over an unlinked report"); + hover(one, alone, QEvent::GraphicsSceneHoverLeave); + + // Double click on the arrow: the other end is selected and + // highlighted, and stays highlighted when the mouse leaves the + // end jumped from. + hover(one, going, QEvent::GraphicsSceneHoverEnter); + doubleClick(one, going); + check(coming->isSelected(), "the end jumped to is selected"); + check(coming->isShownAsJumpTarget(), "the end jumped to is highlighted"); + hover(one, going, QEvent::GraphicsSceneHoverLeave); + check(coming->isShownAsJumpTarget(), "leaving the report keeps the jump highlight"); + wait(2300); + check(!coming->isShownAsJumpTarget(), "the jump highlight goes after a moment"); + + // The text showing the label: a pointing hand, and a double click + // jumps back. + DynamicElementTextItem *label = nullptr; + for (DynamicElementTextItem *text : coming->dynamicTextItems()) + if (text->linkedElementShown()) label = text; + check(label, "the coming report has a text that leads to its other end"); + check(label->linkedElementShown() == going, "the text leads to the going report"); + hover(two, label, QEvent::GraphicsSceneHoverEnter); + check(showsHand(label), "pointing hand over the label text"); + hover(two, label, QEvent::GraphicsSceneHoverLeave); + check(!label->hasCursor(), "hand gone when the mouse leaves the text"); + doubleClick(two, label); + check(going->isSelected(), "a double click on the text jumps back"); + check(going->isShownAsJumpTarget(), "the end jumped back to is highlighted"); + + std::puts("PASS: linked folio reports show a hand and jump to their highlighted other end"); + } + catch (const std::exception &error) + { + std::printf("FAIL: %s\n", error.what()); + return 1; + } + return 0; +} diff --git a/tests/qttest/run_report_jump_probe.py b/tests/qttest/run_report_jump_probe.py new file mode 100755 index 000000000..1da8b0528 --- /dev/null +++ b/tests/qttest/run_report_jump_probe.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Link the report jump integration probe using an existing Ninja app build. + +Reuses app objects without duplicating the app or introducing a core-library +refactor. No application source or existing executable is replaced. The +generated manifest, probe binary, reports and export fixtures stay in build/. +""" +import argparse +import pathlib +import re +import shutil +import subprocess +import sys + +parser = argparse.ArgumentParser() +parser.add_argument('--build', required=True) +parser.add_argument('--binary', required=True) +parser.add_argument('--source', required=True) +parser.add_argument('--fixture', required=True) +args = parser.parse_args() +build = pathlib.Path(args.build).resolve() +binary = pathlib.Path(args.binary).resolve() +source = pathlib.Path(args.source).resolve() +fixture = pathlib.Path(args.fixture).resolve() +manifest = (build / 'build.ninja').read_text(encoding='utf-8') +match = re.search(r'^build (\S*?/sources/main\.cpp\.(?:obj|o)):', manifest, re.MULTILINE) +if not match: + sys.exit('Cannot find the application main object in the Ninja build.') +main_object = match.group(1) +probe_object = main_object.replace('main.cpp.', 'report_jump_probe.cpp.') +probe_binary = binary.with_name('report_jump_probe' + binary.suffix) +def escape(path): + return pathlib.Path(path).as_posix().replace('$', '$$').replace(':', '$:').replace(' ', '$ ') +main_source = source.parents[2] / 'sources' / 'main.cpp' +manifest = manifest.replace(main_object, probe_object) +manifest = manifest.replace(escape(main_source), escape(source)) +if binary.suffix: + manifest = manifest.replace(binary.name, probe_binary.name) +else: + old_target = escape(binary.name if binary.parent == build else binary) + new_target = escape(probe_binary.name if probe_binary.parent == build else probe_binary) + manifest = manifest.replace('build ' + old_target + ':', 'build ' + new_target + ':') + old_file = binary.name if binary.parent == build else binary.as_posix() + new_file = probe_binary.name if probe_binary.parent == build else probe_binary.as_posix() + manifest = re.sub(r'^ TARGET_FILE = ' + re.escape(old_file) + r'$', + ' TARGET_FILE = ' + new_file, manifest, flags=re.MULTILINE) +probe_manifest = build / 'report-jump-probe.ninja' +probe_manifest.write_text(manifest, encoding='utf-8') +ninja = shutil.which('ninja') +if not ninja: + sys.exit('Ninja is required for the integration probe.') +target = probe_binary.name if probe_binary.parent == build else str(probe_binary) +subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], check=True) +output = build / 'report-jump' +output.mkdir(exist_ok=True) +report = output / 'results.txt' +result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=90) +if report.exists(): + print(report.read_text(encoding='utf-8', errors='replace')) +sys.exit(result.returncode)