Compare commits

..

35 Commits

Author SHA1 Message Date
Laurent Trinques 6d09041dce Merge pull request #831 from ispyisail/feature/advisory-slave-limit
Make the slave limit advisory rather than a refusal
2026-09-10 14:50:56 +02:00
ispyisail 1a12d440b2 Merge pull request #834 from ispyisail/fix/qlabel-pixmap-deprecation
Use QLabel::pixmap()'s non-deprecated by-value form on Qt5
2026-09-10 16:51:55 +12:00
ispyisail 85f46f2b48 Use QLabel::pixmap()'s non-deprecated by-value form on Qt5
#824 read the pixmap through the pointer overload, which Qt 5.15
deprecates, so the fix it introduced compiled with two deprecation
warnings of its own. Qt 5.15 offers the by-value form behind
Qt::ReturnByValue, so both branches can take the same overload and the
difference reduces to the argument.

Equivalent: the pointer overload returns nullptr when no pixmap is set,
which the old expression turned into a null QPixmap; pixmap(
Qt::ReturnByValue) returns a null QPixmap directly. It also drops the
null check, so the Qt5 branch is now a single expression.

Verified both arms of the #if, since a preprocessor-branched change is
only half tested otherwise:

 - Qt 5.15.18: deprecation warnings for this file 2 -> 0, builds clean,
   binary runs
 - Qt 6.10.2: builds clean, 488/488, links
 - 22 example projects load and export with no crash or hang

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:50:49 +12:00
Laurent Trinques 9637420756 Update Links to Doygen documentation and qch file 2026-09-10 05:27:57 +02:00
Laurent Trinques 6c68c461b1 Set Doxyfile version to 0.200.1 2026-09-10 05:24:31 +02:00
Laurent Trinques c830101ba6 Update CMakeLists.txt set version to 0.200.1 2026-09-10 02:33:31 +02:00
Laurent Trinques a1c0907811 Set QT_VERSION_MAJOR to 6
https://github.com/qelectrotech/qelectrotech-source-mirror/pull/824#issuecomment-5610090520
2026-09-10 02:30:53 +02:00
ispyisail ffd829bb69 Make the slave limit advisory rather than a refusal
max_slaves records how many contacts a part is expected to carry. It was
enforced as a rule the drawing had to obey, which obstructs the way both
@scorpio810 and @IBSYSLevi described working in #819: draw the schematic
first, choose the physical hardware afterwards. A limit that refuses the
link forces the hardware decision up front, which is exactly what they
said gets in the way.

Two changes, both in the UI rather than in isFull(), which stays the
query it always was:

 - MasterPropertiesWidget::on_link_button_clicked() now says the limit
   is reached and asks whether to link anyway, defaulting to yes,
   instead of refusing outright.

 - LinkSingleElementWidget no longer removes a full master from the
   candidate list. That was the worse half: a master at its limit simply
   was not there, indistinguishable from one that does not exist, with
   nothing to say why. It now stays selectable and the user decides.

PLC masters are deliberately left alone. Their limit is the number of
declared IO slots, which is structural rather than advisory -- a link
past it would have no IO index to map to -- and PlcLinkWidget already
tells the user when it hides one, via m_hidden_masters_label.

Only coils that opt into a limit are affected: max_slaves defaults to
-1, and no project in examples/ sets it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 11:54:52 +12:00
Laurent Trinques 957bbe5edb macOS: drop obsolete lang1/ copy step
lang1/ was a leftover from the pre-Qt6 translation pipeline.
Qt6/CMake now produces all .qm files directly into lang/, which
is already copied above, making this step dead code.
2026-09-09 16:02:02 +02:00
Laurent Trinques ce0ba4a681 Merge pull request #829 from jp2images/fix-macos-bundle-identifier
Set a bundle identifier so macOS file dialogs work
2026-09-09 15:57:21 +02:00
Laurent Trinques 8295083f05 misc: align macOS bundle identifier with app's settings domain
CFBundleIdentifier was "org.qelectrotech", but Qt derives
"org.qelectrotech.QElectroTech" from setOrganizationDomain()
and setApplicationName() for the app's own preferences file
(~/Library/Preferences/org.qelectrotech.QElectroTech.plist).

Align the two so the shipped bundle and the CMake target (see
CMakeLists.txt MACOSX_BUNDLE_GUI_IDENTIFIER) use the same
identifier regardless of build path.

Note: this changes the bundle's LaunchServices identity, so
users may need to redo "Open With QElectroTech" file
associations once after updating.
2026-09-09 15:54:28 +02:00
Jeff Patterson a2441a6f81 Set a bundle identifier so macOS file dialogs work
CMakeLists.txt marks the macOS target as MACOSX_BUNDLE but never sets
MACOSX_BUNDLE_GUI_IDENTIFIER, so CMake's default Info.plist template
substitutes an empty string for CFBundleIdentifier.

An .app with an empty identifier is never registered by LaunchServices
(`lsappinfo info` reports bundleID="" and bundle path=[NULL]). AppKit
runs the open/save panel in an XPC service keyed on the client's bundle
identifier: the service is spawned on each request but presents no
window, so QFileDialog::getOpenFileName() and getSaveFileName() return
an empty string without a panel ever appearing. In QET this means
File > Open and File > Save as silently do nothing -- openProject()
receives an empty path and returns at its `if (filepath.isEmpty())`
guard. Every macOS CMake build has been affected since the target
became a bundle.

Fill in the identifier along with the other bundle metadata CMake's
template expects. org.qelectrotech.QElectroTech is the identifier Qt
already derives from setOrganizationDomain("qelectrotech.org") and
setApplicationName("QElectroTech") for the app's own preferences file,
so the bundle now agrees with what the app writes at runtime.

Verified on macOS 27 with Qt 6.11: before the change File > Open and
File > Save as present nothing; after it both panels open normally. No
code signing step is needed -- the linker's ad-hoc signature still
reports the executable name as its identifier, and the panels work
regardless once the plist is correct.
2026-09-09 05:16:50 -05:00
Laurent Trinques c1f9af8544 Merge pull request #822 from enesgursoy6110/fix/report-link-picker
Show folio identity first in report link picker
2026-09-09 07:49:42 +02:00
Laurent Trinques 033c2f93a8 Merge pull request #827 from ispyisail/feature/per-type-contact-budget
Show used against declared capacity where a master declares contact groups
2026-09-09 07:29:39 +02:00
Laurent Trinques 83623a0fa6 Merge pull request #826 from ispyisail/feature/general-tab-contact-count
Show the NO/NC/SW contact breakdown in a master's General tab
2026-09-09 07:29:07 +02:00
Laurent Trinques c85f80bbcf Merge pull request #825 from ispyisail/fix/contact-usage-counting-v2
Count master contacts in one place, and count contacts not elements
2026-09-09 07:28:15 +02:00
ispyisail f18eda845b Merge branch 'feature/general-tab-contact-count' into feature/per-type-contact-budget
# Conflicts:
#	sources/ui/elementpropertieswidget.cpp
2026-09-09 11:47:26 +12:00
ispyisail 73e9473db4 Keep the slave total in slots, and label the breakdown as contacts
Two units were being stacked in the same block. The line above reports
max_slaves, which is a number of slots, so reporting the line below in
contacts made a coil with one 4 pole slave read "maximum 4 / used 4"
while three slots were still free.

The total goes back to counting linked elements, matching the unit of
the line above it and restoring the original behaviour of that line.

The per-type breakdown keeps the pole multiplier, because that is the
question it answers -- how many contacts an auxiliary block must
provide -- and is now prefixed "Contacts :" so the two units are not
mistaken for each other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 11:46:34 +12:00
ispyisail 7307128fc3 Merge branch 'fix/contact-usage-counting-v2' into feature/general-tab-contact-count 2026-09-09 11:45:49 +12:00
ispyisail 799ff5573f Revert the isFull() change: max_slaves counts slots, not contacts
The earlier commit changed MasterElement::isFull() to compare the
contacts in use against max_slaves. That was wrong, and this restores
the original comparison against the number of linked elements.

max_slaves is a number of slots, not of contacts:

 - it sizes the contact group table in the element editor, one row per
   slot (ElementPropertiesEditorWidget::populateSlaveGroupsTable)
 - a group must match the slave's own contact count before it can be
   chosen, so a 4 pole slave needs a group declaring 4 and occupies
   that single group (ContactGroupSelectionDialog)
 - each slave stores exactly one group index
   (Element::setGroupIndexForElement)

So a coil declaring 4 slots accepts 4 slaves, whatever their pole
count. Counting contacts made one 4 pole slave fill a 4 slot coil on
its own and refuse three further links that should have been allowed.

ContactUsage stays, and its per-type tally is still what the General
tab needs: how many contacts an auxiliary block must provide is a
different question from how many slots are occupied, and only the
former wants the pole multiplier. The header now says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 11:45:36 +12:00
ispyisail 6f2c66afef Show used against declared capacity where a master declares contact groups
Second half of #819: where a coil declares what contacts it provides, the
General tab now reports each type as used against declared rather than as
a bare count.

    NO : 3/4, NC : 1/2, inverseurs : 0/1, autres : 0/0

MasterElement::contactCapacity() sums contactCount over the element's
SlaveContactGroup list, per type, reusing the same ContactUsage tally the
used count is built on. The mapping from ElementData::SlaveState onto the
tally's own type is factored into one helper so the used count and the
declared capacity cannot classify a contact differently.

Falls back to the plain count from the previous commit when an element
declares no groups, which is every element in the standard collection
today -- nothing in the corpus declares slaveContactGroups, so this
changes no existing display.

A type used beyond what is declared reads as e.g. "1/0". That is
deliberate: it says this contact does not fit the part.

Display only. Whether a declared capacity should also feed
MasterElement::isFull() is the open question in #819 and is not touched
here.

Verified end to end against a purpose-built fixture, since no existing
element exercises this path: a coil declaring two NO groups of two, one
NC group of two and one changeover group of one parses and reports
NO=4 NC=2 SW=1 other=0 total=7, matching the declaration exactly.
tst_contactusage gains a case covering capacity summed across groups
(10 cases, all passing).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:49:16 +12:00
ispyisail c39c7414cf Show the NO/NC/SW contact breakdown in a master's General tab
Requested in #819: after drawing a schematic you need to know how many
NO, NC and changeover contacts a coil ended up using, so you can pick an
auxiliary block that satisfies it. Until now the General tab reported
only a single total, and counting the contacts by type meant counting
rows on the cross reference by hand.

Three changes to that block:

 - the used count now counts contacts rather than linked elements. The
   label already said "contacts" while the value was
   linkedElements().count(), so a slave standing for several contacts
   was under-reported. It reads MasterElement::contactUsage(), the
   same count isFull() uses.

 - a breakdown line is added below it, printed only when the master
   actually has contacts to break down.

 - a declared limit of -1 means "no limit set" rather than a real
   limit, so it is printed as such instead of showing "-1", which
   reads as a bad value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:36:25 +12:00
ispyisail c33f250910 Count master contacts in one place, and count contacts not elements
MasterElement::isFull() decided whether a coil had room left with

    connected_elements.size() >= max_slaves

which counts linked *elements*. A slave stands for as many contacts as
its "number" kind information declares, so a 4 pole contact consumed a
single contact from the coil's budget instead of four. 36 elements in
the standard collection declare a number between 2 and 4, so this is
reachable, not theoretical.

Add ContactUsage, a header-only tally holding the two rules that are
easy to get wrong:

 - a slave counts once per contact it declares, not once per element
 - a changeover is counted once, as sw, and never as one NO plus one
   NC. CrossRefItem::NOElements() and NCElements() both return
   changeovers, so a count built by adding those two lists together
   reports one changeover as two contacts.

The upcoming per-type displays (the used count in the element's General
tab, and the per-type budget on the cross reference) need exactly this
count, so it lives in one place rather than being written out three
times, and isFull() now reads it too.

The header carries no graphics dependency, so the counting rules are
unit tested on their own in tests/qttest/tst_contactusage.cpp,
following the same pattern as diagramsortkeys.h.

Verified: all 9 unit tests pass, and both rules were mutation checked
(counting elements instead of contacts fails 2 tests, counting a
changeover as both NO and NC fails 3). The 23 example projects still
load and export without crash or hang, and qet-lint reports no
regressions against its baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:08:00 +12:00
ispyisail c265f0206c Fix Qt5 build: QLabel::pixmap() returns a pointer, not a value
ClickableImageLabel::mousePressEvent() calls pixmap().isNull() and
pixmap().width(). That is the Qt6 signature; in Qt5 QLabel::pixmap()
returns const QPixmap * and the code does not compile:

  error: request for member 'isNull' in '...QLabel::pixmap()',
  which is of pointer type 'const QPixmap*'

CMakeLists.txt defaults QT_VERSION_MAJOR to 5 when it is not specified,
so a default configuration of master has not built since 6b577ee75.

Read the pixmap once into a local, guarded the way the rest of the
codebase handles this split, which also drops four repeated pixmap()
calls in the same expression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 09:52:06 +12:00
enesgursoy6110 36dd1624d2 Show folio identity first in report link picker 2026-09-08 21:02:46 +03:00
Laurent Trinques a583b3c43c Merge pull request #688 from IBSYSLevi/fix/saving-coil-master
Fix: saving coil master
2026-09-08 08:35:00 +02:00
Laurent Trinques 1738c3ad6c Merge pull request #771 from Kellermorph/spinbox-crossref
Add configurable distance between label and slave XRef
2026-09-08 08:20:33 +02:00
Laurent Trinques 82026d8f7c Merge pull request #818 from qelectrotech/revert-804-feature/terminal-potential-grouping
Revert "Feature: Terminal potential grouping"
2026-09-07 15:47:08 +02:00
Kellermorph a494a8fd6c Add 'Text field' option for slave cross-reference positioning 2026-09-05 18:55:14 +02:00
Kellermorph 4c038a868b fix 2026-08-23 12:51:51 +02:00
Kellermorph 6208c7e5df Add configurable distance between label and slave XRef 2026-08-22 12:03:35 +02:00
Levi Jetzer fb0649ceee Fixed incorrect OR and AND checks 2026-08-14 22:23:25 +02:00
Levi Jetzer d7c75ea5a5 Fixed saving coil master -> correction +
Added space for a better style ;)
2026-08-08 09:07:22 +02:00
Levi Jetzer b9153269a4 Fixed saving coil master -> correction
Added check to the type of master so that types which are not using "max slave elements" are saved with the value -1 (infinity).
2026-08-08 09:05:27 +02:00
Levi Jetzer eab9603d8a Fixed saving coil master
Removed "isVisible" check on max_slave_checkbox before saving because isVisible is maybe not true, when the ok button is pressed
Then the value -1 is written and so on not saved to the elements xml
2026-08-07 21:52:40 +02:00
26 changed files with 661 additions and 92 deletions
+18 -3
View File
@@ -19,7 +19,7 @@ include(cmake/hoto_update_cmake_message.cmake)
cmake_minimum_required(VERSION 3.5...4.2)
project(qelectrotech
VERSION 0.100.1
VERSION 0.200.1
DESCRIPTION "QET is a CAD/CAE editor focusing on schematics drawing features."
HOMEPAGE_URL "https://qelectrotech.org/"
LANGUAGES C CXX)
@@ -36,7 +36,7 @@ set(QET_DIR ${PROJECT_SOURCE_DIR})
# includes below, so every subdirectory and every FetchContent dependency
# sees a consistent, already-defined value.
if(NOT DEFINED QT_VERSION_MAJOR)
set(QT_VERSION_MAJOR 5)
set(QT_VERSION_MAJOR 6)
endif()
# Some third-party CMake projects we pull in via FetchContent (e.g.
@@ -163,7 +163,22 @@ else()
endif()
if(APPLE)
set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE TRUE)
# CFBundleIdentifier must not be empty. CMake's default Info.plist
# template fills it from MACOSX_BUNDLE_GUI_IDENTIFIER; with that unset
# the bundle ships an empty identifier, LaunchServices never registers
# the .app, and AppKit's open/save panel service (which is keyed on the
# client's bundle id) silently presents nothing -- every
# QFileDialog::getOpenFileName()/getSaveFileName() call returns an empty
# string without a panel ever appearing, so File > Open and File > Save
# as do nothing at all.
set_target_properties(${PROJECT_NAME} PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_GUI_IDENTIFIER "org.qelectrotech.QElectroTech"
MACOSX_BUNDLE_BUNDLE_NAME "QElectroTech"
MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
MACOSX_BUNDLE_COPYRIGHT "Copyright 2006-2026 The QElectroTech Team"
)
endif()
# The default build only compiles the tracked .ts files to .qm (lrelease).
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = QElectroTech
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = " 0.100.0-dev"
PROJECT_NUMBER = " 0.200.1-dev"
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+2 -2
View File
@@ -48,8 +48,8 @@ Here are the technical choices made for the software development:
* Coding language: [C++](https://en.wikipedia.org/wiki/C%2B%2B)
* GUI translations: [Qt Linguist](http://doc.qt.io/qt-5/qtlinguist-index.html)
* Version control: [GIT](https://github.com/qelectrotech/qelectrotech-source-mirror.git)
* Doxygen documentation :[Doxygen](https://qelectrotech.github.io/qelectrotech-source-mirror/)
* QtCreator qch doxygen :[QElectroTech.qch](https://github.com/qelectrotech/qelectrotech-source-mirror/blob/master/doc/QElectroTech.qch)
* Doxygen documentation :[Doxygen](https://download.qelectrotech.org/qet/doxygen/html/)
* QtCreator qch doxygen :[QElectroTech.qch](https://download.qelectrotech.org/qet/doxygen/QElectroTech.qch)
* File format for projects, elements and titleblocks: [XML](http://www.w3schools.com/xml/xml_whatis.asp)
* Main development platform: [GNU/Linux](http://getgnulinux.org/en/linux/)
* Targeted platforms: Windows, GNU/Linux, Mac OS X, BSDs
+1 -1
View File
@@ -42,7 +42,7 @@
<key>CFBundleIconFile</key>
<string>qelectrotech</string>
<key>CFBundleIdentifier</key>
<string>org.qelectrotech</string>
<string>org.qelectrotech.QElectroTech</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
-4
View File
@@ -175,7 +175,6 @@ QET_LANG_DIR="${current_dir}/lang/"
QET_EXAMPLES_DIR="${current_dir}/examples/"
QET_FONTS_DIR="${current_dir}/fonts/"
QET_LICENSES_DIR="${current_dir}/licenses/"
LANG_DIR="${current_dir}/lang1/"
if [ -d "${QET_ELMT_DIR}" ]; then
cp -R ${QET_ELMT_DIR} $BUNDLE/Contents/Resources/elements
@@ -187,9 +186,6 @@ if [ -d "${QET_LANG_DIR}" ]; then
mkdir $BUNDLE/Contents/Resources/lang
cp ${current_dir}/lang/*.qm $BUNDLE/Contents/Resources/lang
fi
if [ -d "${LANG_DIR}" ]; then
cp ${current_dir}/lang1/*.qm $BUNDLE/Contents/Resources/lang
fi
if [ -d "${QET_EXAMPLES_DIR}" ]; then
mkdir $BUNDLE/Contents/Resources/examples
cp ${current_dir}/examples/*.qet $BUNDLE/Contents/Resources/examples
+91
View File
@@ -0,0 +1,91 @@
/*
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/>.
*/
#ifndef CONTACTUSAGE_H
#define CONTACTUSAGE_H
#include <algorithm>
/**
@brief The ContactUsage struct
How many slave contacts a master element currently uses, broken down
by contact type.
Header-only and free of any graphics dependency so that the counting
rules can be unit tested on their own. MasterElement::contactUsage()
is the thin wrapper that feeds it the linked elements.
This counts contacts, which is what tells you how many contacts an
auxiliary block must provide. It is deliberately not the count that
MasterElement::isFull() uses: a master's max_slaves is a number of
slots, and a slave fills exactly one slot however many contacts it
carries.
Two rules are easy to get wrong, and both live here so that every
caller gets them right:
- a slave stands for as many contacts as its "number" kind
information says, so a 4 pole contact counts as 4, not as 1
- a changeover contact is counted once, as sw. CrossRefItem's
NOElements() and NCElements() both return it, so adding those two
lists together would count it twice.
*/
struct ContactUsage
{
/**
Contact types a slave can declare. Mirrors
ElementData::SlaveState, which is not used directly so that this
header stays free of the element data dependencies and can be
unit tested on its own. MasterElement::contactUsage() maps
between the two.
*/
enum Type
{
NO, ///< Normally open
NC, ///< Normally closed
SW, ///< Changeover
Other ///< Neither of the above
};
int no = 0; ///< Normally open
int nc = 0; ///< Normally closed
int sw = 0; ///< Changeover
int other = 0; ///< Neither of the above
int total() const { return no + nc + sw + other; }
/**
Add one slave element to the tally.
@param type the contact type the slave declares
@param contacts how many contacts it stands for. Values below 1
are treated as 1: an element which declares no
contact count is still one contact.
*/
void addSlave(Type type, int contacts)
{
const int n = std::max(1, contacts);
switch (type)
{
case NO: no += n; break;
case NC: nc += n; break;
case SW: sw += n; break;
case Other: other += n; break;
}
}
};
#endif // CONTACTUSAGE_H
+6 -1
View File
@@ -245,7 +245,8 @@ void DynamicTextFieldEditor::fillInfoComboBox()
else {
strl = QETInformation::elementInfoKeys();
bool is_plc_slave = (type == ElementData::Slave
bool is_slave = (type == ElementData::Slave);
bool is_plc_slave = (is_slave
&& ed.m_slave_type == ElementData::PLCSlave);
if (is_plc_slave) {
@@ -274,6 +275,10 @@ void DynamicTextFieldEditor::fillInfoComboBox()
strl.removeAll(QETInformation::ELMT_PLC_T3);
strl.removeAll(QETInformation::ELMT_PLC_T4);
}
if (is_slave) {
strl.prepend(QETInformation::ELMT_XREF);
}
}
for (int i=0; i<strl.size();++i) {
@@ -371,7 +371,7 @@ void ElementPropertiesEditorWidget::on_m_buttonBox_accepted()
m_data.m_master_type = ui->m_master_type_cb->currentData().value<ElementData::MasterType>();
//If the checkbox is checked, save the number; otherwise, -1 (infinity)
if (ui->max_slaves_checkbox->isVisible() && ui->max_slaves_checkbox->isChecked()) {
if ((m_data.m_master_type == ElementData::Coil || m_data.m_master_type == ElementData::Protection || m_data.m_master_type == ElementData::Commutator) && ui->max_slaves_checkbox->isChecked()) {
m_data.m_max_slaves = ui->max_slaves_spinbox->value();
} else {
m_data.m_max_slaves = -1;
+7
View File
@@ -36,6 +36,7 @@ XRefProperties::XRefProperties()
m_master_label = "%f-%l%c";
m_slave_label = "(%f-%l%c)";
m_offset = 0;
m_slave_offset = 0;
m_xref_pos = Qt::AlignBottom;
}
@@ -56,6 +57,8 @@ void XRefProperties::toSettings(QSettings &settings,
settings.setValue(prefix % "snapto", snap);
int offset = m_offset;
settings.setValue(prefix % "offset", offset);
int slave_offset = m_slave_offset;
settings.setValue(prefix % "slave_offset", slave_offset);
QString master_label = m_master_label;
settings.setValue(prefix % "master_label", master_label);
QString slave_label = m_slave_label;
@@ -86,6 +89,7 @@ void XRefProperties::fromSettings(const QSettings &settings,
QString snap = settings.value(prefix % "snapto", "label").toString();
snap == "bottom"? m_snap_to = Bottom : m_snap_to = Label;
m_offset = settings.value(prefix % "offset", "0").toInt();
m_slave_offset = settings.value(prefix % "slave_offset", "0").toInt();
m_master_label = settings.value(prefix % "master_label", "%f-%l%c").toString();
m_slave_label = settings.value(prefix % "slave_label", "(%f-%l%c)").toString();
@@ -123,6 +127,7 @@ QDomElement XRefProperties::toXml(QDomDocument &xml_document) const
int offset = m_offset;
xml_element.setAttribute("offset", QString::number(offset));
xml_element.setAttribute("slave_offset", QString::number(m_slave_offset));
QString master_label = m_master_label;
xml_element.setAttribute("master_label", master_label);
QString slave_label = m_slave_label;
@@ -157,6 +162,7 @@ bool XRefProperties::fromXml(const QDomElement &xml_element) {
m_xref_pos = Qt::AlignBottom;
m_offset = xml_element.attribute("offset", "0").toInt();
m_slave_offset = xml_element.attribute("slave_offset", "0").toInt();
m_master_label = xml_element.attribute("master_label", "%f-%l%c");
m_slave_label = xml_element.attribute("slave_label","(%f-%l%c)");
foreach (QString key, m_prefix_keys) {
@@ -199,6 +205,7 @@ bool XRefProperties::operator ==(const XRefProperties &xrp) const{
&& m_prefix == xrp.m_prefix
&& m_master_label== xrp.m_master_label
&& m_offset == xrp.m_offset
&& m_slave_offset== xrp.m_slave_offset
&& m_xref_pos == xrp.m_xref_pos
&& m_slave_label == xrp.m_slave_label);
}
+4
View File
@@ -80,6 +80,9 @@ class XRefProperties : public PropertiesInterface
void setOffset(const int offset) {m_offset = offset;}
int offset() const {return m_offset;}
void setSlaveOffset(const int offset) {m_slave_offset = offset;}
int slaveOffset() const {return m_slave_offset;}
void setKey(QString& key) {m_key = key;}
private:
@@ -93,6 +96,7 @@ class XRefProperties : public PropertiesInterface
QString m_master_label;
QString m_slave_label;
int m_offset;
int m_slave_offset;
QString m_key;
};
@@ -1382,36 +1382,68 @@ void DynamicElementTextItem::updateXref()
!m_parent_element.data()->linkedElements().isEmpty())
{
Element *master_elmt = m_parent_element.data()->linkedElements().first();
if(master_elmt && !parentGroup() &&
(
if(master_elmt && !parentGroup())
{
XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
//Champ de texte: store xref in element informations
if(xrp.getXrefPos() == Qt::AlignHCenter)
{
if(m_text_from == DynamicElementTextItem::ElementInfo && m_info_name == "xref")
{
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
DiagramContext dc = m_parent_element->elementInformations();
if(dc.value("xref").toString() != xref_label)
{
dc.addValue("xref", xref_label);
m_parent_element->setElementInformations(dc);
}
//Set up connections for future updates
if(m_update_slave_Xref_connection.isEmpty())
{
m_update_slave_Xref_connection << connect(master_elmt, &Element::xChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::yChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::elementInfoChange, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram(), &Diagram::diagramInformationChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::projectDiagramsOrderChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::diagramRemoved, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::XRefPropertiesChanged, this, &DynamicElementTextItem::updateXref);
}
return;
}
//For label/composite text: fall through to cleanup (delete m_slave_Xref_item)
}
else if(
(m_text_from == DynamicElementTextItem::ElementInfo && m_info_name == "label") ||
(m_text_from == DynamicElementTextItem::CompositeText && m_composite_text.contains("%{label}"))
)
)
{
XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
if(!m_slave_Xref_item)
{
m_slave_Xref_item = new QGraphicsTextItem(xref_label, this);
m_slave_Xref_item->setFont(QETApp::diagramTextsFont(5));
m_slave_Xref_item->setDefaultTextColor(Qt::black);
m_slave_Xref_item->installSceneEventFilter(this);
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
m_update_slave_Xref_connection << connect(master_elmt, &Element::xChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::yChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::elementInfoChange, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram(), &Diagram::diagramInformationChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::projectDiagramsOrderChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::diagramRemoved, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::XRefPropertiesChanged, this, &DynamicElementTextItem::updateXref);
if(!m_slave_Xref_item)
{
m_slave_Xref_item = new QGraphicsTextItem(xref_label, this);
m_slave_Xref_item->setFont(QETApp::diagramTextsFont(5));
m_slave_Xref_item->setDefaultTextColor(Qt::black);
m_slave_Xref_item->installSceneEventFilter(this);
m_update_slave_Xref_connection << connect(master_elmt, &Element::xChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::yChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::elementInfoChange, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram(), &Diagram::diagramInformationChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::projectDiagramsOrderChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::diagramRemoved, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::XRefPropertiesChanged, this, &DynamicElementTextItem::updateXref);
}
else
m_slave_Xref_item->setPlainText(xref_label);
setXref_item(xrp.getXrefPos(), xrp.slaveOffset());
return;
}
else
m_slave_Xref_item->setPlainText(xref_label);
setXref_item(xrp.getXrefPos());
return;
}
}
}
@@ -1428,6 +1460,29 @@ void DynamicElementTextItem::updateXref()
delete m_slave_Xref_item;
m_slave_Xref_item = nullptr;
m_update_slave_Xref_connection.clear();
//If position changed to Champ de texte, store xref in element info
if(m_parent_element->linkType() == Element::Slave &&
!m_parent_element->linkedElements().isEmpty())
{
Element *master_elmt = m_parent_element->linkedElements().first();
if(master_elmt && diagram())
{
XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
if(xrp.getXrefPos() == Qt::AlignHCenter)
{
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
DiagramContext dc = m_parent_element->elementInformations();
if(dc.value("xref").toString() != xref_label)
{
dc.addValue("xref", xref_label);
m_parent_element->setElementInformations(dc);
}
}
}
}
}
}
@@ -1476,7 +1531,7 @@ void DynamicElementTextItem::setPlainText(const QString &text)
? nullptr : m_parent_element.data()->linkedElements().first();
if (master_elmt) {
XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
setXref_item(xrp.getXrefPos());
setXref_item(xrp.getXrefPos(), xrp.slaveOffset());
}
}
}
@@ -1488,44 +1543,44 @@ void DynamicElementTextItem::setTextWidth(qreal width)
emit textWidthChanged(width);
}
void DynamicElementTextItem::setXref_item(Qt::AlignmentFlag m_exHrefPos)
void DynamicElementTextItem::setXref_item(Qt::AlignmentFlag m_exHrefPos, int slave_offset)
{
QRectF r = boundingRect();
QPointF pos;
//QPointF pos(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top());
if (m_exHrefPos == Qt::AlignBottom)
{
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.bottom());
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.bottom() + slave_offset);
}
else if (m_exHrefPos == Qt::AlignTop)
{
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top() - m_slave_Xref_item->boundingRect().height());
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top() - m_slave_Xref_item->boundingRect().height() - slave_offset);
}
else if (m_exHrefPos == Qt::AlignLeft) //
{
pos = QPointF(r.left() - m_slave_Xref_item->boundingRect().width(),r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
pos = QPointF(r.left() - m_slave_Xref_item->boundingRect().width() - slave_offset,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
}
else if (m_exHrefPos == Qt::AlignRight) //
{
pos = QPointF(r.right() ,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
pos = QPointF(r.right() + slave_offset ,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
}
else if (m_exHrefPos == Qt::AlignBaseline) //
{
if(this->alignment() &Qt::AlignBottom)
{
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.bottom());
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.bottom() + slave_offset);
}
else if(this->alignment() &Qt::AlignTop)
{
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top() - m_slave_Xref_item->boundingRect().height());
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top() - m_slave_Xref_item->boundingRect().height() - slave_offset);
}
else if(this->alignment() &Qt::AlignLeft)
{
pos = QPointF(r.left() - m_slave_Xref_item->boundingRect().width(),r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
pos = QPointF(r.left() - m_slave_Xref_item->boundingRect().width() - slave_offset,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
}
else if(this->alignment() &Qt::AlignRight)
{
pos = QPointF(r.right() ,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
pos = QPointF(r.right() + slave_offset ,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
}
}
m_slave_Xref_item->setPos(pos);
@@ -112,7 +112,7 @@ class DynamicElementTextItem : public DiagramTextItem
void updateXref();
void setPlainText(const QString &text);
void setTextWidth(qreal width);
void setXref_item(Qt::AlignmentFlag m_exHrefPos);
void setXref_item(Qt::AlignmentFlag m_exHrefPos, int slave_offset = 0);
void setKeepVisualRotation(bool set);
bool keepVisualRotation() const;
@@ -263,7 +263,16 @@ void ElementTextItemGroup::updateAlignment()
if(m_Xref_item)
m_Xref_item->autoPos();
if(m_slave_Xref_item)
adjustSlaveXrefPos();
{
int slave_offset = 0;
Element *master_elmt = m_parent_element->linkedElements().isEmpty()
? nullptr : m_parent_element->linkedElements().first();
if (master_elmt && m_parent_element->diagram()) {
XRefProperties xrp = m_parent_element->diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
slave_offset = xrp.slaveOffset();
}
adjustSlaveXrefPos(slave_offset);
}
if(m_hold_to_bottom_of_page)
autoPos();
}
@@ -786,12 +795,46 @@ void ElementTextItemGroup::updateXref()
!m_parent_element->linkedElements().isEmpty())
{
Element *master_elmt = m_parent_element->linkedElements().first();
XRefProperties xrp = project->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
//Champ de texte: store xref in element informations
if(xrp.getXrefPos() == Qt::AlignHCenter)
{
for(DynamicElementTextItem *deti : texts())
{
if(deti->textFrom() == DynamicElementTextItem::ElementInfo && deti->infoName() == "xref")
{
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
DiagramContext dc = m_parent_element->elementInformations();
if(dc.value("xref").toString() != xref_label)
{
dc.addValue("xref", xref_label);
m_parent_element->setElementInformations(dc);
}
//Set up connections for future updates
if(m_update_slave_Xref_connection.isEmpty())
{
m_update_slave_Xref_connection << connect(master_elmt, &Element::xChanged, this, &ElementTextItemGroup::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::yChanged, this, &ElementTextItemGroup::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::elementInfoChange, this, &ElementTextItemGroup::updateXref);
m_update_slave_Xref_connection << connect(project, &QETProject::projectDiagramsOrderChanged, this, &ElementTextItemGroup::updateXref);
m_update_slave_Xref_connection << connect(project, &QETProject::diagramRemoved, this, &ElementTextItemGroup::updateXref);
m_update_slave_Xref_connection << connect(project, &QETProject::XRefPropertiesChanged, this, &ElementTextItemGroup::updateXref);
}
return;
}
}
//No "xref" text found: fall through to cleanup
}
for(DynamicElementTextItem *deti : texts())
{
if((deti->textFrom() == DynamicElementTextItem::ElementInfo && deti->infoName() == "label") ||
(deti->textFrom() == DynamicElementTextItem::CompositeText && deti->compositeText().contains("%{label")))
{
XRefProperties xrp = project->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
@@ -810,7 +853,7 @@ void ElementTextItemGroup::updateXref()
else
m_slave_Xref_item->setPlainText(xref_label);
adjustSlaveXrefPos();
adjustSlaveXrefPos(xrp.slaveOffset());
return;
}
}
@@ -830,14 +873,38 @@ void ElementTextItemGroup::updateXref()
delete m_slave_Xref_item;
m_slave_Xref_item = nullptr;
m_update_slave_Xref_connection.clear();
//If position changed to Champ de texte, store xref in element info
if(m_parent_element->linkType() == Element::Slave &&
!m_parent_element->linkedElements().isEmpty() &&
m_parent_element->diagram())
{
Element *master_elmt = m_parent_element->linkedElements().first();
if(master_elmt)
{
XRefProperties xrp = m_parent_element->diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
if(xrp.getXrefPos() == Qt::AlignHCenter)
{
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
DiagramContext dc = m_parent_element->elementInformations();
if(dc.value("xref").toString() != xref_label)
{
dc.addValue("xref", xref_label);
m_parent_element->setElementInformations(dc);
}
}
}
}
}
}
void ElementTextItemGroup::adjustSlaveXrefPos()
void ElementTextItemGroup::adjustSlaveXrefPos(int slave_offset)
{
QRectF r = boundingRect();
QPointF pos(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,
r.bottom());
r.bottom() + slave_offset);
m_slave_Xref_item->setPos(pos);
}
@@ -104,7 +104,7 @@ class ElementTextItemGroup : public QObject, public QGraphicsItemGroup
private:
void updateXref();
void adjustSlaveXrefPos();
void adjustSlaveXrefPos(int slave_offset = 0);
void autoPos();
private:
+71 -1
View File
@@ -227,6 +227,73 @@ void MasterElement::aboutDeleteXref()
}
}
/**
* @brief MasterElement::contactUsage
* Count the slave contacts currently linked to this master, by type.
* This is the single place where that count is worked out: the cross ref
* item, the properties dialog and the link widgets all read it from here,
* so they cannot disagree with each other.
* @return the per type usage
*/
namespace {
/**
Map the element data's contact type onto the tally's own, so that
the used count and the declared capacity cannot classify the same
contact type differently.
*/
ContactUsage::Type contactType(ElementData::SlaveState state)
{
switch (state)
{
case ElementData::NO: return ContactUsage::NO;
case ElementData::NC: return ContactUsage::NC;
case ElementData::SW: return ContactUsage::SW;
case ElementData::Other: break;
}
return ContactUsage::Other;
}
}
ContactUsage MasterElement::contactUsage() const
{
ContactUsage usage;
for (Element *elmt : connected_elements)
{
if (!elmt) {
continue;
}
const ElementData &data = elmt->elementData();
usage.addSlave(contactType(data.m_slave_state), data.m_contact_count);
}
return usage;
}
/**
* @brief MasterElement::contactCapacity
* The contacts this master declares it provides, by type, summed over its
* contact groups. A group stands for contactCount contacts of its type.
* Returns an empty tally when the element declares no groups, which is the
* case for every element in the standard collection today -- callers use
* that to decide whether a capacity is worth showing at all.
* @return the per type capacity
*/
ContactUsage MasterElement::contactCapacity() const
{
ContactUsage capacity;
for (const auto &group : m_data.m_slave_contact_groups) {
capacity.addSlave(contactType(group.type), group.contactCount);
}
return capacity;
}
/**
* @brief MasterElement::isFull
* @return true if the master has reached its maximum number of slaves
@@ -247,7 +314,10 @@ bool MasterElement::isFull() const
return false;
}
// Return true if current connected elements reached or exceeded the limit
// max_slaves is a number of slots, not of contacts: it sizes the
// element's contact group table, and a slave occupies exactly one
// group however many contacts that group stands for. So the slots
// in use are the linked elements, not the contacts they carry.
return connected_elements.size() >= max_slaves;
}
+3
View File
@@ -19,6 +19,7 @@
#define MASTERELEMENT_H
#include "element.h"
#include "../contactusage.h"
#include <QHash>
#include <QMetaObject>
@@ -47,6 +48,8 @@ class MasterElement : public Element
void initLink (QETProject *project) override;
QRectF XrefBoundingRect() const;
ContactUsage contactUsage() const;
ContactUsage contactCapacity() const;
bool isFull() const; // Check Slave-Limit
protected:
+4 -2
View File
@@ -194,8 +194,9 @@ QStringList QETInformation::elementInfoKeys()
ELMT_PLC_ADDRESS,
ELMT_PLC_FUNCTION,
ELMT_PLC_COMMENT,
ELMT_PLC_CROSSREF,
"exclude_from_bom" };
ELMT_PLC_CROSSREF,
ELMT_XREF,
"exclude_from_bom" };
return list;
}
@@ -316,6 +317,7 @@ QString QETInformation::translatedInfoKey(const QString &info)
else if (info == ELMT_PLC_FUNCTION) return QObject::tr("Fonction PLC");
else if (info == ELMT_PLC_COMMENT) return QObject::tr("Commentaire PLC");
else if (info == ELMT_PLC_CROSSREF) return QObject::tr("Réf. croisée PLC");
else if (info == ELMT_XREF) return QObject::tr("Réf. croisée");
else return QString();
}
+1
View File
@@ -83,6 +83,7 @@ namespace QETInformation
static QString ELMT_SUPPLIER_AUX4 = "supplier_auxiliary4";
static QString ELMT_QUANTITY_AUX4 = "quantity_auxiliary4";
static QString ELMT_UNITY_AUX4 = "unity_auxiliary4";
static QString ELMT_XREF = "xref";
/** Default information related to conductor **/
+43 -3
View File
@@ -23,6 +23,7 @@
#include "../qetgraphicsitem/dynamicelementtextitem.h"
#include "../qetgraphicsitem/element.h"
#include "../qetgraphicsitem/elementtextitemgroup.h"
#include "../qetgraphicsitem/masterelement.h"
#include "../qeticons.h"
#include "dynamicelementtextitemeditor.h"
#include "elementinfowidget.h"
@@ -380,9 +381,48 @@ QWidget *ElementPropertiesWidget::generalWidget()
description_string += QString(tr("Rotation : %1°\n")).arg(m_element.data()->rotation());
description_string += QString(tr("Dimensions : %1*%2\n")).arg(m_element -> size().width()).arg(m_element -> size().height());
description_string += QString(tr("Bornes : %1\n")).arg(m_element -> terminals().count());
if (m_element->linkType() == Element::Master){
description_string += QString(tr("Nombre maximum de contacts esclaves définis : %1\n")).arg(m_element -> elementData().m_max_slaves);
description_string += QString(tr("Nombre de contacts esclaves utilisés : %1\n")).arg(m_element ->linkedElements().count());
if (m_element->linkType() == Element::Master)
{
//The declared limit is optional: -1 means the element sets no
//limit at all, which is worth saying rather than printing "-1".
const int max_slaves = m_element->elementData().m_max_slaves;
description_string += max_slaves == -1
? QString(tr("Nombre maximum de contacts esclaves définis : non défini\n"))
: QString(tr("Nombre maximum de contacts esclaves définis : %1\n")).arg(max_slaves);
//Left as a count of linked elements: the line above is a number
//of slots, and a slave fills one slot however many contacts it
//carries, so the two stay in the same unit.
description_string += QString(tr("Nombre de contacts esclaves utilisés : %1\n")).arg(m_element->linkedElements().count());
//The breakdown below is in contacts, not slots: it answers how
//many contacts an auxiliary block must provide.
const MasterElement *master =
static_cast<const MasterElement *>(m_element.data());
const ContactUsage usage = master->contactUsage();
const ContactUsage capacity = master->contactCapacity();
if (capacity.total() > 0)
{
//The element declares contact groups, so it can say not
//only what has been used but what it has to offer. A type
//used beyond what is declared shows as e.g. "1/0", which
//is the point: it says this contact does not fit the part.
description_string += QString(tr(" Contacts : NO : %1/%2, NC : %3/%4, inverseurs : %5/%6, autres : %7/%8\n"))
.arg(usage.no).arg(capacity.no)
.arg(usage.nc).arg(capacity.nc)
.arg(usage.sw).arg(capacity.sw)
.arg(usage.other).arg(capacity.other);
}
else if (usage.total() > 0)
{
//No declared groups, so a plain count of what is in use.
description_string += QString(tr(" Contacts : NO : %1, NC : %2, inverseurs : %3, autres : %4\n"))
.arg(usage.no)
.arg(usage.nc)
.arg(usage.sw)
.arg(usage.other);
}
}
description_string += QString(tr("Emplacement : %1\n")).arg(m_element.data()->location().toString());
+14 -4
View File
@@ -65,7 +65,17 @@ ClickableImageLabel::ClickableImageLabel(const QImage &sourceImage, QWidget *par
*/
void ClickableImageLabel::mousePressEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton || pixmap().isNull())
// QLabel::pixmap() returns a pointer in Qt5 and a value in Qt6.
// Qt 5.15 offers the by-value form behind Qt::ReturnByValue; the
// pointer overload is deprecated there, so take the by-value one
// on both and the difference reduces to the argument.
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
const QPixmap label_pixmap = pixmap(Qt::ReturnByValue);
#else
const QPixmap label_pixmap = pixmap();
#endif
if (event->button() != Qt::LeftButton || label_pixmap.isNull())
return;
// The label may be larger than its pixmap (layout stretching); the
@@ -73,9 +83,9 @@ void ClickableImageLabel::mousePressEvent(QMouseEvent *event)
// so the click has to be re-based against the pixmap's own rect
// within the label, not the label's own top-left.
const QRect pixmapRect(
(width() - pixmap().width()) / 2,
(height() - pixmap().height()) / 2,
pixmap().width(), pixmap().height());
(width() - label_pixmap.width()) / 2,
(height() - label_pixmap.height()) / 2,
label_pixmap.width(), label_pixmap.height());
if (!pixmapRect.contains(event->pos()))
return;
+16 -8
View File
@@ -17,7 +17,6 @@
*/
#include "linksingleelementwidget.h"
#include "contactgroupselectiondialog.h"
#include "../qetgraphicsitem/masterelement.h"
#include "../qetgraphicsitem/conductor.h"
#include "../diagram.h"
#include "../diagramposition.h"
@@ -353,8 +352,16 @@ void LinkSingleElementWidget::buildTree()
QSettings settings;
QVariant v = settings.value(QStringLiteral("link-element-widget/report-state"));
if(!v.isNull())
ui->m_tree_widget->header()->restoreState(v.toByteArray());
auto *header = ui->m_tree_widget->header();
if (v.isNull() || !header->restoreState(v.toByteArray()))
{
// Keep logical column IDs stable for saved layouts, but show the
// folio identity first even when the candidate has no conductor.
for (int column = 5; column < 8; ++column)
header->moveSection(header->visualIndex(column), column - 5);
ui->m_tree_widget->resizeColumnToContents(5);
ui->m_tree_widget->resizeColumnToContents(6);
}
}
setUpCompleter();
@@ -410,11 +417,12 @@ QVector <QPointer<Element>> LinkSingleElementWidget::availableElements()
continue;
}
// If the master is full, we'll remove it from the list!
MasterElement *master = static_cast<MasterElement*>(elmt);
if (master->isFull()) {
elmt_vector.removeAt(i);
}
// A master at its declared limit stays in the list. Removing
// it made a full master indistinguishable from one that does
// not exist: the candidate simply was not there, with nothing
// to say why. The limit is advisory -- see the prompt in
// MasterPropertiesWidget::on_link_button_clicked() -- so the
// user decides, rather than the list deciding for them.
}
}
return elmt_vector;
+15 -6
View File
@@ -307,15 +307,24 @@ void MasterPropertiesWidget::on_link_button_clicked()
int max_slaves = max_slaves_variant.toInt();
int current_slaves = ui->m_link_tree_widget->topLevelItemCount();
// If a limit is set and reached
// If a limit is set and reached, say so but let the user decide.
// The limit records how many contacts the part is expected to
// carry; it is not a rule the drawing has to obey, and refusing
// the link obstructs drawing a schematic before the hardware has
// been chosen.
if (max_slaves != -1 && current_slaves >= max_slaves) {
// Show a message box with the actual window as parent to ensure it's on top
QMessageBox::warning(this->window(),
tr("Nombre maximal d'esclaves atteint."),
tr("Cet élément maître ne peut plus accepter aucun nouveau contact esclave, la limite fixée a été atteinte (Limite: %1).").arg(max_slaves));
return;
const auto answer = QMessageBox::warning(
this->window(),
tr("Nombre maximal d'esclaves atteint."),
tr("La limite fixée pour cet élément maître est atteinte (Limite: %1).\n\n"
"Voulez-vous tout de même lier ce contact esclave ?").arg(max_slaves),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes);
if (answer != QMessageBox::Yes) {
return;
}
}
}
+7
View File
@@ -111,6 +111,7 @@ void XRefPropertiesWidget::buildUi()
ui -> m_xrefpos_cb -> addItem(tr("Left"),"left");
ui -> m_xrefpos_cb -> addItem(tr("Right"),"right");
ui -> m_xrefpos_cb -> addItem(tr("Text alignment"),"alignment");
ui -> m_xrefpos_cb -> addItem(tr("Champ de texte"),"text_field");
m_previous_type_index = ui -> m_type_cb -> currentIndex();
}
@@ -139,6 +140,7 @@ void XRefPropertiesWidget::saveProperties(int index) {
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "left") xrp.setXrefPos(Qt::AlignLeft);
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "right") xrp.setXrefPos(Qt::AlignRight);
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "alignment") xrp.setXrefPos(Qt::AlignBaseline);
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "text_field") xrp.setXrefPos(Qt::AlignHCenter);
xrp.setShowPowerContac(ui->m_show_power_cb->isChecked());
xrp.setShowTerminalName(ui->m_show_terminal_name_cb->isChecked());
xrp.setPrefix("power", ui->m_power_prefix_le->text());
@@ -147,6 +149,7 @@ void XRefPropertiesWidget::saveProperties(int index) {
xrp.setMasterLabel(ui->m_master_le->text());
xrp.setSlaveLabel(ui->m_slave_le->text());
xrp.setOffset(ui->m_offset_sb->value());
xrp.setSlaveOffset(ui->m_slave_offset_sb->value());
m_properties.insert(type, xrp);
}
@@ -177,6 +180,9 @@ void XRefPropertiesWidget::updateDisplay()
int offset = xrp.offset();
ui->m_offset_sb->setValue(offset);
int slave_offset = xrp.slaveOffset();
ui->m_slave_offset_sb->setValue(slave_offset);
if (xrp.snapTo() == XRefProperties::Bottom){
ui->m_snap_to_cb->setCurrentIndex(ui->m_snap_to_cb->findData("bottom"));
ui->m_offset_sb->setEnabled(true);
@@ -191,6 +197,7 @@ void XRefPropertiesWidget::updateDisplay()
else if(xrp.getXrefPos() == Qt::AlignRight) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("right"));
else if(xrp.getXrefPos() == Qt::AlignBaseline) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("alignment"));
else if(xrp.getXrefPos() == Qt::AlignBottom) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("bottom"));
else if(xrp.getXrefPos() == Qt::AlignHCenter) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("text_field"));
ui->m_show_power_cb->setChecked(xrp.showPowerContact());
ui->m_show_terminal_name_cb->setChecked(xrp.showTerminalName());
ui->m_power_prefix_le-> setText(xrp.prefix("power"));
+50 -14
View File
@@ -90,20 +90,56 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>XRef slave position</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="m_xrefpos_cb"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>XRef slave position</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="m_xrefpos_cb"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label_11">
<property name="text">
<string>Distance label - slave :</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="m_slave_offset_sb">
<property name="toolTip">
<string>Distance in pixels between the label and the slave cross reference</string>
</property>
<property name="suffix">
<string notr="true">px</string>
</property>
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
</property>
<property name="minimum">
<number>-50</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
+6
View File
@@ -85,3 +85,9 @@ add_test(NAME tst_diagramsortkeys COMMAND tst_diagramsortkeys)
target_include_directories(tst_diagramsortkeys PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_diagramsortkeys PRIVATE Qt::Test)
# contactusage.h is a header-only helper holding the contact counting
# rules, so this test builds independently of the rest of the QET sources.
add_executable(tst_contactusage tst_contactusage.cpp)
add_test(NAME tst_contactusage COMMAND tst_contactusage)
target_include_directories(tst_contactusage PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_contactusage PRIVATE Qt::Test)
+137
View File
@@ -0,0 +1,137 @@
/*
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 <QtTest>
#include "contactusage.h"
class tst_contactusage : public QObject
{
Q_OBJECT
private slots:
// An empty master uses nothing.
void emptyUsesNothing()
{
ContactUsage usage;
QCOMPARE(usage.no, 0);
QCOMPARE(usage.nc, 0);
QCOMPARE(usage.sw, 0);
QCOMPARE(usage.other, 0);
QCOMPARE(usage.total(), 0);
}
// Each type accumulates into its own field only.
void countsEachTypeSeparately()
{
ContactUsage usage;
usage.addSlave(ContactUsage::NO, 1);
usage.addSlave(ContactUsage::NO, 1);
usage.addSlave(ContactUsage::NC, 1);
usage.addSlave(ContactUsage::SW, 1);
usage.addSlave(ContactUsage::Other, 1);
QCOMPARE(usage.no, 2);
QCOMPARE(usage.nc, 1);
QCOMPARE(usage.sw, 1);
QCOMPARE(usage.other, 1);
QCOMPARE(usage.total(), 5);
}
// A slave standing for several contacts counts once per contact.
// Counting elements rather than contacts made a 4 pole contact
// consume a single contact from the master's budget.
void countsContactsNotElements()
{
ContactUsage usage;
usage.addSlave(ContactUsage::NO, 4);
QCOMPARE(usage.no, 4);
QCOMPARE(usage.total(), 4);
}
// A changeover is one contact of its own kind, never one NO plus
// one NC. CrossRefItem::NOElements() and NCElements() both return
// changeovers, so a count built by adding those two lists would
// report a single changeover as two contacts.
void changeoverIsCountedOnce()
{
ContactUsage usage;
usage.addSlave(ContactUsage::SW, 1);
QCOMPARE(usage.sw, 1);
QCOMPARE(usage.no, 0);
QCOMPARE(usage.nc, 0);
QCOMPARE(usage.total(), 1);
}
// An element which declares no contact count, or a nonsensical one,
// is still a contact.
void missingContactCountIsOneContact_data()
{
QTest::addColumn<int>("declared");
QTest::newRow("zero") << 0;
QTest::newRow("negative") << -1;
}
void missingContactCountIsOneContact()
{
QFETCH(int, declared);
ContactUsage usage;
usage.addSlave(ContactUsage::NO, declared);
QCOMPARE(usage.no, 1);
QCOMPARE(usage.total(), 1);
}
// A declared capacity is summed across groups, so two NO groups of two
// contacts each declare four NO contacts, not two groups.
void capacitySumsAcrossGroups()
{
ContactUsage capacity;
capacity.addSlave(ContactUsage::NO, 2);
capacity.addSlave(ContactUsage::NO, 2);
capacity.addSlave(ContactUsage::NC, 1);
QCOMPARE(capacity.no, 4);
QCOMPARE(capacity.nc, 1);
QCOMPARE(capacity.total(), 5);
}
// The mix a coil would actually carry: two single NO, one 4 pole NO,
// one NC and one changeover.
void tallysARealisticMix()
{
ContactUsage usage;
usage.addSlave(ContactUsage::NO, 1);
usage.addSlave(ContactUsage::NO, 1);
usage.addSlave(ContactUsage::NO, 4);
usage.addSlave(ContactUsage::NC, 1);
usage.addSlave(ContactUsage::SW, 1);
QCOMPARE(usage.no, 6);
QCOMPARE(usage.nc, 1);
QCOMPARE(usage.sw, 1);
QCOMPARE(usage.total(), 8);
}
};
QTEST_APPLESS_MAIN(tst_contactusage)
#include "tst_contactusage.moc"