mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-05 18:54:14 +02:00
b8eb5c90a1
The stacking added in #1287 tested the crosses' bounding rects, so crosses whose frames or empty margins touched were raised although no text was hidden (the list display case found in review). The texts each cross really draws are now recorded while its geometry is built, and a cross is raised only when one of its texts would be drawn over a text of another one. A new "Empiler les références dont les textes se chevauchent" checkbox in the cross reference properties, per type, turns the stacking off; the crosses of that type are then only centred, as before #1287. It is saved in the project (xref attribute "stackoverlapping") and in the settings, and is on when absent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1920 lines
58 KiB
C++
1920 lines
58 KiB
C++
/*
|
|
Copyright 2006-2026 The QElectroTech Team
|
|
This file is part of QElectroTech.
|
|
|
|
QElectroTech 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 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
QElectroTech 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 QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "crossrefitem.h"
|
|
#include "../shownkinds.h"
|
|
|
|
#include <QTimer>
|
|
#include "../qetproject.h"
|
|
#include "../autoNum/assignvariables.h"
|
|
#include "../diagram.h"
|
|
#include "../diagramposition.h"
|
|
#include "../qetapp.h"
|
|
#include "dynamicelementtextitem.h"
|
|
#include "element.h"
|
|
#include "elementtextitemgroup.h"
|
|
#include "qgraphicsitemutility.h"
|
|
#include "terminal.h"
|
|
#include "../properties/elementdata.h"
|
|
|
|
#include <algorithm>
|
|
|
|
//define the height of the header.
|
|
static int header = 5;
|
|
//define the minimal height of the cross (without header)
|
|
static int cross_min_height = 33;
|
|
//margins added around the contacts by drawAsContacts() in the bounding
|
|
//rect of a cross displayed as a list of contacts.
|
|
static const qreal list_margin_left = 30;
|
|
static const qreal list_margin_right = 4;
|
|
|
|
/**
|
|
@brief CrossRefItem::CrossRefItem
|
|
@param elmt : element to display the cross ref
|
|
*/
|
|
CrossRefItem::CrossRefItem(Element *elmt) :
|
|
QGraphicsObject(elmt),
|
|
m_element(elmt)
|
|
{init();}
|
|
|
|
/**
|
|
@brief CrossRefItem::CrossRefItem
|
|
@param elmt : element to display the cross ref
|
|
@param text : If the Xref must be displayed under a text, the text.
|
|
*/
|
|
CrossRefItem::CrossRefItem(Element *elmt, DynamicElementTextItem *text) :
|
|
QGraphicsObject(text),
|
|
m_element (elmt),
|
|
m_text(text)
|
|
{init();}
|
|
|
|
/**
|
|
@brief CrossRefItem::CrossRefItem
|
|
@param elmt : element to display the cross ref
|
|
@param group : If the Xref must be displayed under a group, the group.
|
|
*/
|
|
CrossRefItem::CrossRefItem(Element *elmt, ElementTextItemGroup *group) :
|
|
QGraphicsObject(group),
|
|
m_element(elmt),
|
|
m_group(group)
|
|
{init();}
|
|
|
|
/**
|
|
@brief CrossRefItem::~CrossRefItem
|
|
Default destructor
|
|
*/
|
|
CrossRefItem::~CrossRefItem()
|
|
{}
|
|
|
|
/**
|
|
@brief CrossRefItem::init
|
|
init this Xref
|
|
*/
|
|
void CrossRefItem::init()
|
|
{
|
|
ShownKinds::tag(this, ShownKinds::CrossReferences);
|
|
if(!m_element->diagram())
|
|
{
|
|
qDebug() << "CrossRefItem constructor" << "element is not in a diagram";
|
|
return;
|
|
}
|
|
|
|
QETProject *project = m_element->diagram()->project();
|
|
connect(project, &QETProject::XRefPropertiesChanged, this, &CrossRefItem::updateProperties);
|
|
|
|
m_properties = m_element->diagram()->project()->defaultXRefProperties(m_element->kindInformations()["type"].toString());
|
|
setAcceptHoverEvents(true);
|
|
|
|
setUpConnection();
|
|
linkedChanged();
|
|
updateLabel();
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::setUpConnection
|
|
Set up several connection to keep up to date the Xref
|
|
*/
|
|
void CrossRefItem::setUpConnection()
|
|
{
|
|
for(const QMetaObject::Connection& c : m_update_connection)
|
|
disconnect(c);
|
|
|
|
m_update_connection.clear();
|
|
QETProject *project = m_element->diagram()->project();
|
|
bool set=false;
|
|
|
|
if(m_properties.snapTo() == XRefProperties::Label && (m_text || m_group)) //Snap to label and parent is a text or a group
|
|
set=true;
|
|
else if(m_properties.snapTo() == XRefProperties::Bottom && !m_text && !m_group) //Snap to bottom of element and parent is the element itself
|
|
{
|
|
m_update_connection << connect(m_element, &Element::xChanged, this, &CrossRefItem::autoPos);
|
|
m_update_connection << connect(m_element, &Element::yChanged, this, &CrossRefItem::autoPos);
|
|
m_update_connection << connect(m_element, &Element::rotationChanged, this, &CrossRefItem::autoPos);
|
|
set=true;
|
|
}
|
|
// For PLC masters, always set up connections for update notifications
|
|
// (page reorder, diagram removal, etc.)
|
|
if (!set && m_element->elementData().m_master_type == ElementData::PLC && !m_text && !m_group)
|
|
set = true;
|
|
|
|
if(set)
|
|
{
|
|
m_update_connection
|
|
<< connect(project,
|
|
&QETProject::projectDiagramsOrderChanged,
|
|
this, &CrossRefItem::updateLabel);
|
|
m_update_connection << connect(project,
|
|
&QETProject::diagramRemoved,
|
|
this, &CrossRefItem::updateLabel);
|
|
m_update_connection << connect(m_element,
|
|
&Element::linkedElementChanged,
|
|
this, &CrossRefItem::linkedChanged);
|
|
|
|
auto diagram_ = dynamic_cast<Diagram *>(this->scene());
|
|
auto formula_ = m_properties.masterLabel();
|
|
if (diagram_ &&
|
|
formula_.contains("%F"))
|
|
{
|
|
m_update_connection << connect(diagram_ , &Diagram::diagramInformationChanged,
|
|
this, &CrossRefItem::updateLabel);
|
|
}
|
|
linkedChanged();
|
|
updateLabel();
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::boundingRect
|
|
@return the bounding rect of this item
|
|
*/
|
|
QRectF CrossRefItem::boundingRect() const
|
|
{
|
|
return m_bounding_rect;
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::shape
|
|
@return the shape of this item
|
|
*/
|
|
QPainterPath CrossRefItem::shape() const{
|
|
return m_shape_path;
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::elementPositionText
|
|
@param elmt
|
|
@param add_prefix
|
|
@return the string corresponding to the position of elmt in the diagram.
|
|
if add_prefix is true,
|
|
prefix (for power and delay contact) is added to the position text.
|
|
*/
|
|
QString CrossRefItem::elementPositionText(
|
|
const Element *elmt, const bool &add_prefix) const
|
|
{
|
|
autonum::sequentialNumbers seq;
|
|
QString txt = autonum::AssignVariables::formulaToLabel(
|
|
m_properties.masterLabel(),
|
|
seq, elmt->diagram(),
|
|
elmt);
|
|
|
|
if (add_prefix)
|
|
{
|
|
if (elmt->kindInformations()["type"].toString() == "power")
|
|
txt.prepend(m_properties.prefix("power"));
|
|
else if (elmt->kindInformations()["type"].toString().contains("delay"))
|
|
txt.prepend(m_properties.prefix("delay"));
|
|
else if (elmt->kindInformations()["state"].toString() == "SW")
|
|
txt.prepend(m_properties.prefix("switch"));
|
|
}
|
|
return txt;
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::showAllConfiguredSlaves
|
|
@param elmt : the element displaying the cross reference
|
|
@param xrp : xref properties of that element
|
|
@return true when the contact comb must show every slave contact the
|
|
master defines, even those no slave is linked to yet. That is the case
|
|
when the user asked for it, when the comb (contacts) display is the
|
|
one in use, and when the master really declares contact groups --
|
|
an element which declares none behaves exactly as before.
|
|
*/
|
|
bool CrossRefItem::showAllConfiguredSlaves(
|
|
const Element *elmt,
|
|
const XRefProperties &xrp)
|
|
{
|
|
if (!elmt) return false;
|
|
if (!xrp.showAllConfiguredSlaves()) return false;
|
|
if (xrp.displayHas() != XRefProperties::Contacts) return false;
|
|
|
|
return !elmt->elementData().m_slave_contact_groups.isEmpty();
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::mustDrawAllConfiguredSlaves
|
|
@return showAllConfiguredSlaves for the element of this item and the
|
|
current properties.
|
|
*/
|
|
bool CrossRefItem::mustDrawAllConfiguredSlaves() const
|
|
{
|
|
return showAllConfiguredSlaves(m_element, m_properties);
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::updateProperties
|
|
update the current properties
|
|
*/
|
|
void CrossRefItem::updateProperties()
|
|
{
|
|
XRefProperties xrp = m_element->diagram()->project()->defaultXRefProperties(m_element->kindInformations()["type"].toString());
|
|
|
|
if (m_properties != xrp)
|
|
{
|
|
m_properties = xrp;
|
|
//Through ShownKinds: stays hidden while View > Show hides
|
|
//cross-references
|
|
ShownKinds::setVisible(this, false);
|
|
if(m_properties.snapTo() == XRefProperties::Label && (m_text || m_group)) //Snap to label and parent is text or group
|
|
ShownKinds::setVisible(this, true);
|
|
else if((m_properties.snapTo() == XRefProperties::Bottom && !m_text && !m_group)) //Snap to bottom of element is the parent
|
|
ShownKinds::setVisible(this, true);
|
|
|
|
setUpConnection();
|
|
updateLabel();
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::updateLabel
|
|
Update the content of the item
|
|
*/
|
|
void CrossRefItem::updateLabel()
|
|
{
|
|
//init the shape and bounding rect
|
|
m_shape_path = QPainterPath();
|
|
prepareGeometryChange();
|
|
m_bounding_rect = QRectF();
|
|
m_text_rects.clear();
|
|
|
|
// Build geometry and m_hovered_contacts_map using a QImage-backed
|
|
// painter so font metrics match the screen painter in paint().
|
|
// m_update_map=true allows the draw functions to populate the map;
|
|
// paint() calls them with m_update_map=false so the map is stable.
|
|
QImage dummy(1, 1, QImage::Format_ARGB32_Premultiplied);
|
|
QPainter qp(&dummy);
|
|
QPen pen_;
|
|
pen_.setWidthF(0.5);
|
|
qp.setPen(pen_);
|
|
qp.setFont(QETApp::diagramTextsFont(5));
|
|
|
|
// PLC table is drawn and managed entirely by CrossRefItem
|
|
if (m_element->elementData().m_master_type == ElementData::PLC)
|
|
{
|
|
// Position at the PLC table position from the .elmt definition
|
|
QList<QPointF> positions = m_element->plcTablePositions();
|
|
QPointF pos = positions.isEmpty() ? QPointF(0, 0) : positions.first();
|
|
setPos(pos);
|
|
|
|
// Populate m_hovered_contacts_map using drawAsPlcTable on a
|
|
// dummy painter (m_update_map=true).
|
|
m_update_map = true;
|
|
drawAsPlcTable(qp);
|
|
m_update_map = false;
|
|
|
|
update();
|
|
QTimer::singleShot(0, this, [this]{ update(); });
|
|
return;
|
|
}
|
|
//Draw cross or contact, if master element is linked, or if the user
|
|
//asks for the contact comb to show the contact groups of the master
|
|
//even before they get a slave.
|
|
else if (! m_element->linkedElements().isEmpty()
|
|
|| mustDrawAllConfiguredSlaves())
|
|
{
|
|
m_update_map = true;
|
|
XRefProperties::DisplayHas dh = m_properties.displayHas();
|
|
|
|
if (dh == XRefProperties::Cross)
|
|
drawAsCross(qp);
|
|
else if (dh == XRefProperties::Contacts)
|
|
drawAsContacts(qp);
|
|
m_update_map = false;
|
|
}
|
|
|
|
autoPos();
|
|
update();
|
|
// Schedule a second update after the scene has finished laying out,
|
|
// so the initial render uses the correct bounding rect.
|
|
QTimer::singleShot(0, this, [this]{ update(); });
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::autoPos
|
|
Calculate and set position automatically.
|
|
*/
|
|
void CrossRefItem::autoPos()
|
|
{
|
|
// For PLC masters, position is set by updateLabel() based on
|
|
// m_plc_table_positions - don't override it here.
|
|
if (m_element->elementData().m_master_type == ElementData::PLC)
|
|
return;
|
|
|
|
//We calculate the position according to the snapTo of the xrefproperties
|
|
if (m_properties.snapTo() == XRefProperties::Bottom)
|
|
stackAtBottom();
|
|
else
|
|
QGIUtility::centerToParentBottom(this);
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::stackAtBottom
|
|
Places every cross reference of this folio that snaps to the bottom.
|
|
Each one is centred under its element at the bottom of the folio, and
|
|
when its texts would be drawn over the texts of another one, the one
|
|
of the higher element goes above the other: several coils in one
|
|
column get their crosses stacked in the same order as the coils,
|
|
instead of all on the same spot. Crosses that only touch, or overlap
|
|
without any text over text, stay where they are.
|
|
The types whose properties do not ask for stacking are only centred.
|
|
*/
|
|
void CrossRefItem::stackAtBottom()
|
|
{
|
|
Diagram *diagram = m_element->diagram();
|
|
if (!diagram) return;
|
|
|
|
QList<CrossRefItem *> xrefs;
|
|
for (QGraphicsItem *item : diagram->items()) {
|
|
if (item->type() != CrossRefItem::Type) continue;
|
|
auto xref = static_cast<CrossRefItem *>(item);
|
|
if (xref->m_properties.snapTo() == XRefProperties::Bottom
|
|
&& !xref->m_text && !xref->m_group
|
|
&& xref->m_element->elementData().m_master_type != ElementData::PLC)
|
|
xrefs << xref;
|
|
}
|
|
if (!xrefs.contains(this)) xrefs << this;
|
|
|
|
// The lowest element first, so it keeps the bottom of the folio.
|
|
// Position, then uuid, so the order is the same on every load.
|
|
std::sort(xrefs.begin(), xrefs.end(),
|
|
[](const CrossRefItem *a, const CrossRefItem *b) {
|
|
const QPointF pa = a->m_element->sceneBoundingRect().center();
|
|
const QPointF pb = b->m_element->sceneBoundingRect().center();
|
|
if (pa.y() != pb.y()) return pa.y() > pb.y();
|
|
if (pa.x() != pb.x()) return pa.x() < pb.x();
|
|
return a->m_element->uuid() < b->m_element->uuid();
|
|
});
|
|
|
|
struct Placed { QRectF rect; QList<QRectF> texts; };
|
|
const qreal gap = 5;
|
|
QList<Placed> placed;
|
|
for (CrossRefItem *xref : std::as_const(xrefs)) {
|
|
const qreal offset = xref->m_properties.offset();
|
|
QGIUtility::centerToBottomDiagram(xref, xref->m_element,
|
|
offset <= 40 ? 5 : offset);
|
|
if (xref->boundingRect().isEmpty()
|
|
|| !xref->m_properties.stackOverlapping())
|
|
continue;
|
|
|
|
Placed self;
|
|
self.rect = xref->sceneBoundingRect();
|
|
const QTransform to_scene = xref->sceneTransform();
|
|
for (const QRectF &text : std::as_const(xref->m_text_rects))
|
|
self.texts << to_scene.mapRect(text);
|
|
const qreal bottom = self.rect.bottom();
|
|
|
|
// Move up past every cross already placed whose texts it would
|
|
// draw over. Each move goes strictly up, so this ends.
|
|
for (bool moved = true; moved; ) {
|
|
moved = false;
|
|
for (const Placed &other : std::as_const(placed)) {
|
|
const qreal dy = other.rect.top() - gap - self.rect.bottom();
|
|
if (dy >= 0 || !textsOverlap(self.texts, other.texts))
|
|
continue;
|
|
self.rect.translate(0, dy);
|
|
for (QRectF &text : self.texts) text.translate(0, dy);
|
|
moved = true;
|
|
}
|
|
}
|
|
if (self.rect.bottom() != bottom)
|
|
xref->setPos(xref->parentItem()->mapFromScene(
|
|
xref->scenePos() + QPointF(0, self.rect.bottom() - bottom)));
|
|
placed << self;
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::textsOverlap
|
|
@return true if one of the rects of a intersects one of the rects of b
|
|
*/
|
|
bool CrossRefItem::textsOverlap(const QList<QRectF> &a, const QList<QRectF> &b)
|
|
{
|
|
for (const QRectF &ra : a)
|
|
for (const QRectF &rb : b)
|
|
if (ra.intersects(rb)) return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::sceneEvent
|
|
@param event
|
|
@return
|
|
*/
|
|
bool CrossRefItem::sceneEvent(QEvent *event)
|
|
{
|
|
//By default when a QGraphicsItem is a child of a QGraphicsItemGroup
|
|
//all events are forwarded to group.
|
|
//We override it, when this Xref is in a group
|
|
if(m_group)
|
|
{
|
|
switch (event->type())
|
|
{
|
|
case QEvent::GraphicsSceneHoverEnter:
|
|
hoverEnterEvent(static_cast<QGraphicsSceneHoverEvent *>(event));
|
|
break;
|
|
case QEvent::GraphicsSceneHoverMove:
|
|
hoverMoveEvent(static_cast<QGraphicsSceneHoverEvent *>(event));
|
|
break;
|
|
case QEvent::GraphicsSceneHoverLeave:
|
|
hoverLeaveEvent(static_cast<QGraphicsSceneHoverEvent *>(event));
|
|
break;
|
|
case QEvent::GraphicsSceneMouseDoubleClick:
|
|
mouseDoubleClickEvent(static_cast<QGraphicsSceneMouseEvent *>(event));
|
|
break;
|
|
default:break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return QGraphicsObject::sceneEvent(event);
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::paint
|
|
Paint this item
|
|
@param painter
|
|
@param option
|
|
@param widget
|
|
*/
|
|
void CrossRefItem::paint(
|
|
QPainter *painter,
|
|
const QStyleOptionGraphicsItem *option,
|
|
QWidget *widget)
|
|
{
|
|
Q_UNUSED(option)
|
|
Q_UNUSED(widget)
|
|
// Draw directly — no QPicture involved anywhere.
|
|
// QPicture::play() + nested drawPicture() (m_hdr_no_ctc/m_hdr_nc_ctc)
|
|
// caused a use-after-free crash (QRegion::begin, Qt5Gui+0x49af60)
|
|
// confirmed by analysis of 19+ coredumps.
|
|
// m_update_map=false: draw functions do not overwrite m_hovered_contacts_map.
|
|
|
|
// PLC: do not draw here (Element::drawPlcTable handles visual rendering).
|
|
// The m_hovered_contacts_map was populated in updateLabel() for
|
|
// click navigation and PDF hyperlink injection.
|
|
if (m_element->elementData().m_master_type == ElementData::PLC)
|
|
return;
|
|
|
|
if (m_element->linkedElements().isEmpty()
|
|
&& !mustDrawAllConfiguredSlaves()) return;
|
|
|
|
QPen pen_;
|
|
pen_.setWidthF(0.5);
|
|
painter->save();
|
|
painter->setPen(pen_);
|
|
painter->setFont(QETApp::diagramTextsFont(5));
|
|
|
|
m_update_map = false;
|
|
XRefProperties::DisplayHas dh = m_properties.displayHas();
|
|
if (dh == XRefProperties::Cross)
|
|
drawAsCross(*painter);
|
|
else if (dh == XRefProperties::Contacts)
|
|
drawAsContacts(*painter);
|
|
painter->restore();
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::mouseDoubleClickEvent
|
|
@param event
|
|
*/
|
|
void CrossRefItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
|
|
{
|
|
event->accept();
|
|
|
|
// Find the element under the click position directly from the map,
|
|
// rather than relying on m_hovered_contact which may have been reset
|
|
// by hoverMoveEvent between the two clicks of the double-click.
|
|
QPointF pos = event->pos();
|
|
Element *target = m_hovered_contact;
|
|
|
|
if (!target) {
|
|
for (auto it = m_hovered_contacts_map.begin();
|
|
it != m_hovered_contacts_map.end(); ++it) {
|
|
if (it.value().contains(pos)) {
|
|
target = it.key();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
QetGraphicsItem::showItem(target);
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::hoverEnterEvent
|
|
@param event
|
|
*/
|
|
void CrossRefItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
|
|
{
|
|
m_hovered_contact = nullptr;
|
|
QGraphicsObject::hoverEnterEvent(event);
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::hoverMoveEvent
|
|
@param event
|
|
*/
|
|
void CrossRefItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
|
|
{
|
|
QPointF pos = event->pos();
|
|
|
|
if (m_hovered_contact)
|
|
{
|
|
foreach(QRectF rect, m_hovered_contacts_map.values(m_hovered_contact))
|
|
{
|
|
//Mouse hover the same rect than previous hover event
|
|
if (rect.contains(pos))
|
|
{
|
|
QGraphicsObject::hoverMoveEvent(event);
|
|
return;
|
|
}
|
|
}
|
|
|
|
//At this point, mouse don't hover previous rect
|
|
m_hovered_contact = nullptr;
|
|
|
|
foreach (Element *elmt, m_hovered_contacts_map.keys())
|
|
{
|
|
foreach(QRectF rect, m_hovered_contacts_map.values(elmt))
|
|
{
|
|
//Mouse hover a contact
|
|
if (rect.contains(pos))
|
|
{
|
|
m_hovered_contact = elmt;
|
|
}
|
|
}
|
|
}
|
|
|
|
updateLabel();
|
|
QGraphicsObject::hoverMoveEvent(event);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
foreach (Element *elmt, m_hovered_contacts_map.keys())
|
|
{
|
|
foreach(QRectF rect, m_hovered_contacts_map.values(elmt))
|
|
{
|
|
//Mouse hover a contact
|
|
if (rect.contains(pos))
|
|
{
|
|
m_hovered_contact = elmt;
|
|
updateLabel();
|
|
QGraphicsObject::hoverMoveEvent(event);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::hoverLeaveEvent
|
|
@param event
|
|
*/
|
|
void CrossRefItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
|
|
{
|
|
m_hovered_contact = nullptr;
|
|
updateLabel();
|
|
QGraphicsObject::hoverLeaveEvent(event);
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::linkedChanged
|
|
*/
|
|
void CrossRefItem::linkedChanged()
|
|
{
|
|
for(const QMetaObject::Connection& c : m_slave_connection)
|
|
disconnect(c);
|
|
|
|
m_slave_connection.clear();
|
|
|
|
//Hidden only by View > Show: keep following the slaves, so the
|
|
//label is right when it is shown again
|
|
if(!isVisible() && !ShownKinds::isHidden(this))
|
|
return;
|
|
|
|
for(Element *elmt : m_element->linkedElements())
|
|
{
|
|
m_slave_connection << connect(elmt,
|
|
&Element::xChanged,
|
|
this,
|
|
&CrossRefItem::updateLabel);
|
|
m_slave_connection << connect(elmt,
|
|
&Element::yChanged,
|
|
this,
|
|
&CrossRefItem::updateLabel);
|
|
}
|
|
|
|
updateLabel();
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::buildHeaderContact
|
|
Draw NO and NC contact symbols directly onto painter.
|
|
Previously used QPicture (m_hdr_no_ctc/m_hdr_nc_ctc) which caused
|
|
use-after-free crashes via nested QPicture::play() calls.
|
|
*/
|
|
void CrossRefItem::buildHeaderContact(QPainter &painter, QPointF no_pos, QPointF nc_pos)
|
|
{
|
|
QPen pen_;
|
|
pen_.setWidthF(0.2);
|
|
painter.save();
|
|
painter.setPen(pen_);
|
|
|
|
//draw the NO contact header symbol
|
|
painter.drawLine(no_pos.x()+0, no_pos.y()+3, no_pos.x()+5, no_pos.y()+3);
|
|
QPointF p1[3] = {
|
|
QPointF(no_pos.x()+5, no_pos.y()+0),
|
|
QPointF(no_pos.x()+10, no_pos.y()+3),
|
|
QPointF(no_pos.x()+15, no_pos.y()+3),
|
|
};
|
|
painter.drawPolyline(p1, 3);
|
|
|
|
//draw the NC contact header symbol
|
|
QPointF p2[3] = {
|
|
QPointF(nc_pos.x()+0, nc_pos.y()+3),
|
|
QPointF(nc_pos.x()+5, nc_pos.y()+3),
|
|
QPointF(nc_pos.x()+5, nc_pos.y()+0)
|
|
};
|
|
painter.drawPolyline(p2, 3);
|
|
QPointF p3[3] = {
|
|
QPointF(nc_pos.x()+4, nc_pos.y()+0),
|
|
QPointF(nc_pos.x()+10, nc_pos.y()+3),
|
|
QPointF(nc_pos.x()+15, nc_pos.y()+3),
|
|
};
|
|
painter.drawPolyline(p3, 3);
|
|
|
|
painter.restore();
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::setUpCrossBoundingRect
|
|
Get the numbers of slaves elements linked to this parent element,
|
|
for calculate the size of the cross bounding rect.
|
|
The cross ref item is drawing according to the size of
|
|
the cross bounding rect.
|
|
@param painter
|
|
*/
|
|
void CrossRefItem::setUpCrossBoundingRect(QPainter &painter)
|
|
{
|
|
//No need to calcul if nothing is linked
|
|
if (m_element->isFree()) return;
|
|
|
|
QStringList no_str, nc_str;
|
|
|
|
// Helper lambda: build "[13-14] pos" string for an element.
|
|
// For power contacts (e.g. 3-pole contactor), all named terminals
|
|
// are collected (e.g. "[1-2-3-4-5-6] pos").
|
|
// For other contacts (NO/NC/SW), only the first 2 named terminals
|
|
// are used — users are expected to name and order their terminals
|
|
// in the element editor.
|
|
// Helper lambda: build "[13-14] pos" for an element.
|
|
// - Power: all terminals sorted numerically → "[1-2-3-4-5-6] pos"
|
|
// - SW with typed terminals: show relevant pair per column (handled below)
|
|
// - Others: first 2 named terminals
|
|
auto buildLabel = [this](Element *elmt, bool is_no_col) -> QString {
|
|
const bool is_power =
|
|
elmt->kindInformations()["type"].toString() == "power";
|
|
const bool is_sw =
|
|
elmt->kindInformations()["state"].toString() == "SW";
|
|
|
|
QStringList tnames;
|
|
|
|
if (is_sw) {
|
|
// Check if terminals have explicit No/Nc/Common types
|
|
bool has_typed = false;
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
if (t->terminalType() == TerminalData::No ||
|
|
t->terminalType() == TerminalData::Nc ||
|
|
t->terminalType() == TerminalData::Common) {
|
|
has_typed = true; break;
|
|
}
|
|
}
|
|
if (has_typed) {
|
|
QString no_name, nc_name, common_name;
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
if (!t->name().isEmpty()) {
|
|
if (t->terminalType() == TerminalData::No) no_name = t->name();
|
|
else if (t->terminalType() == TerminalData::Nc) nc_name = t->name();
|
|
else if (t->terminalType() == TerminalData::Common) common_name = t->name();
|
|
}
|
|
}
|
|
// NO column: show NO+Common pair; NC column: show NC+Common pair
|
|
if (is_no_col)
|
|
tnames << no_name << common_name;
|
|
else
|
|
tnames << nc_name << common_name;
|
|
} else {
|
|
// Fallback: first 2 named terminals
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
const QString tn = t->name();
|
|
if (!tn.isEmpty()) { tnames << tn; if (tnames.size() >= 2) break; }
|
|
}
|
|
}
|
|
} else {
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
const QString tn = t->name();
|
|
if (!tn.isEmpty()) {
|
|
tnames << tn;
|
|
if (!is_power && tnames.size() >= 2) break;
|
|
}
|
|
}
|
|
if (is_power) {
|
|
std::sort(tnames.begin(), tnames.end(),
|
|
[](const QString &a, const QString &b){
|
|
int i_a = a.size();
|
|
while (i_a > 0 && a[i_a-1].isDigit()) --i_a;
|
|
int i_b = b.size();
|
|
while (i_b > 0 && b[i_b-1].isDigit()) --i_b;
|
|
bool a_ok = false, b_ok = false;
|
|
int ai = a.mid(i_a).toInt(&a_ok);
|
|
int bi = b.mid(i_b).toInt(&b_ok);
|
|
if (a_ok && b_ok && a.left(i_a) == b.left(i_b))
|
|
return ai < bi;
|
|
return a < b;
|
|
});
|
|
}
|
|
}
|
|
|
|
QString pos = elementPositionText(elmt, true);
|
|
if (!tnames.isEmpty() && m_properties.showTerminalName())
|
|
return QStringLiteral("[") + tnames.join("-") + QStringLiteral("] ") + pos;
|
|
return pos;
|
|
};
|
|
|
|
for (auto elmt : NOElements()) {
|
|
no_str.append(buildLabel(elmt, true));
|
|
}
|
|
for (auto elmt : NCElements()) {
|
|
nc_str.append(buildLabel(elmt, false));
|
|
}
|
|
|
|
//There is no string to display, we return now
|
|
if (no_str.isEmpty() && nc_str.isEmpty()) return;
|
|
|
|
//this is the default size of cross ref item
|
|
QRectF default_bounding(0, 0, 40, header + cross_min_height);
|
|
|
|
//Bounding rect of the NO text
|
|
QRectF no_bounding;
|
|
for (auto str : no_str)
|
|
{
|
|
QRectF bounding = painter.boundingRect(QRectF(0, 0, 500, 20), Qt::AlignLeft, str);
|
|
no_bounding.setHeight(no_bounding.height() + bounding.height());
|
|
if (bounding.width() > no_bounding.width())
|
|
no_bounding.setWidth(bounding.width());
|
|
}
|
|
//Adjust according to the NO
|
|
if (no_bounding.height() > default_bounding.height() - header)
|
|
default_bounding.setHeight(no_bounding.height() + header); //adjust the height
|
|
if (no_bounding.width() > default_bounding.width()/2)
|
|
default_bounding.setWidth(no_bounding.width()*2);//adjust the width
|
|
|
|
//Bounding rect of the NC text
|
|
QRectF nc_bounding;
|
|
for (auto str : nc_str)
|
|
{
|
|
QRectF bounding = painter.boundingRect(QRectF(0, 0, 500, 20), Qt::AlignLeft, str);
|
|
nc_bounding.setHeight(nc_bounding.height() + bounding.height());
|
|
if (bounding.width() > nc_bounding.width())
|
|
nc_bounding.setWidth(bounding.width());
|
|
}
|
|
//Adjust according to the NC
|
|
if (nc_bounding.height() > default_bounding.height() - header)
|
|
default_bounding.setHeight(nc_bounding.height() + header); //adjust the height
|
|
if (nc_bounding.width() > default_bounding.width()/2)
|
|
default_bounding.setWidth(nc_bounding.width()*2);//adjust the width
|
|
|
|
//Minor adjustement for better visual
|
|
default_bounding.adjust(0, 0, 4, 0);
|
|
m_shape_path.addRect(default_bounding);
|
|
prepareGeometryChange();
|
|
m_bounding_rect = default_bounding;
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::drawAsCross
|
|
Draw this crossref with a cross
|
|
@param painter: painter to use
|
|
*/
|
|
void CrossRefItem::drawAsCross(QPainter &painter)
|
|
{
|
|
//calculate the size of the cross
|
|
setUpCrossBoundingRect(painter);
|
|
m_drawed_contacts = 0;
|
|
if (m_update_map) m_hovered_contacts_map.clear();
|
|
|
|
//Bounding rect is empty that mean there's no contact to draw
|
|
if (boundingRect().isEmpty()) return;
|
|
|
|
//draw the cross
|
|
QRectF br = boundingRect();
|
|
painter.drawLine(br.width()/2, 0, br.width()/2, br.height()); //vertical line
|
|
painter.drawLine(0, header, br.width(), header); //horizontal line
|
|
|
|
//Add the symbolic contacts (drawn directly, no QPicture)
|
|
static const qreal hdr_symbol_width = 15.0;
|
|
QPointF no_pos((m_bounding_rect.width()/4) - (hdr_symbol_width/2), 0);
|
|
QPointF nc_pos((m_bounding_rect.width() * 3/4) - (hdr_symbol_width/2), 0);
|
|
buildHeaderContact(painter, no_pos, nc_pos);
|
|
|
|
//and fill it
|
|
fillCrossRef(painter);
|
|
}
|
|
|
|
namespace {
|
|
/**
|
|
@brief contactOption
|
|
Map the contact group declared by a master onto the CONTACTS flags
|
|
used by CrossRefItem::drawContact, so a group no slave is linked to
|
|
yet is drawn like the slave it waits for.
|
|
@param group : the contact group of the master
|
|
@return the flags describing the contact to draw
|
|
*/
|
|
int contactOption(const ElementData::SlaveContactGroup &group)
|
|
{
|
|
int option = 0;
|
|
|
|
switch (group.type)
|
|
{
|
|
case ElementData::NO: option = CrossRefItem::NO; break;
|
|
case ElementData::NC: option = CrossRefItem::NC; break;
|
|
case ElementData::SW: option = CrossRefItem::SW; break;
|
|
case ElementData::Other: option = CrossRefItem::Other; break;
|
|
}
|
|
|
|
switch (group.subtype)
|
|
{
|
|
case ElementData::Power: option += CrossRefItem::Power; break;
|
|
case ElementData::DelayOn: option += CrossRefItem::DelayOn; break;
|
|
case ElementData::DelayOff: option += CrossRefItem::DelayOff; break;
|
|
case ElementData::delayOnOff: option += CrossRefItem::DelayOnOff; break;
|
|
case ElementData::SSimple:
|
|
case ElementData::PLCSlave: break;
|
|
}
|
|
|
|
return option;
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::drawAsContacts
|
|
Draw this crossref with symbolic contacts
|
|
@param painter painter to use
|
|
*/
|
|
void CrossRefItem::drawAsContacts(QPainter &painter)
|
|
{
|
|
if (m_element -> isFree() && !mustDrawAllConfiguredSlaves())
|
|
return;
|
|
|
|
m_drawed_contacts = 0;
|
|
if (m_update_map) m_hovered_contacts_map.clear();
|
|
QRectF bounding_rect;
|
|
|
|
//Draw every contact group of the master, in the order the master
|
|
//defines them, when the user asked for it and the master declares
|
|
//contact groups. Otherwise the comb keeps its historical behavior:
|
|
//linked slaves only, in position order.
|
|
if (mustDrawAllConfiguredSlaves())
|
|
{
|
|
const QVector<ElementData::SlaveContactGroup> groups =
|
|
m_element->elementData().m_slave_contact_groups;
|
|
|
|
//A contact group waits for exactly one slave: a slave assigned to
|
|
//a group is drawn where the master puts it, whatever its position
|
|
//on the diagram.
|
|
QHash<int, Element *> slotted;
|
|
QList<Element *> unassigned;
|
|
foreach (Element *elmt, m_element->linkedElements()) //position order
|
|
{
|
|
const int index = m_element->groupIndexForElement(elmt);
|
|
if (index >= 0 && index < groups.size() && !slotted.contains(index))
|
|
slotted.insert(index, elmt);
|
|
else
|
|
unassigned << elmt;
|
|
}
|
|
|
|
for (int i = 0; i < groups.size(); ++i)
|
|
{
|
|
if (Element *slave = slotted.value(i, nullptr))
|
|
bounding_rect = bounding_rect.united(
|
|
drawLinkedSlaveContacts(painter, slave));
|
|
else
|
|
{
|
|
//No slave is linked to this group yet: the contact
|
|
//symbol of the group is drawn with the terminal names
|
|
//the master defines for it, there is no cross reference
|
|
//to show for it.
|
|
const int option = contactOption(groups.at(i));
|
|
const int poles = qMax(1, groups.at(i).contactCount);
|
|
QStringList labels = groups.at(i).labels;
|
|
|
|
//A single pole simple contact (NO or NC) reads its two
|
|
//numbers the other way round (checked against the
|
|
//diagram). Changeover contacts are not handled here:
|
|
//their labels are mapped to the right contact half in
|
|
//drawContact(), per pole, so multi pole changeovers work
|
|
//too. Groups with several NO/NC poles keep the order the
|
|
//master defines: a 3 pole power contact already reads
|
|
//correctly that way.
|
|
if (poles == 1 && (option & NOC))
|
|
std::reverse(labels.begin(), labels.end());
|
|
|
|
//The declared terminals are distributed over the declared
|
|
//poles. terminalCount and contactCount are edited
|
|
//independently in the element editor, so the list can be
|
|
//shorter than two entries per pole (three for a switch):
|
|
//every pole then gets its share of what exists, instead
|
|
//of a fixed 2/3 stride starving all but the first poles.
|
|
const int per_pole = labels.size() / poles;
|
|
const int extra = labels.size() % poles;
|
|
int begin = 0;
|
|
for (int pole = 0; pole < poles; ++pole)
|
|
{
|
|
const int count = per_pole + (pole < extra ? 1 : 0);
|
|
const QStringList pole_labels = labels.mid(begin, count);
|
|
begin += count;
|
|
bounding_rect = bounding_rect.united(
|
|
drawContact(painter,
|
|
option,
|
|
nullptr,
|
|
pole,
|
|
pole_labels));
|
|
}
|
|
}
|
|
}
|
|
|
|
//Slaves the master doesn't assign to one of its groups (a link
|
|
//made before the master declared groups, for example) keep their
|
|
//usual place: the end of the comb, in position order.
|
|
foreach (Element *elmt, unassigned)
|
|
bounding_rect = bounding_rect.united(
|
|
drawLinkedSlaveContacts(painter, elmt));
|
|
}
|
|
else
|
|
{
|
|
//Draw each linked contact, in position order
|
|
foreach (Element *elmt, m_element->linkedElements())
|
|
bounding_rect = bounding_rect.united(
|
|
drawLinkedSlaveContacts(painter, elmt));
|
|
}
|
|
|
|
bounding_rect.adjust(-list_margin_left, -4, list_margin_right, 4);
|
|
prepareGeometryChange();
|
|
m_bounding_rect = bounding_rect;
|
|
m_shape_path.addRect(bounding_rect);
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::drawLinkedSlaveContacts
|
|
Draw the contact symbols of one slave linked to this master.
|
|
@param painter : painter to use
|
|
@param elmt : the slave element to draw
|
|
@return the bounding rect of the draw
|
|
*/
|
|
QRectF CrossRefItem::drawLinkedSlaveContacts(QPainter &painter, Element *elmt)
|
|
{
|
|
QRectF bounding_rect;
|
|
DiagramContext info = elmt->kindInformations();
|
|
|
|
for (int i=0; i<info["number"].toInt(); i++)
|
|
{
|
|
int option = 0;
|
|
|
|
QString state = info["state"].toString();
|
|
if (state == "NO") option = NO;
|
|
else if (state == "NC") option = NC;
|
|
else if (state == "SW") option = SW;
|
|
else if (state == "Other") option = Other;
|
|
|
|
QString type = info["type"].toString();
|
|
if (type == "power") option += Power;
|
|
else if (type == "delayOn") option += DelayOn;
|
|
else if (type == "delayOff") option += DelayOff;
|
|
else if (type == "delayOnOff") option += DelayOnOff;
|
|
|
|
bounding_rect = bounding_rect.united(
|
|
drawContact(painter, option, elmt, i));
|
|
}
|
|
|
|
return bounding_rect;
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::drawContact
|
|
Draw one contact, the type of contact to draw is define in flags.
|
|
@param painter : painter to use
|
|
@param flags : define how to draw the contact (see enul CONTACTS)
|
|
@param elmt : the element to display text (the position of the contact).
|
|
It may be nullptr when the contact comes from a contact group the master
|
|
defines but no slave is linked to yet: no position text, no hover/click
|
|
support, and the terminal names then come from master_labels.
|
|
@param pole_index : which contact of the group is drawn (0 based), used
|
|
to pick the right pair of terminal names of a linked multi-pole contact.
|
|
@param master_labels : the terminal names the master declares for this
|
|
pole (sliced from ElementData::SlaveContactGroup::labels by the caller),
|
|
used when elmt is nullptr so an empty slot shows the numbers the master
|
|
declares, the same way a linked slave would show them.
|
|
@return The bounding rect of the draw (contact + text)
|
|
*/
|
|
QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, int pole_index, const QStringList &master_labels)
|
|
{
|
|
QString str = elmt ? elementPositionText(elmt) : QString();
|
|
|
|
// Collect terminal names from the element definition (.elmt)
|
|
// e.g. name="13" and name="14" on each terminal
|
|
// For power contacts, sort numerically and pick the pair for pole_index.
|
|
// For SW contacts with typed terminals (No/Nc/Common), filter by role.
|
|
QStringList terminal_names;
|
|
const bool is_power_ctc =
|
|
elmt && elmt->kindInformations()["type"].toString() == "power";
|
|
const bool is_sw = (flags & SW) && !(flags & NOC);
|
|
|
|
// Check if SW terminals have explicit No/Nc/Common types
|
|
bool sw_has_typed_terminals = false;
|
|
if (is_sw && elmt) {
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
if (t->terminalType() == TerminalData::No ||
|
|
t->terminalType() == TerminalData::Nc ||
|
|
t->terminalType() == TerminalData::Common) {
|
|
sw_has_typed_terminals = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (elmt) {
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
const QString tname = t->name();
|
|
if (!tname.isEmpty())
|
|
terminal_names << tname;
|
|
}
|
|
} else if (!master_labels.isEmpty()) {
|
|
//Empty slot of the contact comb: the slave is missing but the
|
|
//master already declares the terminal names, so the slot shows
|
|
//them instead of staying mute. master_labels contains exactly
|
|
//the terminals of this pole (the caller slices the declared
|
|
//terminal list over the declared poles), in the order a linked
|
|
//slave would receive them.
|
|
if (is_sw) {
|
|
//The labels are stored in terminal order (for a typical
|
|
//changeover contact: common, NC, NO, i.e. 11, 12, 14), while
|
|
//the symbol draws NC bottom-left, NO top-left and the common
|
|
//on the right: every stored entry goes to its own position.
|
|
//Entries the master doesn't declare (terminal count lower
|
|
//than three) simply stay empty instead of landing on the
|
|
//wrong contact half like the raw stored order would.
|
|
terminal_names << master_labels.value(1)
|
|
<< master_labels.value(2)
|
|
<< master_labels.value(0);
|
|
} else {
|
|
terminal_names = master_labels;
|
|
}
|
|
}
|
|
|
|
if (is_power_ctc) {
|
|
// Sort terminals alphanumerically so names like "R1","R2"... or "1","2"...
|
|
// are ordered correctly. Extract trailing digits for numeric comparison;
|
|
// fall back to full string comparison when no digits are found.
|
|
std::sort(terminal_names.begin(), terminal_names.end(),
|
|
[](const QString &a, const QString &b){
|
|
// Extract trailing numeric part
|
|
int i_a = a.size();
|
|
while (i_a > 0 && a[i_a-1].isDigit()) --i_a;
|
|
int i_b = b.size();
|
|
while (i_b > 0 && b[i_b-1].isDigit()) --i_b;
|
|
bool a_ok = false, b_ok = false;
|
|
int ai = a.mid(i_a).toInt(&a_ok);
|
|
int bi = b.mid(i_b).toInt(&b_ok);
|
|
if (a_ok && b_ok && a.left(i_a) == b.left(i_b))
|
|
return ai < bi;
|
|
return a < b;
|
|
});
|
|
// Pick the pair for this pole: pole 0 → [0,1], pole 1 → [2,3], etc.
|
|
int idx = pole_index * 2;
|
|
if (idx + 1 < terminal_names.size())
|
|
terminal_names = QStringList() << terminal_names[idx] << terminal_names[idx+1];
|
|
else
|
|
terminal_names.clear();
|
|
} else if (is_sw && sw_has_typed_terminals) {
|
|
// Build [NO_name, Common_name, NC_name] from typed terminals
|
|
QString no_name, nc_name, common_name;
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
if (!t->name().isEmpty()) {
|
|
if (t->terminalType() == TerminalData::No) no_name = t->name();
|
|
else if (t->terminalType() == TerminalData::Nc) nc_name = t->name();
|
|
else if (t->terminalType() == TerminalData::Common) common_name = t->name();
|
|
}
|
|
}
|
|
// drawText expects: [0]=NC, [1]=NO, [2]=Common
|
|
// (drawText uses [1] for NO top-left, [0] for NC bottom-left, [2] for Common right)
|
|
terminal_names.clear();
|
|
terminal_names << nc_name << no_name << common_name;
|
|
}
|
|
|
|
int offset = m_drawed_contacts*10;
|
|
QRectF bounding_rect = QRectF(0, offset, 24, 10);
|
|
|
|
QPen pen = painter.pen();
|
|
elmt && m_hovered_contact == elmt ? pen.setColor(Qt::blue) :pen.setColor(Qt::black);
|
|
painter.setPen(pen);
|
|
|
|
//Draw NO or NC contact
|
|
if (flags &NOC)
|
|
{
|
|
bounding_rect = QRectF(0, offset, 24, 10);
|
|
|
|
//draw the basic line
|
|
painter.drawLine(0, offset+6, 8, offset+6);
|
|
painter.drawLine(16, offset+6, 24, offset+6);
|
|
|
|
// Draw terminal names on each side of the contact symbol
|
|
// terminal_names[0] on the left, terminal_names[1] on the right
|
|
if (!terminal_names.isEmpty() && m_properties.showTerminalName()) {
|
|
painter.setFont(QETApp::diagramTextsFont(4));
|
|
QRectF bt(0, offset, 24, 10);
|
|
if (terminal_names.size() >= 1)
|
|
drawText(painter, bt, Qt::AlignLeft|Qt::AlignTop, terminal_names[0]);
|
|
if (terminal_names.size() >= 2)
|
|
drawText(painter, bt, Qt::AlignRight|Qt::AlignTop, terminal_names[1]);
|
|
painter.setFont(QETApp::diagramTextsFont(5));
|
|
}
|
|
|
|
//draw open contact
|
|
if (flags &NO) {
|
|
painter.drawLine(8, offset+9, 16, offset+6);
|
|
}
|
|
//draw close contact
|
|
if (flags &NC) {
|
|
QPointF p1[3] = {
|
|
QPointF(8, offset+6),
|
|
QPointF(9, offset+6),
|
|
QPointF(9, offset+2.5)
|
|
};
|
|
painter.drawPolyline(p1,3);
|
|
painter.drawLine(8, offset+3, 16, offset+6);
|
|
}
|
|
|
|
//draw half circle for power contact
|
|
if (flags &Power) {
|
|
QRectF arc(4, offset+4, 4, 4);
|
|
if (flags &NO)
|
|
painter.drawArc(arc, 180*16, 180*16);
|
|
else
|
|
painter.drawArc(arc, 0, 180*16);
|
|
}
|
|
|
|
// draw half circle for delay contact
|
|
if(flags &Delay) {
|
|
// for delay on contact
|
|
if (flags &DelayOn) {
|
|
if (flags &NO) {
|
|
painter.drawLine(12, offset+8, 12, offset+11);
|
|
QRectF r(9.5, offset+9, 5, 3);
|
|
painter.drawArc(r, 180*16, 180*16);
|
|
}
|
|
if (flags &NC) {
|
|
painter.drawLine(QPointF(12.5, offset+5), QPointF(12.5, offset+8));
|
|
QRectF r(10, offset+6, 5, 3);
|
|
painter.drawArc(r, 180*16, 180*16);
|
|
}
|
|
}
|
|
// for delay off contact
|
|
else if ( flags &DelayOff){
|
|
if (flags &NO) {
|
|
painter.drawLine(12, offset+8, 12, offset+9.5);
|
|
QRectF r(9.5, offset+9.5, 5, 3);
|
|
painter.drawArc(r, 0, 180*16);
|
|
}
|
|
if (flags &NC) {
|
|
painter.drawLine(QPointF(12.5, offset+5), QPointF(12.5, offset+7));
|
|
QRectF r(10, offset+7.5, 5, 3);
|
|
painter.drawArc(r, 0, 180*16);
|
|
}
|
|
|
|
}
|
|
else {
|
|
// for delay on contact
|
|
if (flags &NO) {
|
|
painter.drawLine(12, offset+8, 12, offset+11);
|
|
QRectF r(9.5, offset+11.7, 5, 3);
|
|
painter.drawArc(r, 0, 180*16);
|
|
QRectF rl(9.5, offset+9, 5, 3);
|
|
painter.drawArc(rl, 180*16, 180*16);
|
|
}
|
|
if (flags &NC) {
|
|
painter.drawLine(QPointF(12.5, offset+5), QPointF(12.5, offset+8));
|
|
QRectF r(9.5, offset+10.7, 5, 3);
|
|
painter.drawArc(r, 0, 180*16);
|
|
QRectF rl(9.5, offset+8, 5, 3);
|
|
painter.drawArc(rl, 180*16, 180*16);
|
|
}
|
|
}
|
|
}
|
|
|
|
//The hit rect is registered even when the position text is
|
|
//empty: a linked contact had (and keeps) its hover/click entry
|
|
//in that case too, only the drawing is skipped. Free slots
|
|
//(elmt == nullptr) have nothing to click and stay out of the map.
|
|
QRectF text_rect = painter.boundingRect(QRectF(30, offset, 5, 10), Qt::AlignLeft | Qt::AlignVCenter, str);
|
|
if (!str.isEmpty())
|
|
{
|
|
drawText(painter, text_rect, Qt::AlignLeft | Qt::AlignVCenter, str);
|
|
bounding_rect = bounding_rect.united(text_rect);
|
|
}
|
|
|
|
if (m_update_map && elmt)
|
|
m_hovered_contacts_map.insert(elmt, text_rect);
|
|
|
|
++m_drawed_contacts;
|
|
}
|
|
|
|
//Draw a switch contact
|
|
|
|
|
|
|
|
else if (flags &SW)
|
|
{
|
|
bounding_rect = QRectF(0, offset, 24, 20);
|
|
bounding_rect.adjust(0, -4, 4, 4);
|
|
|
|
//draw the NO side
|
|
painter.drawLine(0, offset+6, 8, offset+6);
|
|
//Draw the NC side
|
|
QPointF p1[3] = {
|
|
QPointF(0, offset+16),
|
|
QPointF(8, offset+16),
|
|
QPointF(8, offset+12)
|
|
};
|
|
painter.drawPolyline(p1, 3);
|
|
|
|
//Draw the common side
|
|
QPointF p2[3] = {
|
|
QPointF(7, offset+14),
|
|
QPointF(16, offset+11),
|
|
QPointF(24, offset+11),
|
|
};
|
|
painter.drawPolyline(p2, 3);
|
|
|
|
// Draw terminal names for switch contact (3 terminals)
|
|
// terminal_names[0] = NC (bottom-left)
|
|
// terminal_names[1] = NO (top-left)
|
|
// terminal_names[2] = Common (right)
|
|
if (!terminal_names.isEmpty() && m_properties.showTerminalName()) {
|
|
painter.setFont(QETApp::diagramTextsFont(4));
|
|
// Storage order set above: [0]=NC, [1]=NO, [2]=Common
|
|
if (terminal_names.size() >= 2)
|
|
drawText(painter, QRectF(0, offset, 8, 8),
|
|
Qt::AlignLeft|Qt::AlignTop, terminal_names[1]); // NO top-left
|
|
if (terminal_names.size() >= 3)
|
|
drawText(painter, QRectF(16, offset+4, 8, 6),
|
|
Qt::AlignRight|Qt::AlignTop, terminal_names[2]); // Common right
|
|
if (terminal_names.size() >= 1)
|
|
drawText(painter, QRectF(0, offset+9, 8, 6),
|
|
Qt::AlignLeft|Qt::AlignTop, terminal_names[0]); // NC bottom-left
|
|
painter.setFont(QETApp::diagramTextsFont(5));
|
|
}
|
|
|
|
//Draw the half ellipse off delay
|
|
if (flags &Delay)
|
|
{
|
|
painter.drawLine(12, offset+13, 12, offset+16);
|
|
if (flags &DelayOn) {
|
|
QRectF r(9.5, offset+14, 5, 3);
|
|
painter.drawArc(r, 180*16, 180*16);
|
|
}
|
|
else if (flags &DelayOff) {
|
|
QRectF r(9.5, offset+16.5, 5, 3);
|
|
painter.drawArc(r, 0, 180*16);
|
|
}
|
|
else if (flags &DelayOnOff) {
|
|
QRectF r(9.5, offset+14, 5, 3);
|
|
painter.drawArc(r, 180*16, 180*16);
|
|
QRectF rr(9.5, offset+17, 5, 3);
|
|
painter.drawArc(rr, 0, 180*16);
|
|
}
|
|
|
|
}
|
|
|
|
//Draw position text
|
|
QRectF text_rect = painter.boundingRect(
|
|
QRectF(30, offset+4, 5, 10),
|
|
Qt::AlignLeft | Qt::AlignVCenter,
|
|
str);
|
|
if (!str.isEmpty())
|
|
{
|
|
drawText(painter, text_rect,
|
|
Qt::AlignLeft | Qt::AlignVCenter,
|
|
str);
|
|
bounding_rect = bounding_rect.united(text_rect);
|
|
}
|
|
//Hit rect kept even for an empty position text (as before),
|
|
//free slots are not clickable.
|
|
if (m_update_map && elmt)
|
|
m_hovered_contacts_map.insert(elmt, text_rect);
|
|
|
|
//a switch contact take place of two normal contact
|
|
m_drawed_contacts += 2;
|
|
|
|
}
|
|
|
|
//Draw Other symbol "ↈ"
|
|
else if(flags &Other)
|
|
{
|
|
bounding_rect = QRectF(0, offset, 24, 20);
|
|
bounding_rect.adjust(0, -4, 4, 4);
|
|
|
|
//Draw the first arc symbol
|
|
QRectF r(8, offset+4, 5, 3);
|
|
painter.drawArc(r, 10*16, 270*16);
|
|
|
|
//Draw the second arc symbol
|
|
QRectF r2(11.2, offset+4, 5, 3);
|
|
painter.drawArc(r2, 160*16, 300*16);
|
|
|
|
//Draw position text
|
|
QRectF text_rect = painter.boundingRect(
|
|
QRectF(30, offset, 5, 10),
|
|
Qt::AlignLeft | Qt::AlignVCenter,
|
|
str);
|
|
if (!str.isEmpty())
|
|
{
|
|
drawText(painter, text_rect,
|
|
Qt::AlignLeft | Qt::AlignVCenter,
|
|
str);
|
|
bounding_rect = bounding_rect.united(text_rect);
|
|
}
|
|
//Hit rect kept even for an empty position text (as before),
|
|
//free slots are not clickable.
|
|
if (m_update_map && elmt)
|
|
m_hovered_contacts_map.insert(elmt, text_rect);
|
|
++m_drawed_contacts;
|
|
}
|
|
return bounding_rect;
|
|
}
|
|
|
|
|
|
/**
|
|
@brief CrossRefItem::fillCrossRef
|
|
Fill the content of the cross ref
|
|
@param painter painter to use.
|
|
*/
|
|
void CrossRefItem::fillCrossRef(QPainter &painter)
|
|
{
|
|
if (m_element->isFree()) return;
|
|
|
|
qreal middle_cross = m_bounding_rect.width()/2;
|
|
|
|
//Fill NO
|
|
QPointF no_top_left(0, header);
|
|
foreach(Element *elmt, NOElements())
|
|
{
|
|
QPen pen = painter.pen();
|
|
m_hovered_contact == elmt ? pen.setColor(Qt::blue) :pen.setColor(Qt::black);
|
|
painter.setPen(pen);
|
|
|
|
// Collect terminal names for NO column.
|
|
// Power: all terminals sorted numerically.
|
|
// SW with typed terminals: NO+Common pair.
|
|
// Others: first 2 named terminals.
|
|
const bool is_power_no =
|
|
elmt->kindInformations()["type"].toString() == "power";
|
|
const bool is_sw_no =
|
|
elmt->kindInformations()["state"].toString() == "SW";
|
|
QStringList tnames;
|
|
if (is_sw_no) {
|
|
bool has_typed = false;
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
if (t->terminalType() == TerminalData::No ||
|
|
t->terminalType() == TerminalData::Nc ||
|
|
t->terminalType() == TerminalData::Common) {
|
|
has_typed = true; break;
|
|
}
|
|
}
|
|
if (has_typed) {
|
|
QString no_name, common_name;
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
if (!t->name().isEmpty()) {
|
|
if (t->terminalType() == TerminalData::No) no_name = t->name();
|
|
else if (t->terminalType() == TerminalData::Common) common_name = t->name();
|
|
}
|
|
}
|
|
tnames << no_name << common_name;
|
|
} else {
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
const QString tn = t->name();
|
|
if (!tn.isEmpty()) { tnames << tn; if (tnames.size() >= 2) break; }
|
|
}
|
|
}
|
|
} else {
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
const QString tn = t->name();
|
|
if (!tn.isEmpty()) {
|
|
tnames << tn;
|
|
if (!is_power_no && tnames.size() >= 2) break;
|
|
}
|
|
}
|
|
}
|
|
QString terminal_label;
|
|
if (!tnames.isEmpty() && m_properties.showTerminalName())
|
|
terminal_label = QStringLiteral("[") + tnames.join("-") + QStringLiteral("]");
|
|
|
|
QString str = elementPositionText(elmt, true);
|
|
if (!terminal_label.isEmpty())
|
|
str = terminal_label + QStringLiteral(" ") + str;
|
|
|
|
QRectF bounding = painter.boundingRect(
|
|
QRectF(no_top_left,
|
|
QSize(middle_cross, 1)),
|
|
Qt::AlignLeft,
|
|
str);
|
|
drawText(painter, bounding, Qt::AlignLeft, str);
|
|
|
|
if (m_update_map) {
|
|
QString pos_str = elementPositionText(elmt, true);
|
|
QRectF pos_rect = painter.boundingRect(bounding, Qt::AlignRight, pos_str);
|
|
pos_rect.adjust(-2, -1, 2, 1); // extend hit area slightly
|
|
m_hovered_contacts_map.insert(elmt, pos_rect);
|
|
}
|
|
|
|
no_top_left.ry() += bounding.height();
|
|
}
|
|
|
|
//Fill NC
|
|
QPointF nc_top_left(middle_cross, header);
|
|
foreach(Element *elmt, NCElements())
|
|
{
|
|
QPen pen = painter.pen();
|
|
m_hovered_contact == elmt ? pen.setColor(Qt::blue)
|
|
:pen.setColor(Qt::black);
|
|
painter.setPen(pen);
|
|
|
|
// Collect terminal names for NC column.
|
|
// Power: all terminals sorted numerically.
|
|
// SW with typed terminals: NC+Common pair.
|
|
// Others: first 2 named terminals.
|
|
const bool is_power_nc =
|
|
elmt->kindInformations()["type"].toString() == "power";
|
|
const bool is_sw_nc =
|
|
elmt->kindInformations()["state"].toString() == "SW";
|
|
QStringList tnames_nc;
|
|
if (is_sw_nc) {
|
|
bool has_typed = false;
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
if (t->terminalType() == TerminalData::No ||
|
|
t->terminalType() == TerminalData::Nc ||
|
|
t->terminalType() == TerminalData::Common) {
|
|
has_typed = true; break;
|
|
}
|
|
}
|
|
if (has_typed) {
|
|
QString nc_name, common_name;
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
if (!t->name().isEmpty()) {
|
|
if (t->terminalType() == TerminalData::Nc) nc_name = t->name();
|
|
else if (t->terminalType() == TerminalData::Common) common_name = t->name();
|
|
}
|
|
}
|
|
tnames_nc << nc_name << common_name;
|
|
} else {
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
const QString tn = t->name();
|
|
if (!tn.isEmpty()) { tnames_nc << tn; if (tnames_nc.size() >= 2) break; }
|
|
}
|
|
}
|
|
} else {
|
|
for (Terminal *t : elmt->terminals()) {
|
|
if (!t) continue;
|
|
const QString tn = t->name();
|
|
if (!tn.isEmpty()) {
|
|
tnames_nc << tn;
|
|
if (!is_power_nc && tnames_nc.size() >= 2) break;
|
|
}
|
|
}
|
|
}
|
|
QString terminal_label;
|
|
if (!tnames_nc.isEmpty() && m_properties.showTerminalName())
|
|
terminal_label = QStringLiteral("[") + tnames_nc.join("-") + QStringLiteral("]");
|
|
|
|
QString str = elementPositionText(elmt, true);
|
|
if (!terminal_label.isEmpty())
|
|
str = terminal_label + QStringLiteral(" ") + str;
|
|
|
|
QRectF bounding = painter.boundingRect(
|
|
QRectF(nc_top_left,
|
|
QSize(middle_cross, 1)),
|
|
Qt::AlignRight,
|
|
str);
|
|
drawText(painter, bounding, Qt::AlignRight, str);
|
|
|
|
if (m_update_map) {
|
|
QString pos_str = elementPositionText(elmt, true);
|
|
QRectF pos_rect = painter.boundingRect(bounding, Qt::AlignRight, pos_str);
|
|
pos_rect.adjust(-2, -1, 2, 1); // extend hit area slightly
|
|
m_hovered_contacts_map.insert(elmt, pos_rect);
|
|
}
|
|
|
|
nc_top_left.ry() += bounding.height();
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::AddExtraInfo
|
|
Add the comment info of the parent item if needed.
|
|
@param painter painter to use for draw the text
|
|
@param type type of Info do be draw e.g. comment, location.
|
|
*/
|
|
void CrossRefItem::AddExtraInfo(QPainter &painter, const QString& type)
|
|
{
|
|
QString text = autonum::AssignVariables::formulaToLabel(
|
|
m_element->elementInformations()[type].toString(),
|
|
m_element->rSequenceStruct(),
|
|
m_element->diagram(),
|
|
m_element);
|
|
bool must_show = m_element -> elementInformations().keyMustShow(type);
|
|
|
|
if (!text.isEmpty() && must_show)
|
|
{
|
|
painter.save();
|
|
painter.setFont(QETApp::diagramTextsFont(6));
|
|
|
|
QRectF r, text_bounding;
|
|
qreal center = boundingRect().center().x();
|
|
qreal width = boundingRect().width() > 70
|
|
? boundingRect().width()/2
|
|
: 35;
|
|
|
|
r = QRectF(QPointF(center - width,
|
|
boundingRect().bottom()),
|
|
QPointF(center + width,
|
|
boundingRect().bottom() + 1));
|
|
|
|
text_bounding = painter.boundingRect(
|
|
r,
|
|
Qt::TextWordWrap | Qt::AlignHCenter,
|
|
text);
|
|
drawText(painter, text_bounding,
|
|
Qt::TextWordWrap | Qt::AlignHCenter,
|
|
text);
|
|
|
|
text_bounding.adjust(-1,0,1,0); //adjust only for better visual
|
|
|
|
m_shape_path.addRect(text_bounding);
|
|
prepareGeometryChange();
|
|
m_bounding_rect = m_bounding_rect.united(text_bounding);
|
|
if (type == "comment") painter.drawRoundedRect(text_bounding,
|
|
2,
|
|
2);
|
|
painter.restore();
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::drawText
|
|
Draws text in rect with painter, like QPainter::drawText. While the
|
|
geometry is built (m_update_map), also stores the rect really covered
|
|
by the text, so stackAtBottom() can tell texts drawn over each other.
|
|
*/
|
|
void CrossRefItem::drawText(QPainter &painter, const QRectF &rect,
|
|
int flags, const QString &text)
|
|
{
|
|
QRectF drawn;
|
|
painter.drawText(rect, flags, text, &drawn);
|
|
if (m_update_map && !text.isEmpty())
|
|
m_text_rects << painter.transform().mapRect(drawn);
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::NOElements
|
|
@return The linked elements of m_element which are open or switch contact.
|
|
If linked element is a power contact,
|
|
xref property is set to not show power contact
|
|
and this cross item must be drawn as a cross,
|
|
the element is not appended in the list.
|
|
*/
|
|
QList<Element *> CrossRefItem::NOElements() const
|
|
{
|
|
QList<Element *> no_list;
|
|
|
|
foreach (Element *elmt, m_element->linkedElements())
|
|
{
|
|
//We continue if element is a power contact and xref property
|
|
//is set to not show power contact
|
|
if (m_properties.displayHas() == XRefProperties::Cross &&
|
|
!m_properties.showPowerContact() &&
|
|
elmt -> kindInformations()["type"].toString() == "power")
|
|
continue;
|
|
|
|
QString state = elmt->kindInformations()["state"].toString();
|
|
|
|
if (state == "NO" || state == "SW")
|
|
{
|
|
no_list.append(elmt);
|
|
}
|
|
}
|
|
|
|
return no_list;
|
|
}
|
|
|
|
/**
|
|
@brief CrossRefItem::NCElements
|
|
@return The linked elements of m_element which are close
|
|
or switch contact
|
|
If linked element is a power contact,
|
|
xref property is set to not show power contact
|
|
and this cross item must be drawn as a cross,
|
|
the element is not appended in the list.
|
|
*/
|
|
QList<Element *> CrossRefItem::NCElements() const
|
|
{
|
|
QList<Element *> nc_list;
|
|
|
|
foreach (Element *elmt, m_element->linkedElements())
|
|
{
|
|
//We continue if element is a power contact and xref property
|
|
//is set to not show power contact
|
|
if (m_properties.displayHas() == XRefProperties::Cross &&
|
|
!m_properties.showPowerContact() &&
|
|
elmt -> kindInformations()["type"].toString() == "power")
|
|
continue;
|
|
|
|
QString state = elmt->kindInformations()["state"].toString();
|
|
|
|
if (state == "NC" || state == "SW")
|
|
{
|
|
nc_list.append(elmt);
|
|
}
|
|
}
|
|
|
|
return nc_list;
|
|
}
|
|
|
|
/**
|
|
* @brief CrossRefItem::drawAsPlcTable
|
|
* Draw the PLC IO table for PLC master elements.
|
|
* The table shows columns: Type | Address | Function | Comment | CrossRef
|
|
* Columns and rows are configurable via PlcMasterData.
|
|
* @param painter painter to use
|
|
*/
|
|
void CrossRefItem::drawAsPlcTable(QPainter &painter)
|
|
{
|
|
// Get PLC data from the element
|
|
ElementData ed = m_element->elementData();
|
|
if (ed.m_master_type != ElementData::PLC)
|
|
return;
|
|
|
|
ElementData::PlcMasterData plc_data = ed.plcMasterData();
|
|
if (plc_data.ios.isEmpty())
|
|
return;
|
|
|
|
// Define column indices
|
|
const int COL_TYPE = 0;
|
|
const int COL_ADDRESS = 1;
|
|
const int COL_FUNCTION = 2;
|
|
const int COL_COMMENT = 3;
|
|
const int COL_CROSSREF = 4;
|
|
const int COL_COUNT = 5;
|
|
|
|
// Column headers (French)
|
|
QMap<int, QString> headers;
|
|
headers[COL_TYPE] = QObject::tr("Type");
|
|
headers[COL_ADDRESS] = QObject::tr("Adresse");
|
|
headers[COL_FUNCTION] = QObject::tr("Fonction");
|
|
headers[COL_COMMENT] = QObject::tr("Commentaire");
|
|
headers[COL_CROSSREF] = QObject::tr("Réf. croisée");
|
|
|
|
// Build visible columns (must match Element::drawPlcTable logic)
|
|
QList<int> visible_cols;
|
|
if (!plc_data.columnOrder.isEmpty()) {
|
|
for (int logical : plc_data.columnOrder) {
|
|
if (logical >= 0 && logical < COL_COUNT
|
|
&& plc_data.colVisible.value(logical, true)
|
|
&& !visible_cols.contains(logical))
|
|
visible_cols.append(logical);
|
|
}
|
|
for (int i = 0; i < COL_COUNT; ++i) {
|
|
if (plc_data.colVisible.value(i, true) && !visible_cols.contains(i))
|
|
visible_cols.append(i);
|
|
}
|
|
} else {
|
|
for (int i = 0; i < COL_COUNT; ++i) {
|
|
if (plc_data.colVisible.value(i, true))
|
|
visible_cols.append(i);
|
|
}
|
|
}
|
|
if (visible_cols.isEmpty())
|
|
return;
|
|
|
|
// Default column widths if not set (in scene units, roughly mm * 2.835)
|
|
QMap<int, qreal> col_widths;
|
|
for (int col : visible_cols) {
|
|
if (plc_data.colWidths.contains(col) && plc_data.colWidths[col] > 0)
|
|
col_widths[col] = plc_data.colWidths[col];
|
|
else {
|
|
switch (col) {
|
|
case COL_TYPE: col_widths[col] = 35; break;
|
|
case COL_ADDRESS: col_widths[col] = 25; break;
|
|
case COL_FUNCTION: col_widths[col] = 50; break;
|
|
case COL_COMMENT: col_widths[col] = 40; break;
|
|
case COL_CROSSREF: col_widths[col] = 30; break;
|
|
default: col_widths[col] = 30; break;
|
|
}
|
|
}
|
|
}
|
|
|
|
qreal row_h = plc_data.rowHeight > 0 ? plc_data.rowHeight : 8.0;
|
|
qreal header_h = plc_data.showHeaders ? (row_h + 2.0) : 0;
|
|
|
|
// Calculate total width
|
|
qreal total_width = 0;
|
|
for (int col : visible_cols)
|
|
total_width += col_widths[col];
|
|
|
|
// Calculate total height: header + all IO rows
|
|
int total_ios = plc_data.ios.size();
|
|
|
|
// Collect active break positions (sorted)
|
|
QList<int> breaks;
|
|
for (int bp : plc_data.breakPositions) {
|
|
if (bp > 0 && bp < total_ios && !breaks.contains(bp))
|
|
breaks.append(bp);
|
|
}
|
|
std::sort(breaks.begin(), breaks.end());
|
|
|
|
// Build block boundaries
|
|
QList<int> block_starts;
|
|
block_starts.append(0);
|
|
for (int bp : breaks)
|
|
block_starts.append(bp);
|
|
|
|
qreal total_height;
|
|
int block_count = block_starts.size();
|
|
qreal block_total_width = total_width; // width of one block, before multi-block scaling
|
|
|
|
if (block_count > 1) {
|
|
int max_rows = 0;
|
|
for (int b = 0; b < block_count; ++b) {
|
|
int start = block_starts.at(b);
|
|
int end = (b + 1 < block_starts.size()) ? block_starts.at(b + 1) : total_ios;
|
|
max_rows = qMax(max_rows, end - start);
|
|
}
|
|
total_height = header_h + max_rows * row_h;
|
|
total_width = total_width * block_count + (block_count - 1) * 3;
|
|
} else {
|
|
total_height = header_h + total_ios * row_h;
|
|
}
|
|
|
|
// Draw background rectangle
|
|
QRectF bg_rect(0, 0, total_width, total_height);
|
|
painter.fillRect(bg_rect, Qt::white);
|
|
QPen border_pen(Qt::black, 0.5);
|
|
painter.setPen(border_pen);
|
|
painter.drawRect(bg_rect);
|
|
|
|
// Draw header row
|
|
|
|
for (int block = 0; block < block_count; ++block) {
|
|
qreal block_x = block * (block_total_width + 3);
|
|
qreal cx = block_x;
|
|
|
|
// Draw column headers
|
|
QFont header_font = painter.font();
|
|
header_font.setBold(true);
|
|
painter.setFont(header_font);
|
|
|
|
for (int col : visible_cols) {
|
|
QRectF header_rect(cx, 0, col_widths[col], header_h);
|
|
painter.fillRect(header_rect, QColor(220, 220, 220));
|
|
painter.drawRect(header_rect);
|
|
painter.drawText(header_rect, Qt::AlignCenter, headers[col]);
|
|
cx += col_widths[col];
|
|
}
|
|
|
|
painter.setFont(painter.font());
|
|
|
|
// Draw IO rows for this block
|
|
int start_idx = block_starts.at(block);
|
|
int end_idx = (block + 1 < block_starts.size()) ? block_starts.at(block + 1) : total_ios;
|
|
|
|
for (int row = 0; row < (end_idx - start_idx); ++row) {
|
|
int io_idx = start_idx + row;
|
|
const ElementData::PlcIO &io = plc_data.ios.at(io_idx);
|
|
|
|
qreal ry = header_h + row * row_h;
|
|
cx = block_x;
|
|
|
|
// Get linked slave element for this IO (via group index)
|
|
QString cross_ref_text = io.crossRef;
|
|
for (Element *slave : m_element->linkedElements()) {
|
|
if (m_element->groupIndexForElement(slave) == io_idx) {
|
|
cross_ref_text = elementPositionText(slave);
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (int col : visible_cols) {
|
|
QRectF cell_rect(cx, ry, col_widths[col], row_h);
|
|
painter.drawRect(cell_rect);
|
|
|
|
QString cell_text;
|
|
switch (col) {
|
|
case COL_TYPE:
|
|
cell_text = ElementData::translatedPlcIOType(io.type);
|
|
break;
|
|
case COL_ADDRESS:
|
|
cell_text = io.address;
|
|
break;
|
|
case COL_FUNCTION:
|
|
cell_text = io.functionText;
|
|
break;
|
|
case COL_COMMENT:
|
|
cell_text = io.comment;
|
|
break;
|
|
case COL_CROSSREF:
|
|
cell_text = cross_ref_text;
|
|
break;
|
|
}
|
|
|
|
QRectF text_rect = cell_rect.adjusted(1, 0, -1, 0);
|
|
painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, cell_text);
|
|
|
|
// If function text doesn't fit, try to show as much as possible
|
|
if (col == COL_FUNCTION && !io.functionText.isEmpty()) {
|
|
QFontMetrics fm(painter.font());
|
|
QString elided = fm.elidedText(io.functionText, Qt::ElideRight, text_rect.width());
|
|
// Clear and redraw with elided text
|
|
painter.save();
|
|
painter.setBrush(Qt::white);
|
|
painter.drawRect(text_rect);
|
|
painter.restore();
|
|
painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, elided);
|
|
}
|
|
|
|
// Store hover map for cross-reference cells
|
|
if (col == COL_CROSSREF && m_update_map && !cross_ref_text.isEmpty()) {
|
|
for (Element *slave : m_element->linkedElements()) {
|
|
if (m_element->groupIndexForElement(slave) == io_idx) {
|
|
m_hovered_contacts_map.insert(slave, cell_rect);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
cx += col_widths[col];
|
|
}
|
|
}
|
|
}
|
|
|
|
prepareGeometryChange();
|
|
m_bounding_rect = bg_rect;
|
|
m_shape_path.addRect(bg_rect);
|
|
}
|