Compare commits

...

14 Commits

Author SHA1 Message Date
Laurent Trinques c1694f2b4f Merge pull request #620 from ispyisail/fix/element-panel-dark-theme
Fix element library icons invisible on dark OS themes
2026-08-01 15:47:18 +02:00
Laurent Trinques 2d99ba67f1 Merge pull request #588 from Kellermorph/Fix-PLC-Manager
Fix-PLC-Manager
2026-08-01 15:30:10 +02:00
ispyisail bb61dde811 Apply the elements-panel light palette to the viewport too
ElementsPanel and ElementsTreeView already force a fixed light palette
(white base, black text) on themselves, specifically because element
icons are rendered with colors read directly from each .elmt file --
almost always black linework, matching printed-schematic convention --
onto a transparent background. That only stays legible if the row
background is reliably light, regardless of the OS/desktop theme.

But QAbstractItemView paints row backgrounds using its viewport's
palette, not the view widget's own palette. setPalette() on the view
itself doesn't propagate to viewport() in the general case, so under
styles that actually respect the viewport's (unset, therefore
theme-inherited) palette -- e.g. KDE Plasma's Breeze Dark -- the row
background falls through to the app's dark palette while the element
linework is still literal black, making library icons and terminal
symbols invisible.

Apply the same QPalette to viewport() right after setPalette() in both
constructors, so the fix these two classes already clearly intended
actually takes effect under every style.

Fixes https://qelectrotech.org/bugtracker/view.php?id=335
2026-08-02 01:23:23 +12:00
Kellermorph e91bab14fe Update 2026-08-01 14:26:39 +02:00
Laurent Trinques eb140dac60 Merge pull request #594 from ispyisail/feature-alpha-autonum
Add alphabetical auto-numbering (a, b, ... z, aa, ab, ...)
2026-08-01 12:55:07 +02:00
ispyisail f7a79e75af Add alphabetical auto-numbering (a, b, ... z, aa, ab, ...) (#579)
Adds a real base-26 incrementing part type to the autonumbering engine,
alongside the 14 existing NumStrategy leaves. Unlike StringNum (a fixed,
non-incrementing text segment), AlphaNum::next()/previous() carry/borrow
entirely within the part's own value -- the composition loop in
NumerotationContextCommands doesn't need to change, since (unlike #578's
wrap-and-carry) nothing here needs to signal an adjacent part.

- incrementAlpha()/decrementAlpha() implement the spreadsheet-column-name
  algorithm: increment carries right-to-left on 'z'/'Z' overflow,
  prepending a new leading letter if the whole value overflows (z -> aa,
  az -> ba). decrement is the exact inverse, including the symmetric
  shrink case (aa -> z) once every position has borrowed. A single letter
  already at "a"/"A" has no representable predecessor and is clamped
  rather than turned into "z" -- caught via manual testing, since the
  initial implementation mutated the string in the borrow loop before
  checking whether to clamp, silently discarding the original value.
- Registered in NumerotationContext::validRegExpNum() but deliberately
  not in validRegExpNumber(), so addValue() doesn't force alphabetic
  values through int conversion.
- New "Cyclique"-adjacent "Alphabétique" entry in numparteditorw's type
  dropdown, with its own letters-only QRegularExpressionValidator; the
  increase spinbox is disabled since the step is always exactly one
  letter, not a configurable amount.

Also wires the new part type through to actual element/conductor labels,
which turned out to be required for the feature to do anything visible
beyond folio numbering (which applies a NumerotationContext's
represented string directly). Element and conductor numbering instead
go through a separate formula-substitution layer
(autonum::sequentialNumbers + %sequ_/%seqt_/%seqh_-style placeholders in
AssignVariables::assignSequence()) that numerotationContextToFormula()
auto-populates. Without a matching placeholder, an "alpha" part would
silently vanish from the generated formula and never reach the label,
even though the underlying counter was advancing correctly:
- sequentialNumbers gained an `alpha` QStringList member (copy ctor,
  operator=, operator==, toXml/fromXml, clear()).
- numerotationContextToFormula() emits a new %seqa_N placeholder for
  alpha parts, the same way %sequ_N is emitted for unit parts.
- setSequential()/setSequentialToList() populate seqStruct.alpha,
  passing the raw string through as-is rather than the .toInt()-based
  formatting used for the numeric part types.
- AssignVariables::assignSequence() substitutes %seqa_N from
  seqStruct.alpha, mirroring the existing %sequ_N/%seqt_N/%seqh_N
  substitutions.
No "alphafolio" variant was added, matching the discussion's scope (only
unit/ten/hundred have folio-anchored variants).

Verified against production code via the numbering config dialog's own
Suivant/Précédent buttons: from "a", 25 clicks reached "z"; one more
produced "aa"; 25 more reached "az"; one more produced "ba" (carry).
Reversed: "ba"->"az"->(25 clicks)->"aa"->"z" (shrink)->(25 clicks)->"a".
One more "previous" at "a" correctly stayed at "a" after the clamp fix.
Also confirmed the Formule field auto-updates to "%seqa_1" the instant
the type is switched to "Alphabétique", confirming the formula-generation
wiring works live in the UI, not just at the engine level.
2026-08-01 22:45:49 +12:00
Laurent Trinques 25effbd137 Merge pull request #592 from DieterMayerOSS/pr/font-report
Report salvaged and unreadable font descriptions after opening a project
2026-08-01 11:41:55 +02:00
Dieter Mayer 3bc2e8e712 Make the font restoration counters safe against nested project loads
DialogWaiting pumps the event loop while the folios of a project are
built, so a second openAndAddProject() can run to completion nested
inside the first one (drop on another editor window, queued open) and
the plain reset/read counters would then report the wrong numbers.

Replace them with a RAII counting window (FontRestorationScope): the
constructor keeps the enclosing counts aside, the destructor restores
them. The nesting is strictly LIFO - the nested load completes inside
the pump of the outer one - so each load reports exactly its own
numbers, and the early-return paths of openAndAddProject() restore the
outer window automatically.

Suggested by ispyisail in the review of the reporting change.
2026-08-01 11:14:04 +02:00
Laurent Trinques b446074731 Merge pull request #593 from ispyisail/feature-wrap-and-carry-autonum
Add wrap-and-carry (cyclic/modulo) auto-numbering
2026-08-01 11:09:58 +02:00
Laurent Trinques a4d75c4d8c Merge pull request #586 from ispyisail/feature-jump-to-element
Add Ctrl+G "jump to element" quick-open popup
2026-08-01 10:58:33 +02:00
ispyisail 68c260342e Add wrap-and-carry (cyclic/modulo) auto-numbering (#578)
Adds a configurable wrap-at-N counter type to the autonumbering engine
(NumerotationContext + NumerotationContextCommands), covering PLC/rack-style
addressing conventions like "e0.0...e0.7, e1.0...e1.7" (8 channels per
card) generally, rather than hardcoding octal specifically.

- New "wrap" part type (WrapNum, alongside the existing UnitNum/TenNum/
  HundredNum strategies) stores a modulus in addition to the existing
  value/increase/initialvalue fields. Its own next()/previous() only wraps
  its own value modulo the configured modulus -- carrying into (or
  borrowing from) the adjacent part requires visibility across parts,
  which only the composition loop has.
- NumerotationContextCommands::next()/previous() gained carry()/borrow()
  helpers: when a wrap part's own next() would reach/exceed its modulus
  (or go below 0 on previous()), the nearest preceding numeric part is
  bumped by exactly one unit, skipping non-numeric parts (e.g. a "."
  string separator). Wrap parts chain correctly if adjacent (e.g. seconds
  wrapping into minutes wrapping into hours).
- For the leading part of a wrap-and-carry pair to stay fixed except when
  carried into (i.e. actually produce "e0.0...e0.7, e1.0..." rather than
  advancing on every step under its own strategy), its own increase must
  be 0. The increase spinbox's minimum was 1, which made this
  configuration impossible through the UI -- lowered to 0 and documented
  with a tooltip, since this wasn't obvious from the UI alone.
- NumerotationContext gained a 5th pipe-separated field (modulus) in its
  serialized string form, defaulting to 0 (non-wrapping) for every
  existing part type; toXml()/fromXml() persist it as a "modulus" XML
  attribute the same way "initialvalue" is already persisted for
  unitfolio/tenfolio/hundredfolio.
- New "Cyclique (modulo)" entry in the part-type dropdown (numparteditorw),
  available for element, conductor, and folio autonumbering alike, since
  all three already go through NumerotationContextCommands.

Verified in the running app via the numbering config dialog's own
Suivant/Précédent buttons (which call the production
NumerotationContextCommands::next()/previous() directly): a two-part
context (unit, increase=0 + wrap mod 8) produced exactly
e0.0→...→e0.7→e1.0→...→e1.7 on repeated "next", and the exact reverse
(with correct borrowing) on repeated "previous".
2026-08-01 19:33:39 +12:00
Dieter Mayer 0fa2591c4f Report salvaged and unreadable font descriptions after opening a project
Until now a font description that could not be parsed only produced
console warnings most users never see, so nobody learned that their
texts silently lost their formatting (see the reports in issue #553).

Count in QETUtils::fontFromString() how many descriptions were salvaged
from a foreign or corrupt format and how many stayed unreadable, and
show a message box after opening a project when either happened:
salvaged descriptions are rewritten in the stable format on the next
save, unreadable ones fall back to the default font. Projects without
font issues open exactly as before, and non-interactive opens only log
the counters.

Verified with a Qt 5.15 build on a project carrying 52 19-field and
one 21-field description: the dialog reports 53 restored descriptions;
the same file on a Qt 6.11 build (which parses those formats natively)
shows no dialog.

See issue #553.
2026-08-01 08:49:55 +02:00
Kellermorph 4c1f82f814 Fix-PLC-Manager 2026-07-31 21:02:07 +02:00
ispyisail 80fcd4283d Add Ctrl+G "jump to element" type-ahead popup (#574)
Implements the third pillar of #574: a lightweight quick-open popup
for jumping straight to an element on the current diagram, rather
than scrolling/scanning visually.

New JumpToElementDialog (sources/ui/): a small QDialog with a filter
QLineEdit and a live-filtered QListWidget beneath it. Built from
every Element on the diagram, searchable against its label
(elementInformations().value("label")), type name (Element::name()),
and every other element information value, joined into one
lowercased search string per candidate. Up/Down move through the
filtered list, Enter selects the highlighted element on the diagram
(clearing the rest of the selection) and scrolls it into view via
ensureVisible(), Escape cancels without changing the current
selection. All three are handled via an event filter on the line
edit, so the user never has to leave the text field to navigate or
confirm.

Triggered by a new Ctrl+G action in QETDiagramEditor, added next to
the existing Ctrl+F "search and replace" action and to the Edit
menu. Confirmed free: not used anywhere in qetdiagrameditor.cpp or
qetmainwindow.cpp today.

Explicitly not a duplicate of the existing SearchAndReplace module
(also on this menu, via Ctrl+F): that's a bulk property search/replace
tool across whole diagrams; this is a single-item navigational
popup with no editing capability.

Verified end-to-end in a real running session (Xvfb + xdotool)
against a multi-transistor schematic: Ctrl+G opens the popup listing
every element; typing "Q16" live-filters down to the one match;
arrow keys move the highlighted row through the filtered list;
Enter selects the highlighted element (confirmed via the properties
panel showing its label) and closes the popup; Escape closes it
without changing the selection.

See discussion #574.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-01 01:05:43 +12:00
29 changed files with 1222 additions and 152 deletions
+2
View File
@@ -699,6 +699,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/importelementdialog.h
${QET_DIR}/sources/ui/importelementtextpatterndialog.cpp
${QET_DIR}/sources/ui/importelementtextpatterndialog.h
${QET_DIR}/sources/ui/jumptoelementdialog.cpp
${QET_DIR}/sources/ui/jumptoelementdialog.h
${QET_DIR}/sources/ui/inditextpropertieswidget.cpp
${QET_DIR}/sources/ui/inditextpropertieswidget.h
${QET_DIR}/sources/ui/linksingleelementwidget.cpp
@@ -43,6 +43,14 @@ ElementsTreeView::ElementsTreeView(QWidget *parent) :
{
// force du noir sur une alternance de blanc (comme le schema) et de gris
// clair, avec du blanc sur bleu pas trop fonce pour la selection
//
// Element icons are rendered with colors read directly from each .elmt
// file (almost always black linework, matching printed-schematic
// convention) onto a transparent background -- so this view must keep
// a light background regardless of the OS/desktop theme, or the icons
// become invisible on dark themes. QAbstractItemView paints its rows
// using the viewport's palette, not the view's own, so the palette
// must be applied to both to actually take effect under every style.
QPalette qp = palette();
qp.setColor(QPalette::Text, Qt::black);
qp.setColor(QPalette::Base, Qt::white);
@@ -50,6 +58,7 @@ ElementsTreeView::ElementsTreeView(QWidget *parent) :
qp.setColor(QPalette::Highlight, QColor("#678db2"));
qp.setColor(QPalette::HighlightedText, Qt::black);
setPalette(qp);
viewport()->setPalette(qp);
}
/**
+37 -3
View File
@@ -44,6 +44,7 @@ namespace autonum
ten_folio = other.ten_folio;
hundred = other.hundred;
hundred_folio = other.hundred_folio;
alpha = other.alpha;
}
sequentialNumbers::~sequentialNumbers()
@@ -61,6 +62,7 @@ namespace autonum
ten_folio = other.ten_folio;
hundred = other.hundred;
hundred_folio = other.hundred_folio;
alpha = other.alpha;
return (*this);
}
@@ -72,7 +74,8 @@ namespace autonum
ten == other.ten && \
ten_folio == other.ten_folio && \
hundred == other.hundred && \
hundred_folio == other.hundred_folio)
hundred_folio == other.hundred_folio && \
alpha == other.alpha)
return true;
else
return false;
@@ -129,6 +132,11 @@ namespace autonum
document,
"hundredFolio",
hundred_folio.join(";")));
if(!alpha.isEmpty())
element.appendChild(QETXML::textToDomElement(
document,
"alpha",
alpha.join(";")));
return element;
}
@@ -162,6 +170,9 @@ namespace autonum
from = element.firstChildElement("hundredFolio");
hundred_folio = from.text().split(";");
from = element.firstChildElement("alpha");
alpha = from.text().split(";");
}
//Clear this sequence
@@ -173,6 +184,7 @@ namespace autonum
ten_folio.clear();
hundred.clear();
hundred_folio.clear();
alpha.clear();
}
/**
@@ -280,6 +292,11 @@ namespace autonum
str.replace("%{plc_function}", dc.value("plc_function").toString());
str.replace("%{plc_comment}", dc.value("plc_comment").toString());
str.replace("%{plc_crossref}", dc.value("plc_crossref").toString());
str.replace("%{plc_tc}", dc.value("plc_tc").toString());
str.replace("%{plc_t1}", dc.value("plc_t1").toString());
str.replace("%{plc_t2}", dc.value("plc_t2").toString());
str.replace("%{plc_t3}", dc.value("plc_t3").toString());
str.replace("%{plc_t4}", dc.value("plc_t4").toString());
return str;
}
@@ -414,8 +431,10 @@ namespace autonum
m_seq_struct.ten_folio.size()),
qMax(m_seq_struct.hundred_folio.size(),
m_seq_struct.unit.size())),
qMax(m_seq_struct.hundred.size(),
m_seq_struct.ten.size())
qMax(
qMax(m_seq_struct.hundred.size(),
m_seq_struct.ten.size()),
m_seq_struct.alpha.size())
);
for (int i=1; i<=max ; i++)
@@ -438,6 +457,9 @@ namespace autonum
if (m_assigned_label.contains("%seqhf_" + QString::number(i)) && m_seq_struct.hundred_folio.size() >= i) {
m_assigned_label.replace("%seqhf_" + QString::number(i),m_seq_struct.hundred_folio.at(i-1));
}
if (m_assigned_label.contains("%seqa_" + QString::number(i)) && m_seq_struct.alpha.size() >= i) {
m_assigned_label.replace("%seqa_" + QString::number(i),m_seq_struct.alpha.at(i-1));
}
}
}
@@ -462,6 +484,9 @@ namespace autonum
number = QString("%1").arg(context.itemAt(i).at(1).toInt(), 2, 10, QChar('0'));
else if (type == "hundred" || type == "hundredfolio")
number = QString("%1").arg(context.itemAt(i).at(1).toInt(), 3, 10, QChar('0'));
else if (type == "alpha")
//Alphabetic value, not an integer -- used as-is.
number = context.itemAt(i).at(1);
else number = QString::number(context.itemAt(i).at(1).toInt());
list.append(number);
}
@@ -551,6 +576,10 @@ namespace autonum
autonum::setSequentialToList(seqStruct.hundred_folio, context,"hundredfolio");
autonum::setFolioSequentialToHash(seqStruct.hundred_folio, diagram->m_elmt_hundredfolio_max, hashKey);
}
if (label.contains("%seqa_"))
{
autonum::setSequentialToList(seqStruct.alpha, context,"alpha");
}
}
}
@@ -570,6 +599,7 @@ namespace autonum
int count_tenf = 0;
int count_hundred = 0;
int count_hundredf = 0;
int count_alpha = 0;
for(int i=0 ; i<nc.size() ; i++)
{
@@ -626,6 +656,10 @@ namespace autonum
count_hundredf++;
formula.append("%seqhf_" + QString::number(count_hundredf));
}
else if (type == "alpha") {
count_alpha++;
formula.append("%seqa_" + QString::number(count_alpha));
}
}
return formula;
+1
View File
@@ -52,6 +52,7 @@ namespace autonum
QStringList ten_folio;
QStringList hundred;
QStringList hundred_folio;
QStringList alpha;
};
/**
+18 -10
View File
@@ -51,12 +51,14 @@ void NumerotationContext::clear ()
@param value the value itself
@param increase the increase number of value
@param initialvalue
@param modulus wrap-and-carry modulus (0 means "not a wrapping part")
@return true if value is append
*/
bool NumerotationContext::addValue(const QString &type,
const QVariant &value,
const int increase,
const int initialvalue) {
const int initialvalue,
const int modulus) {
if (!keyIsAcceptable(type) && !value.canConvert<QString>())
return false;
if (keyIsNumber(type) && !value.canConvert<int>())
@@ -70,7 +72,9 @@ bool NumerotationContext::addValue(const QString &type,
+ "|"
+ QString::number(increase)
+ "|"
+ QString::number(initialvalue);
+ QString::number(initialvalue)
+ "|"
+ QString::number(modulus);
return true;
}
@@ -125,7 +129,7 @@ QStringList NumerotationContext::itemAt(const int i) const
*/
QString NumerotationContext::validRegExpNum () const
{
return ("unit|unitfolio|ten|tenfolio|hundred|hundredfolio|string|idfolio|folio|plant|locmach|elementline|elementcolumn|elementprefix");
return ("unit|unitfolio|ten|tenfolio|hundred|hundredfolio|wrap|alpha|string|idfolio|folio|plant|locmach|elementline|elementcolumn|elementprefix");
}
/**
@@ -134,7 +138,7 @@ QString NumerotationContext::validRegExpNum () const
*/
QString NumerotationContext::validRegExpNumber() const
{
return ("unit|unitfolio|ten|tenfolio|hundred|hundredfolio");
return ("unit|unitfolio|ten|tenfolio|hundred|hundredfolio|wrap");
}
/**
@@ -172,6 +176,9 @@ QDomElement NumerotationContext::toXml(QDomDocument &d, const QString& str) {
strl.at(0) == ("hundredfolio")) {
part.setAttribute("initialvalue", strl.at(3));
}
if (strl.at(0) == ("wrap") && strl.size() > 4) {
part.setAttribute("modulus", strl.at(4));
}
num_auto.appendChild(part);
}
return num_auto;
@@ -183,7 +190,7 @@ QDomElement NumerotationContext::toXml(QDomDocument &d, const QString& str) {
*/
void NumerotationContext::fromXml(QDomElement &e) {
clear();
foreach(QDomElement qde, QET::findInDomElement(e, "part")) addValue(qde.attribute("type"), qde.attribute("value"), qde.attribute("increase").toInt(), qde.attribute("initialvalue").toInt());
foreach(QDomElement qde, QET::findInDomElement(e, "part")) addValue(qde.attribute("type"), qde.attribute("value"), qde.attribute("increase").toInt(), qde.attribute("initialvalue").toInt(), qde.attribute("modulus").toInt());
}
/**
@@ -193,10 +200,11 @@ void NumerotationContext::fromXml(QDomElement &e) {
@param content to replace current value
*/
void NumerotationContext::replaceValue(int index, QString content) {
QString sep = "|";
QString type = content_[index].split("|").at(0);
QStringList strl = content_[index].split("|");
QString type = strl.at(0);
const QString& value = std::move(content);
QString increase = content_[index].split("|").at(2);
QString initvalue = content_[index].split("|").at(3);
content_[index].replace(content_[index], type + "|" + value + "|" + increase + "|" + initvalue);
QString increase = strl.at(2);
QString initvalue = strl.at(3);
QString modulus = strl.size() > 4 ? strl.at(4) : QStringLiteral("0");
content_[index] = type + "|" + value + "|" + increase + "|" + initvalue + "|" + modulus;
}
+1
View File
@@ -36,6 +36,7 @@ class NumerotationContext
bool addValue(const QString &,
const QVariant & = QVariant(1),
const int = 1,
const int = 0,
const int = 0);
QString operator[] (const int &) const;
void operator << (const NumerotationContext &);
@@ -48,6 +48,15 @@ NumerotationContext NumerotationContextCommands::next()
QStringList str = context_.itemAt(i);
setNumStrategy(str.at(0));
contextnum << strategy_ -> next(context_, i);
//Wrap-and-carry: str still holds the pre-increment value, so
//this checks the same condition WrapNum::next() used to decide
//whether to wrap its own value back to 0.
if (str.at(0) == "wrap" && str.size() > 4) {
int modulus = str.at(4).toInt();
if (modulus > 0 && (str.at(1).toInt() + str.at(2).toInt()) >= modulus)
carry(contextnum, i - 1);
}
}
return contextnum;
}
@@ -64,10 +73,80 @@ NumerotationContext NumerotationContextCommands::previous()
QStringList str = context_.itemAt(i);
setNumStrategy(str.at(0));
contextnum << strategy_ -> previous(context_, i);
if (str.at(0) == "wrap" && str.size() > 4) {
int modulus = str.at(4).toInt();
if (modulus > 0 && (str.at(1).toInt() - str.at(2).toInt()) < 0)
borrow(contextnum, i - 1);
}
}
return contextnum;
}
/**
@brief NumerotationContextCommands::carry
Add one unit to the nearest numeric part at or before from_index in
contextnum, skipping non-numeric parts (e.g. a "." string separator)
along the way. If that part is itself a wrap part and this pushes it
to (or past) its own modulus, it wraps back to 0 and the carry
cascades further back -- so wrap parts can be chained (e.g. seconds
wrapping into minutes wrapping into hours).
@param contextnum the context being built by next(); already contains
entries for every index <= from_index
@param from_index index to start looking from, going backwards
*/
void NumerotationContextCommands::carry(NumerotationContext &contextnum, int from_index)
{
for (int j = from_index; j >= 0; --j) {
QStringList strl = contextnum.itemAt(j);
if (!contextnum.keyIsNumber(strl.at(0)))
continue;
int value = strl.at(1).toInt() + 1;
if (strl.at(0) == "wrap" && strl.size() > 4) {
int modulus = strl.at(4).toInt();
if (modulus > 0 && value >= modulus) {
contextnum.replaceValue(j, QString::number(value - modulus));
carry(contextnum, j - 1);
return;
}
}
contextnum.replaceValue(j, QString::number(value));
return;
}
//No preceding numeric part: the carry has nowhere to go and is dropped,
//same as any other counter in this engine has no overflow tracking
//beyond the parts the user actually configured.
}
/**
@brief NumerotationContextCommands::borrow
Inverse of carry(): subtract one unit from the nearest numeric part at
or before from_index. If that part is itself a wrap part and this
takes it below 0, it wraps to (modulus - 1) and the borrow cascades
further back.
*/
void NumerotationContextCommands::borrow(NumerotationContext &contextnum, int from_index)
{
for (int j = from_index; j >= 0; --j) {
QStringList strl = contextnum.itemAt(j);
if (!contextnum.keyIsNumber(strl.at(0)))
continue;
int value = strl.at(1).toInt() - 1;
if (strl.at(0) == "wrap" && strl.size() > 4) {
int modulus = strl.at(4).toInt();
if (modulus > 0 && value < 0) {
contextnum.replaceValue(j, QString::number(value + modulus));
borrow(contextnum, j - 1);
return;
}
}
contextnum.replaceValue(j, QString::number(value));
return;
}
}
/**
@brief NumerotationContextCommands::toFinalString
@return the string represented by the numerotation context
@@ -117,10 +196,18 @@ void NumerotationContextCommands::setNumStrategy(const QString &str) {
strategy_ = new HundredFNum (diagram_);
return;
}
else if (str == "wrap") {
strategy_ = new WrapNum (diagram_);
return;
}
else if (str == "string") {
strategy_ = new StringNum (diagram_);
return;
}
else if (str == "alpha") {
strategy_ = new AlphaNum (diagram_);
return;
}
else if (str == "idfolio") {
strategy_ = new IdFolioNum (diagram_);
return;
@@ -431,6 +518,62 @@ NumerotationContext HundredFNum::previous(const NumerotationContext &nc, const i
return (previousNumber(nc, i));
}
/**
Constructor
*/
WrapNum::WrapNum (Diagram *d):
NumStrategy (d)
{}
/**
@brief WrapNum::toRepresentedString
@return the represented string of num
*/
QString WrapNum::toRepresentedString(const QString num) const
{
return (num);
}
/**
@brief WrapNum::next
Wraps this part's own value back to 0 every `modulus` values (carrying
into the adjacent part is handled by NumerotationContextCommands::next(),
which has visibility into the other parts).
@return the next NumerotationContext nc at position i
*/
NumerotationContext WrapNum::next (const NumerotationContext &nc, const int i) const
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
int increase = strl.at(2).toInt();
int modulus = strl.size() > 4 ? strl.at(4).toInt() : 0;
int new_value = strl.at(1).toInt() + increase;
if (modulus > 0)
new_value %= modulus;
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus);
return (newnc);
}
/**
@brief WrapNum::previous
@return the previous NumerotationContext nc at posiiton i
*/
NumerotationContext WrapNum::previous(const NumerotationContext &nc, const int i) const
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
int increase = strl.at(2).toInt();
int modulus = strl.size() > 4 ? strl.at(4).toInt() : 0;
int new_value = strl.at(1).toInt() - increase;
if (modulus > 0) {
new_value %= modulus;
if (new_value < 0)
new_value += modulus;
}
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus);
return (newnc);
}
/**
Constructor
*/
@@ -465,6 +608,120 @@ NumerotationContext StringNum::previous(const NumerotationContext &nc, const int
return (nextString(nc, i));
}
namespace
{
/**
* @brief incrementAlpha
* Base-26 letter increment (a, b, ... z, aa, ab, ... az, ba, ...),
* the same algorithm as incrementing a spreadsheet column name.
* Carries right-to-left on 'z'/'Z' overflow; if the whole string
* overflows, a new leading letter is prepended (lowercase 'a').
* @param value : current alphabetic value; treated as "a" if empty.
* @return the next value.
*/
QString incrementAlpha(QString value)
{
if (value.isEmpty()) {
return QStringLiteral("a");
}
int i = value.length() - 1;
while (i >= 0 && (value.at(i) == QLatin1Char('z') || value.at(i) == QLatin1Char('Z'))) {
value[i] = value.at(i).isUpper() ? QLatin1Char('A') : QLatin1Char('a');
--i;
}
if (i < 0) {
value.prepend(QLatin1Char('a'));
} else {
value[i] = QChar(value.at(i).unicode() + 1);
}
return value;
}
/**
* @brief decrementAlpha
* Inverse of incrementAlpha(): borrows right-to-left on 'a'/'A'
* underflow. Symmetric shrink case (e.g. "aa" -> "z"): once every
* position has borrowed, the leading letter is dropped rather than
* left as an extra 'z'. A single-letter value already at "a"/"A" has
* no representable predecessor and is left unchanged, the same way
* the numeric parts don't clamp but a blank label would be worse
* here than a value that stops decreasing.
* @param value : current alphabetic value; treated as "a" if empty.
* @return the previous value.
*/
QString decrementAlpha(QString value)
{
if (value.isEmpty()) {
return QStringLiteral("a");
}
if (value.length() == 1) {
//A single letter has no representable predecessor once it
//reaches "a"/"A" -- clamp rather than mutate, since the loop
//below would otherwise turn it into "z"/"Z" (borrowing past
//the only position there is).
if (value.at(0) == QLatin1Char('a') || value.at(0) == QLatin1Char('A')) {
return value;
}
return QChar(value.at(0).unicode() - 1);
}
int i = value.length() - 1;
while (i >= 0 && (value.at(i) == QLatin1Char('a') || value.at(i) == QLatin1Char('A'))) {
value[i] = value.at(i).isUpper() ? QLatin1Char('Z') : QLatin1Char('z');
--i;
}
if (i < 0) {
//Every position borrowed: the whole value was "a...a", whose
//predecessor is one fewer "z" (e.g. "aa" -> "z").
value.remove(0, 1);
} else {
value[i] = QChar(value.at(i).unicode() - 1);
}
return value;
}
}
/**
Constructor
*/
AlphaNum::AlphaNum (Diagram *d):
NumStrategy (d)
{}
/**
@brief AlphaNum::toRepresentedString
@return the represented string of str
*/
QString AlphaNum::toRepresentedString(const QString str) const
{
return (str);
}
/**
@brief AlphaNum::next
@return the next NumerotationContext nc at position i
*/
NumerotationContext AlphaNum::next (const NumerotationContext &nc, const int i) const
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
newnc.addValue(strl.at(0), incrementAlpha(strl.at(1)), strl.at(2).toInt());
return (newnc);
}
/**
@brief AlphaNum::previous
@return the previous NumerotationContext nc at posiiton i
*/
NumerotationContext AlphaNum::previous(const NumerotationContext &nc, const int i) const
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
newnc.addValue(strl.at(0), decrementAlpha(strl.at(1)), strl.at(2).toInt());
return (newnc);
}
/**
Constructor
*/
@@ -38,6 +38,8 @@ class NumerotationContextCommands
private:
void setNumStrategy (const QString &);
static void carry(NumerotationContext &contextnum, int from_index);
static void borrow(NumerotationContext &contextnum, int from_index);
Diagram *diagram_;
NumerotationContext context_;
@@ -115,6 +117,24 @@ class HundredFNum: public NumStrategy
NumerotationContext previous (const NumerotationContext &, const int) const override;
};
/**
@brief The WrapNum class
A counter that wraps back to 0 (borrowing from initialvalue on the
way down) every `modulus` values, instead of counting up forever like
UnitNum/TenNum/HundredNum. Its own next()/previous() only computes its
own wrapped value; carrying into (or borrowing from) the preceding
numeric part is handled by NumerotationContextCommands::next()/
previous(), since only the composition loop can see adjacent parts.
*/
class WrapNum: public NumStrategy
{
public:
WrapNum (Diagram *);
QString toRepresentedString(const QString) const override;
NumerotationContext next (const NumerotationContext &, const int) const override;
NumerotationContext previous (const NumerotationContext &, const int) const override;
};
class StringNum: public NumStrategy
{
public:
@@ -124,6 +144,23 @@ class StringNum: public NumStrategy
NumerotationContext previous (const NumerotationContext &, const int) const override;
};
/**
@brief The AlphaNum class
Alphabetic auto-numbering (a, b, ... z, aa, ab, ...). Unlike StringNum
(a fixed, non-incrementing text segment), this is a real base-26
counter: next()/previous() carry/borrow entirely within this part's
own value, the same self-contained shape every other incrementing
NumStrategy already has.
*/
class AlphaNum: public NumStrategy
{
public:
AlphaNum (Diagram *);
QString toRepresentedString(const QString) const override;
NumerotationContext next (const NumerotationContext &, const int) const override;
NumerotationContext previous (const NumerotationContext &, const int) const override;
};
class IdFolioNum: public NumStrategy
{
public:
+57
View File
@@ -18,6 +18,8 @@
#include "numparteditorw.h"
#include "ui_numparteditorw.h"
#include <QRegularExpressionValidator>
/**
@brief NumPartEditorW::NumPartEditorW
Constructor
@@ -28,6 +30,7 @@ NumPartEditorW::NumPartEditorW(int type, QWidget *parent) :
QWidget(parent),
ui(new Ui::NumPartEditorW),
intValidator (new QIntValidator(0,99999,this)),
alphaValidator (new QRegularExpressionValidator(QRegularExpression("[A-Za-z]+"), this)),
m_edited_type(type)
{
ui -> setupUi(this);
@@ -51,6 +54,7 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
QWidget(parent),
ui(new Ui::NumPartEditorW),
intValidator (new QIntValidator(0,99999,this)),
alphaValidator (new QRegularExpressionValidator(QRegularExpression("[A-Za-z]+"), this)),
m_edited_type(type)
{
ui -> setupUi(this);
@@ -71,6 +75,10 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
setType(NumPartEditorW::hundred, true);
else if (strl.at(0)=="hundredfolio")
setType(NumPartEditorW::hundredfolio, true);
else if (strl.at(0)=="wrap")
setType(NumPartEditorW::wrap, true);
else if (strl.at(0)=="alpha")
setType(NumPartEditorW::alpha);
else if (strl.at(0)=="string")
setType(NumPartEditorW::string);
else if (strl.at(0)=="idfolio")
@@ -89,6 +97,8 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
setType(NumPartEditorW::elementprefix);
ui -> value_field -> setText(strl.at(1));
ui -> increase_spinBox -> setValue(strl.at(2).toInt());
if (strl.at(0)=="wrap" && strl.size() > 4)
ui -> modulus_spinBox -> setValue(strl.at(4).toInt());
}
}
@@ -98,6 +108,7 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
NumPartEditorW::~NumPartEditorW()
{
delete intValidator;
delete alphaValidator;
delete ui;
}
@@ -110,6 +121,8 @@ void NumPartEditorW::setVisibleItems()
items << tr("Chiffre 1")
<< tr("Chiffre 01")
<< tr("Chiffre 001")
<< tr("Cyclique (modulo)")
<< tr("Alphabétique")
<< tr("Texte");
}
else if (m_edited_type == 1)
@@ -120,6 +133,8 @@ void NumPartEditorW::setVisibleItems()
<< tr("Chiffre 01 - Folio")
<< tr("Chiffre 001")
<< tr("Chiffre 001 - Folio")
<< tr("Cyclique (modulo)")
<< tr("Alphabétique")
<< tr("Texte")
<< tr("N° folio")
<< tr("Folio")
@@ -133,6 +148,8 @@ void NumPartEditorW::setVisibleItems()
<< tr("Chiffre 01 - Folio")
<< tr("Chiffre 001")
<< tr("Chiffre 001 - Folio")
<< tr("Cyclique (modulo)")
<< tr("Alphabétique")
<< tr("Texte")
<< tr("N° folio")
<< tr("Folio")
@@ -195,6 +212,12 @@ NumerotationContext NumPartEditorW::toNumContext()
case elementprefix:
type_str = "elementprefix";
break;
case wrap:
type_str = "wrap";
break;
case alpha:
type_str = "alpha";
break;
}
if (type_str == "unitfolio"
|| type_str == "tenfolio"
@@ -203,6 +226,12 @@ NumerotationContext NumPartEditorW::toNumContext()
ui -> value_field -> displayText(),
ui -> increase_spinBox -> value(),
ui->value_field->displayText().toInt());
else if (type_str == "wrap")
nc.addValue(type_str,
ui -> value_field -> displayText(),
ui -> increase_spinBox -> value(),
0,
ui -> modulus_spinBox -> value());
else
nc.addValue(type_str,
ui -> value_field -> displayText(),
@@ -260,6 +289,10 @@ void NumPartEditorW::on_type_cb_activated(int) {
setType(elementcolumn);
else if (ui->type_cb->currentText() == tr("Element Prefix"))
setType(elementprefix);
else if (ui->type_cb->currentText() == tr("Cyclique (modulo)"))
setType(wrap);
else if (ui->type_cb->currentText() == tr("Alphabétique"))
setType(alpha);
emit changed();
}
@@ -280,6 +313,14 @@ void NumPartEditorW::on_increase_spinBox_valueChanged(int) {
if (!ui -> value_field -> text().isEmpty()) emit changed();
}
/**
@brief NumPartEditorW::on_modulus_spinBox_valueChanged
emit changed when modulus_spinBox value changed
*/
void NumPartEditorW::on_modulus_spinBox_valueChanged(int) {
if (!ui -> value_field -> text().isEmpty()) emit changed();
}
/**
@brief NumPartEditorW::setType
Set good behavior by type t
@@ -299,6 +340,7 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
|| t==tenfolio
|| t==hundred
|| t==hundredfolio
|| t==wrap
)
&& (type_==string
|| type_==folio
@@ -317,9 +359,12 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
ui -> value_field -> setValidator(intValidator);
ui -> increase_spinBox -> setEnabled(true);
ui -> increase_spinBox -> setValue(1);
if (t == wrap)
ui -> modulus_spinBox -> setValue(8);
}
//@t isn't a numeric type
else if (t == string
|| t == alpha
|| t == folio
|| t == idfolio
|| t == elementline
@@ -333,6 +378,13 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
ui -> value_field -> setValidator(nullptr);
ui -> value_field -> setEnabled(true);
}
else if (t==alpha) {
//Alphabetic step is always exactly one letter (a, b, ...);
//there is no numeric "increase" to configure, unlike the
//digit-based part types.
ui -> value_field -> setValidator(alphaValidator);
ui -> value_field -> setEnabled(true);
}
else if (t==folio) {
ui -> value_field -> setDisabled(true);
ui -> increase_spinBox -> setDisabled(true);
@@ -362,6 +414,7 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
ui -> increase_spinBox -> setDisabled(true);
}
}
ui -> modulus_spinBox -> setEnabled(t == wrap);
type_= t;
}
@@ -384,6 +437,8 @@ void NumPartEditorW::setCurrentIndex(NumPartEditorW::type t) {
i = ui->type_cb->findText(tr("Chiffre 001"));
else if (t == hundredfolio)
i = ui->type_cb->findText(tr("Chiffre 001 - Folio"));
else if (t == alpha)
i = ui->type_cb->findText(tr("Alphabétique"));
else if (t == string)
i = ui->type_cb->findText(tr("Texte"));
else if (t == idfolio)
@@ -400,5 +455,7 @@ void NumPartEditorW::setCurrentIndex(NumPartEditorW::type t) {
i = ui->type_cb->findText(tr("Element Column"));
else if (t == elementprefix)
i = ui->type_cb->findText(tr("Element Prefix"));
else if (t == wrap)
i = ui->type_cb->findText(tr("Cyclique (modulo)"));
ui->type_cb->setCurrentIndex(i);
}
+4
View File
@@ -47,8 +47,10 @@ class NumPartEditorW : public QWidget
~NumPartEditorW() override;
enum type {unit,unitfolio,ten,tenfolio, hundred, hundredfolio,
alpha,
string,idfolio,folio,plant,locmach,
elementline,elementcolumn,elementprefix,
wrap,
};
NumerotationContext toNumContext();
bool isValid ();
@@ -63,6 +65,7 @@ class NumPartEditorW : public QWidget
void on_type_cb_activated(int);
void on_value_field_textEdited();
void on_increase_spinBox_valueChanged(int);
void on_modulus_spinBox_valueChanged(int);
void setType (NumPartEditorW::type t, bool=false);
signals:
@@ -71,6 +74,7 @@ class NumPartEditorW : public QWidget
private:
Ui::NumPartEditorW *ui;
QValidator *intValidator;
QValidator *alphaValidator;
int m_edited_type = -1; ///<0 == element : 1 == conductor : 2 == folio
+35 -1
View File
@@ -75,6 +75,9 @@
<property name="wrapping">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Mettre à 0 pour un chiffre qui n'avance que par le report d'un chiffre cyclique suivant (ex: le "0" de "0.7")</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
@@ -88,7 +91,38 @@
<string/>
</property>
<property name="minimum">
<number>1</number>
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="modulus_spinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Valeur à laquelle ce chiffre revient à 0 en incrémentant le chiffre précédent (0 = pas de cycle)</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="accelerated">
<bool>true</bool>
</property>
<property name="prefix">
<string>mod. </string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>99999</number>
</property>
</widget>
</item>
+9 -3
View File
@@ -388,9 +388,15 @@ void projectDataBase::createElementNomenclatureView()
"ei.supplier_auxiliary4 AS supplier_auxiliary4,"
"ei.quantity_auxiliary4 AS quantity_auxiliary4,"
"ei.unity_auxiliary4 AS unity_auxiliary4,"
"ei.exclude_from_bom AS exclude_from_bom,"
"d.pos AS diagram_position,"
"ei.exclude_from_bom AS exclude_from_bom,"
"ei.plc_type AS plc_type,"
"ei.plc_address AS plc_address,"
"ei.plc_function AS plc_function,"
"ei.plc_comment AS plc_comment,"
"ei.plc_crossref AS plc_crossref,"
"d.pos AS diagram_position,"
"e.type AS element_type,"
"e.sub_type AS element_sub_type,"
"di.title AS title,"
+17 -7
View File
@@ -47,6 +47,7 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
m_button_group.addButton(ui->m_coil_cb, 4);
m_button_group.addButton(ui->m_protection_cb, 5);
m_button_group.addButton(ui->m_thumbnail_cb, 6);
m_button_group.addButton(ui->m_plc_cb, 7);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), [this](int id)
#else
@@ -76,7 +77,7 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
else
{
int checked = 0;
for (int i=1 ; i<7 ; ++i) {
for (int i=1 ; i<8 ; ++i) {
if (m_button_group.button(i)->isChecked()) {++checked;}
}
@@ -85,7 +86,7 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
case 0 :
check_box->setCheckState(Qt::Unchecked);
break;
case 6:
case 7:
check_box->setCheckState(Qt::Checked);
break;
default:
@@ -187,12 +188,16 @@ void ElementQueryWidget::setQuery(const QString &query)
if (ui->m_protection_cb) {
++c;
}
ui->m_thumbnail_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Thumbnail)) ? true : false);
if (ui->m_thumbnail_cb->isChecked()) {
++c;
}
ui->m_thumbnail_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Thumbnail)) ? true : false);
if (ui->m_thumbnail_cb->isChecked()) {
++c;
}
ui->m_plc_cb->setChecked (str_type.contains(ElementData::masterTypeToString(ElementData::PLC)) ? true : false);
if (ui->m_plc_cb->isChecked()) {
++c;
}
if (c == 6) {
if (c == 7) {
ui->m_all_cb->setCheckState(Qt::Checked);
} else if (c > 0) {
ui->m_all_cb->setCheckState(Qt::PartiallyChecked);
@@ -366,6 +371,11 @@ QString ElementQueryWidget::queryStr() const
if (ui->m_protection_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Protection) += "'";
b = true;
}
if (ui->m_plc_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'";
}
where.append(")");
+90 -80
View File
@@ -234,86 +234,96 @@
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Type d'éléments</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="5" column="0">
<widget class="QCheckBox" name="m_simple_cb">
<property name="text">
<string>Simples</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="m_protection_cb">
<property name="text">
<string>Organes de protection</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="m_all_cb">
<property name="text">
<string>Tous</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="m_button_cb">
<property name="text">
<string>Boutons et commutateurs</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="m_terminal_cb">
<property name="text">
<string>Borniers</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="m_thumbnail_cb">
<property name="text">
<string>Vignettes</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="m_coil_cb">
<property name="text">
<string>Contacteurs et relais</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Type d'éléments</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QCheckBox" name="m_all_cb">
<property name="text">
<string>Tous</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="m_simple_cb">
<property name="text">
<string>Simples</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="m_terminal_cb">
<property name="text">
<string>Borniers</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="m_protection_cb">
<property name="text">
<string>Organes de protection</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="m_coil_cb">
<property name="text">
<string>Contacteurs et relais</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="m_thumbnail_cb">
<property name="text">
<string>Vignettes</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="m_button_cb">
<property name="text">
<string>Boutons et commutateurs</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="m_plc_cb">
<property name="text">
<string>Automates (MAE/SPS)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
+11 -1
View File
@@ -240,7 +240,12 @@ void DynamicTextFieldEditor::fillInfoComboBox()
QETInformation::ELMT_PLC_ADDRESS,
QETInformation::ELMT_PLC_FUNCTION,
QETInformation::ELMT_PLC_COMMENT,
QETInformation::ELMT_PLC_CROSSREF
QETInformation::ELMT_PLC_CROSSREF,
QETInformation::ELMT_PLC_TC,
QETInformation::ELMT_PLC_T1,
QETInformation::ELMT_PLC_T2,
QETInformation::ELMT_PLC_T3,
QETInformation::ELMT_PLC_T4
};
strl = plc_keys + strl;
} else {
@@ -249,6 +254,11 @@ void DynamicTextFieldEditor::fillInfoComboBox()
strl.removeAll(QETInformation::ELMT_PLC_FUNCTION);
strl.removeAll(QETInformation::ELMT_PLC_COMMENT);
strl.removeAll(QETInformation::ELMT_PLC_CROSSREF);
strl.removeAll(QETInformation::ELMT_PLC_TC);
strl.removeAll(QETInformation::ELMT_PLC_T1);
strl.removeAll(QETInformation::ELMT_PLC_T2);
strl.removeAll(QETInformation::ELMT_PLC_T3);
strl.removeAll(QETInformation::ELMT_PLC_T4);
}
}
@@ -683,7 +683,7 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
m_plc_table->horizontalHeader()->resizeSection(2, 150);
m_plc_table->horizontalHeader()->resizeSection(3, 150);
m_plc_table->horizontalHeader()->resizeSection(4, 100);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200);
tables_splitter->addWidget(m_plc_table);
@@ -697,7 +697,7 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
m_plc_terminal_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
m_plc_terminal_table->horizontalHeader()->resizeSection(0, 50);
m_plc_terminal_table->horizontalHeader()->resizeSection(1, 80);
m_plc_terminal_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_plc_terminal_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_terminal_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_terminal_table->setMinimumHeight(200);
tables_splitter->addWidget(m_plc_terminal_table);
@@ -780,7 +780,7 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
auto *sb = new QSpinBox(m_plc_gb);
sb->setMinimum(10);
sb->setMaximum(200);
sb->setMaximum(500);
sb->setValue(40);
sb->setSuffix(tr(" mm"));
m_plc_col_width_spinboxes.append(sb);
+9
View File
@@ -56,6 +56,14 @@ ElementsPanel::ElementsPanel(QWidget *parent) :
// force du noir sur une alternance de blanc (comme le schema) et de gris
// clair, avec du blanc sur bleu pas trop fonce pour la selection
//
// Element icons are rendered with colors read directly from each .elmt
// file (almost always black linework, matching printed-schematic
// convention) onto a transparent background -- so this view must keep
// a light background regardless of the OS/desktop theme, or the icons
// become invisible on dark themes. QAbstractItemView paints its rows
// using the viewport's palette, not the view's own, so the palette
// must be applied to both to actually take effect under every style.
QPalette qp = palette();
qp.setColor(QPalette::Text, Qt::black);
qp.setColor(QPalette::Base, Qt::white);
@@ -63,6 +71,7 @@ ElementsPanel::ElementsPanel(QWidget *parent) :
qp.setColor(QPalette::Highlight, QColor("#678db2"));
qp.setColor(QPalette::HighlightedText, Qt::black);
setPalette(qp);
viewport()->setPalette(qp);
// we handle double click on items ourselves
connect(this, &ElementsPanel::itemDoubleClicked, this, &ElementsPanel::slot_doubleClick);
+59
View File
@@ -40,10 +40,12 @@
#include "recentfiles.h"
#include "shortcutmanager.h"
#include "ui/bomexportdialog.h"
#include "ui/jumptoelementdialog.h"
#include "ui/diagrampropertieseditordockwidget.h"
#include "ui/backupdialog.h"
#include "ui/dialogwaiting.h"
#include "undocommand/addelementtextcommand.h"
#include "utils/qetutils.h"
#include "undocommand/rotateselectioncommand.h"
#include "undocommand/rotatetextscommand.h"
#include "diagram.h"
@@ -748,6 +750,19 @@ void QETDiagramEditor::setUpActions()
this->m_search_and_replace_widget.setHidden(!m_search_and_replace_widget.isHidden());
}
});
m_jump_to_element = new QAction(tr("Atteindre un élément"), this);
m_jump_to_element->setShortcut(Qt::CTRL | Qt::Key_G);
m_jump_to_element->setStatusTip(tr("Recherche et sélectionne rapidement un élément du folio", "status bar tip"));
connect(m_jump_to_element, &QAction::triggered, [this]()
{
DiagramView *diagram_view = this->currentDiagramView();
if (!diagram_view || !diagram_view->diagram()) {
return;
}
JumpToElementDialog dialog(diagram_view->diagram(), this);
dialog.exec();
});
}
/**
@@ -862,6 +877,7 @@ void QETDiagramEditor::setUpMenu()
menu_edition -> addActions(m_depth_action_group->actions());
menu_edition -> addSeparator();
menu_edition -> addAction(m_find);
menu_edition -> addAction(m_jump_to_element);
// menu Projet
menu_project -> addAction(m_project_edit_properties);
@@ -1169,6 +1185,13 @@ bool QETDiagramEditor::openAndAddProject(
//Create the project
DialogWaiting::instance(this);
//Per-project window for the font counters reported below; the folios
//(and with them the stored font descriptions) are built between here
//and the end of addProject(). RAII, because DialogWaiting pumps the
//event loop during the load: a nested openAndAddProject() gets its
//own window and this one resumes unharmed.
QETUtils::FontRestorationScope font_scope;
QETProject *project = new QETProject(filepath);
if (project -> state() != QETProject::Ok)
{
@@ -1195,6 +1218,42 @@ bool QETDiagramEditor::openAndAddProject(
addProject(project);
DialogWaiting::dropInstance();
//Report font descriptions which could not be read as-is (written by
//an incompatible Qt version or corrupted), so the user learns about
//it from somewhere else than the console. See issue #553.
const int salvaged_fonts = font_scope.salvaged();
const int unreadable_fonts = font_scope.unreadable();
if (salvaged_fonts || unreadable_fonts)
{
qInfo().nospace() << "Project font descriptions: "
<< salvaged_fonts << " salvaged from a foreign format, "
<< unreadable_fonts << " unreadable (default font applies)";
}
if (interactive && (salvaged_fonts || unreadable_fonts))
{
QStringList details;
if (salvaged_fonts) {
details << tr("%n description(s) de police écrite(s) dans un "
"format étranger ou corrompu ont été restaurée(s). "
"Elles seront réécrites dans un format stable au "
"prochain enregistrement du projet.",
"message box content",
salvaged_fonts);
}
if (unreadable_fonts) {
details << tr("%n description(s) de police n'ont pas pu être "
"lue(s) ; la police par défaut sera utilisée pour "
"ces textes.",
"message box content",
unreadable_fonts);
}
QET::QetMessageBox::information(
this,
tr("Polices du projet", "message box title"),
details.join("\n\n")
);
}
BackupDialog backup_dialog(this);
if (backup_dialog.exec() == QDialog::Accepted)
{
+2 -1
View File
@@ -222,7 +222,8 @@ class QETDiagramEditor : public QETMainWindow
*m_close_file, ///< Close current project file
*m_save_file, ///< Save current project
*m_save_file_as, ///< Save current project as a specific file
*m_find = nullptr;
*m_find = nullptr,
*m_jump_to_element = nullptr; ///< Open the "jump to element" quick-open popup
QList <QAction *> m_zoom_action_toolBar; ///Only zoom action must displayed in the toolbar
+86 -20
View File
@@ -1396,6 +1396,27 @@ void Element::setElementInformations(DiagramContext dc)
m_data.m_informations.addValue(QStringLiteral("label"), actual_label); //Update the label if there is a formula
}
emit elementInfoChange(old_info, m_data.m_informations);
// Propagate label change to linked PLC slaves (label is changed via
// setElementInformations through the undo stack, not via setElementData)
if (m_data.m_type == ElementData::Master && m_data.m_master_type == ElementData::PLC)
{
if (!m_group_index_map.isEmpty())
{
const QString new_label = actualLabel();
for (auto it = m_group_index_map.constBegin(); it != m_group_index_map.constEnd(); ++it)
{
Element *slave = it.key();
if (!slave)
continue;
if (slave->elementInformations().value(QETInformation::ELMT_LABEL).toString() == new_label)
continue;
DiagramContext ctx = slave->elementInformations();
ctx.addValue(QETInformation::ELMT_LABEL, new_label);
slave->setElementInformations(ctx);
}
}
}
}
/**
@@ -1433,7 +1454,9 @@ void Element::setElementData(ElementData data)
{
const auto &new_plc = m_data.plcMasterData();
bool plc_changed = (old_plc.ios != new_plc.ios);
if (plc_changed && !m_group_index_map.isEmpty())
bool label_changed = (old_info.value(QStringLiteral("label")) !=
m_data.m_informations.value(QStringLiteral("label")));
if (!m_group_index_map.isEmpty() && (plc_changed || label_changed))
{
for (auto it = m_group_index_map.constBegin(); it != m_group_index_map.constEnd(); ++it)
{
@@ -1442,24 +1465,64 @@ void Element::setElementData(ElementData data)
if (!slave || io_idx < 0 || io_idx >= new_plc.ios.size())
continue;
const auto &io = new_plc.ios.at(io_idx);
DiagramContext ctx = slave->elementInformations();
ctx.addValue(QETInformation::ELMT_PLC_TYPE,
ElementData::translatedPlcIOType(io.type));
ctx.addValue(QETInformation::ELMT_PLC_ADDRESS, io.address);
ctx.addValue(QETInformation::ELMT_PLC_FUNCTION, io.functionText);
ctx.addValue(QETInformation::ELMT_PLC_COMMENT, io.comment);
ctx.addValue(QETInformation::ELMT_PLC_CROSSREF,
[&]() -> QString {
if (!diagram() || !diagram()->project())
return QString();
XRefProperties xrp = diagram()->project()
->defaultXRefProperties("plc");
autonum::sequentialNumbers seq;
return autonum::AssignVariables::formulaToLabel(
xrp.slaveLabel(), seq, diagram(), this);
}());
ctx.addValue(QETInformation::ELMT_LABEL, actualLabel());
slave->setElementInformations(ctx);
if (plc_changed)
{
DiagramContext ctx = slave->elementInformations();
ctx.addValue(QETInformation::ELMT_PLC_TYPE,
ElementData::translatedPlcIOType(io.type));
ctx.addValue(QETInformation::ELMT_PLC_ADDRESS, io.address);
ctx.addValue(QETInformation::ELMT_PLC_FUNCTION, io.functionText);
ctx.addValue(QETInformation::ELMT_PLC_COMMENT, io.comment);
ctx.addValue(QETInformation::ELMT_PLC_CROSSREF,
[&]() -> QString {
if (!diagram() || !diagram()->project())
return QString();
XRefProperties xrp = diagram()->project()
->defaultXRefProperties("plc");
autonum::sequentialNumbers seq;
return autonum::AssignVariables::formulaToLabel(
xrp.slaveLabel(), seq, diagram(), this);
}());
ctx.addValue(QETInformation::ELMT_LABEL, actualLabel());
ctx.addValue(QETInformation::ELMT_PLC_TC,
QString::number(io.terminalCount));
for (int t = 0; t < io.terminalCount && t < 4; ++t)
{
QString val = (t < io.terminals.size())
? io.terminals.at(t) : QString();
ctx.addValue(
QStringList({
QETInformation::ELMT_PLC_T1,
QETInformation::ELMT_PLC_T2,
QETInformation::ELMT_PLC_T3,
QETInformation::ELMT_PLC_T4
}).at(t), val);
}
slave->setElementInformations(ctx);
// Update master labels on slave terminals
QList<Terminal *> slave_terms = slave->terminals();
for (int t = 0; t < slave_terms.size(); ++t)
{
if (t < io.terminals.size())
{
slave_terms.at(t)->setUseMasterLabel(true);
slave_terms.at(t)->setMasterLabelIndex(t);
}
else
{
slave_terms.at(t)->setUseMasterLabel(false);
}
}
}
if (label_changed)
{
// Only label changed, update the label on the slave
DiagramContext ctx = slave->elementInformations();
ctx.addValue(QETInformation::ELMT_LABEL, actualLabel());
slave->setElementInformations(ctx);
}
}
}
}
@@ -1900,7 +1963,10 @@ void Element::drawPlcTable(QPainter *painter)
}
QRectF text_rect = cr.adjusted(1, 0, -1, 0);
painter->drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, cell_text);
painter->save();
painter->setClipRect(text_rect, Qt::IntersectClip);
painter->drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, cell_text);
painter->restore();
cx += col_widths[col];
}
+40 -9
View File
@@ -289,8 +289,17 @@ void Terminal::paint(
painter->save();
painter->translate(label_pos);
painter->rotate(d->m_label_rotation);
QRectF text_rect(-text_size.width()/2.0, -text_size.height()/2.0,
text_size.width(), text_size.height());
qreal rx = 0, ry = 0;
if (d->m_label_halignment & Qt::AlignLeft) rx = 0;
else if (d->m_label_halignment & Qt::AlignHCenter) rx = -text_size.width() / 2.0;
else if (d->m_label_halignment & Qt::AlignRight) rx = -text_size.width();
if (d->m_label_valignment & Qt::AlignTop) ry = 0;
else if (d->m_label_valignment & Qt::AlignVCenter) ry = -text_size.height() / 2.0;
else if (d->m_label_valignment & Qt::AlignBottom) ry = -text_size.height();
QRectF text_rect(QPointF(rx, ry), text_size);
painter->drawText(text_rect, static_cast<int>(d->m_label_halignment | d->m_label_valignment), display_name);
painter->restore();
} else {
@@ -307,7 +316,7 @@ void Terminal::paint(
if (d->m_label_frame) {
painter->drawRect(text_rect.adjusted(-1, -1, 1, 1));
}
painter->drawText(text_rect, static_cast<int>(Qt::AlignLeft | Qt::AlignTop), display_name);
painter->drawText(text_rect, static_cast<int>(d->m_label_halignment | d->m_label_valignment), display_name);
}
}
@@ -816,12 +825,24 @@ QString Terminal::name() const
if (elmt->linkType() == Element::Master) {
int group_idx = elmt->groupIndexForElement(parent_element_);
if (group_idx >= 0) {
const auto &groups = elmt->elementData().m_slave_contact_groups;
if (group_idx < groups.size()) {
int label_idx = d->m_master_label_index;
const QStringList &labels = groups.at(group_idx).labels;
if (label_idx >= 0 && label_idx < labels.size()) {
return labels.at(label_idx);
// For PLC masters, use io.terminals as labels
if (elmt->elementData().m_master_type == ElementData::PLC) {
const auto &plc_data = elmt->elementData().plcMasterData();
if (group_idx < plc_data.ios.size()) {
int label_idx = d->m_master_label_index;
const QStringList &labels = plc_data.ios.at(group_idx).terminals;
if (label_idx >= 0 && label_idx < labels.size()) {
return labels.at(label_idx);
}
}
} else {
const auto &groups = elmt->elementData().m_slave_contact_groups;
if (group_idx < groups.size()) {
int label_idx = d->m_master_label_index;
const QStringList &labels = groups.at(group_idx).labels;
if (label_idx >= 0 && label_idx < labels.size()) {
return labels.at(label_idx);
}
}
}
}
@@ -832,6 +853,16 @@ QString Terminal::name() const
return d->m_name;
}
/**
@brief Terminal::baseName
Return the original terminal name (T1, T2...) without master label override.
Used for sorting when linking.
*/
QString Terminal::baseName() const
{
return d->m_name;
}
/**
@brief Terminal::terminalType
@return the type of this terminal (Generic, Inner, Outer, No, Nc, Common)
+1
View File
@@ -76,6 +76,7 @@ class Terminal : public QGraphicsObject
Element *parentElement () const;
QUuid uuid () const;
QString name () const;
QString baseName () const;
TerminalData::Type terminalType() const;
bool useMasterLabel() const { return d->m_use_master_label; }
void setUseMasterLabel(bool use);
+5
View File
@@ -130,6 +130,11 @@ namespace QETInformation
static QString ELMT_PLC_FUNCTION = "plc_function";
static QString ELMT_PLC_COMMENT = "plc_comment";
static QString ELMT_PLC_CROSSREF = "plc_crossref";
static QString ELMT_PLC_TC = "plc_tc";
static QString ELMT_PLC_T1 = "plc_t1";
static QString ELMT_PLC_T2 = "plc_t2";
static QString ELMT_PLC_T3 = "plc_t3";
static QString ELMT_PLC_T4 = "plc_t4";
+198
View File
@@ -0,0 +1,198 @@
/*
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 "jumptoelementdialog.h"
#include "../diagram.h"
#include "../qetgraphicsitem/element.h"
#include <QEvent>
#include <QKeyEvent>
#include <QLineEdit>
#include <QListWidget>
#include <QVBoxLayout>
/**
@brief JumpToElementDialog::JumpToElementDialog
@param diagram : the diagram whose elements can be jumped to
@param parent
*/
JumpToElementDialog::JumpToElementDialog(Diagram *diagram, QWidget *parent) :
QDialog(parent),
m_diagram(diagram)
{
setWindowTitle(tr("Atteindre un élément", "window title"));
m_filter_edit = new QLineEdit(this);
m_filter_edit->setPlaceholderText(tr("Nom, label ou information de l'élément…"));
m_filter_edit->installEventFilter(this);
m_result_list = new QListWidget(this);
m_result_list->setFocusPolicy(Qt::NoFocus);
auto *layout = new QVBoxLayout(this);
layout->addWidget(m_filter_edit);
layout->addWidget(m_result_list);
setLayout(layout);
resize(420, 320);
connect(m_filter_edit, &QLineEdit::textChanged, this, &JumpToElementDialog::updateFilteredList);
connect(m_result_list, &QListWidget::itemActivated, this, &JumpToElementDialog::activateCurrentItem);
buildCandidates();
updateFilteredList(QString());
m_filter_edit->setFocus();
}
JumpToElementDialog::~JumpToElementDialog()
{
}
/**
@brief JumpToElementDialog::buildCandidates
Collect every element on m_diagram into m_candidates, along with the
searchable text used to filter it (its type name, its label, and its
other element informations).
*/
void JumpToElementDialog::buildCandidates()
{
m_candidates.clear();
if (!m_diagram) {
return;
}
for (auto item : m_diagram->items()) {
Element *element = qgraphicsitem_cast<Element *>(item);
if (!element) {
continue;
}
const QString label = element->elementInformations().value(QStringLiteral("label")).toString();
const QString name = element->name();
Candidate candidate;
candidate.element = element;
candidate.display_text = label.isEmpty() ? name : (label + QStringLiteral(" — ") + name);
QStringList search_parts;
search_parts << label << name;
const DiagramContext infos = element->elementInformations();
for (const QString &key : infos.keys()) {
search_parts << infos.value(key).toString();
}
candidate.search_text = search_parts.join(QLatin1Char(' ')).toLower();
m_candidates << candidate;
}
}
/**
@brief JumpToElementDialog::updateFilteredList
Refill m_result_list with every candidate whose search text contains
@a filter_text (case-insensitive), and select the first match.
@param filter_text
*/
void JumpToElementDialog::updateFilteredList(const QString &filter_text)
{
m_result_list->clear();
const QString needle = filter_text.trimmed().toLower();
for (int i = 0; i < m_candidates.size(); ++i) {
const Candidate &candidate = m_candidates.at(i);
if (!candidate.element) {
continue;
}
if (!needle.isEmpty() && !candidate.search_text.contains(needle)) {
continue;
}
auto *list_item = new QListWidgetItem(candidate.display_text, m_result_list);
list_item->setData(Qt::UserRole, i);
}
if (m_result_list->count() > 0) {
m_result_list->setCurrentRow(0);
}
}
/**
@brief JumpToElementDialog::activateCurrentItem
Select the element corresponding to the currently highlighted result
on the diagram, scroll it into view, and close this dialog.
*/
void JumpToElementDialog::activateCurrentItem()
{
QListWidgetItem *current = m_result_list->currentItem();
if (!current || !m_diagram) {
reject();
return;
}
const int index = current->data(Qt::UserRole).toInt();
if (index < 0 || index >= m_candidates.size()) {
reject();
return;
}
Element *element = m_candidates.at(index).element;
if (!element) {
reject();
return;
}
m_diagram->clearSelection();
element->setSelected(true);
element->ensureVisible();
accept();
}
/**
@brief JumpToElementDialog::eventFilter
Redirect Up/Down/Enter/Escape typed in the filter field to the result
list, so the user never has to leave the text field to navigate or
confirm a choice.
*/
bool JumpToElementDialog::eventFilter(QObject *watched, QEvent *event)
{
if (watched == m_filter_edit && event->type() == QEvent::KeyPress) {
auto *key_event = static_cast<QKeyEvent *>(event);
switch (key_event->key()) {
case Qt::Key_Down:
case Qt::Key_Up: {
const int row_count = m_result_list->count();
if (row_count == 0) {
return true;
}
int row = m_result_list->currentRow();
row = key_event->key() == Qt::Key_Down
? (row + 1) % row_count
: (row - 1 + row_count) % row_count;
m_result_list->setCurrentRow(row);
return true;
}
case Qt::Key_Return:
case Qt::Key_Enter:
activateCurrentItem();
return true;
case Qt::Key_Escape:
reject();
return true;
default:
break;
}
}
return QDialog::eventFilter(watched, event);
}
+67
View File
@@ -0,0 +1,67 @@
/*
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 JUMPTOELEMENTDIALOG_H
#define JUMPTOELEMENTDIALOG_H
#include <QDialog>
#include <QPointer>
class Diagram;
class Element;
class QLineEdit;
class QListWidget;
/**
@brief The JumpToElementDialog class
A lightweight, transient "quick open" popup: type part of an element's
label or other information to live-filter the elements on a diagram,
then Enter to select the chosen element on the diagram and scroll it
into view. Up/Down move through the filtered list, Escape cancels
without changing the current selection.
*/
class JumpToElementDialog : public QDialog
{
Q_OBJECT
public:
explicit JumpToElementDialog(Diagram *diagram, QWidget *parent = nullptr);
~JumpToElementDialog() override;
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private slots:
void updateFilteredList(const QString &filter_text);
void activateCurrentItem();
private:
void buildCandidates();
struct Candidate {
QPointer<Element> element;
QString display_text;
QString search_text;
};
QPointer<Diagram> m_diagram;
QList<Candidate> m_candidates;
QLineEdit *m_filter_edit;
QListWidget *m_result_list;
};
#endif // JUMPTOELEMENTDIALOG_H
+38 -1
View File
@@ -216,6 +216,16 @@ void MasterPropertiesWidget::reset()
*/
QUndoCommand* MasterPropertiesWidget::associatedUndo() const
{
// PLC masters manage their slave links via the IO table (setElementData),
// not via the link tree widget. The link tree is always empty for PLC
// masters, so we must not create an unlinkAll command.
if (m_element &&
m_element->elementData().m_type == ElementData::Master &&
m_element->elementData().m_master_type == ElementData::PLC)
{
return nullptr;
}
QList <Element *> to_link;
QList <Element *> linked_ = m_element->linkedElements();
@@ -880,6 +890,8 @@ void MasterPropertiesWidget::plcRemoveRow()
if (selected.isEmpty())
return;
m_plc_updating = true;
// Remove from bottom to top to preserve indices
std::sort(selected.begin(), selected.end(),
[](const QModelIndex &a, const QModelIndex &b) { return a.row() > b.row(); });
@@ -887,6 +899,9 @@ void MasterPropertiesWidget::plcRemoveRow()
for (const QModelIndex &idx : selected) {
m_plc_table->removeRow(idx.row());
}
m_plc_updating = false;
plcUpdateDisplaySettings();
}
/**
@@ -902,6 +917,8 @@ void MasterPropertiesWidget::plcMoveRowUp()
if (row <= 0)
return;
m_plc_updating = true;
// Swap with row above
for (int col = 0; col < m_plc_table->columnCount(); ++col) {
QWidget *w1 = m_plc_table->cellWidget(row, col);
@@ -916,6 +933,8 @@ void MasterPropertiesWidget::plcMoveRowUp()
}
m_plc_table->setCurrentCell(row - 1, m_plc_table->currentColumn());
m_plc_updating = false;
plcUpdateDisplaySettings();
}
/**
@@ -931,6 +950,8 @@ void MasterPropertiesWidget::plcMoveRowDown()
if (row < 0 || row >= m_plc_table->rowCount() - 1)
return;
m_plc_updating = true;
// Swap with row below
for (int col = 0; col < m_plc_table->columnCount(); ++col) {
QWidget *w1 = m_plc_table->cellWidget(row, col);
@@ -945,6 +966,8 @@ void MasterPropertiesWidget::plcMoveRowDown()
}
m_plc_table->setCurrentCell(row + 1, m_plc_table->currentColumn());
m_plc_updating = false;
plcUpdateDisplaySettings();
}
/**
@@ -978,7 +1001,14 @@ void MasterPropertiesWidget::plcUpdateDisplaySettings()
ElementData::PlcMasterData plc_data = ed.plcMasterData();
plc_data.ios.clear();
// Read IOs from table
// Build address -> original IO lookup to correctly reattach terminal data
// after row reorder (move up/down) or row removal
QHash<QString, int> addr_to_orig_idx;
for (int i = 0; i < ed.plcMasterData().ios.size(); ++i) {
addr_to_orig_idx[ed.plcMasterData().ios.at(i).address] = i;
}
// Read IOs from table, preserving terminal data from original IOs
for (int row = 0; row < m_plc_table->rowCount(); ++row) {
ElementData::PlcIO io;
@@ -1002,6 +1032,13 @@ void MasterPropertiesWidget::plcUpdateDisplaySettings()
if (crossref_item)
io.crossRef = crossref_item->text();
// Preserve terminal data by looking up original IO via address
if (addr_to_orig_idx.contains(io.address)) {
const auto &orig_io = ed.plcMasterData().ios.at(addr_to_orig_idx.value(io.address));
io.terminalCount = orig_io.terminalCount;
io.terminals = orig_io.terminals;
}
plc_data.ios.append(io);
}
+51 -13
View File
@@ -406,22 +406,46 @@ void LinkElementCommand::makeLink(const QList<Element *> &element_list)
elmt->setGroupIndexForElement(m_element, group_idx);
// Set master labels on slave terminals
const auto &groups = elmt->elementData().m_slave_contact_groups;
if (group_idx < groups.size())
if (elmt->elementData().m_master_type == ElementData::PLC)
{
const QStringList &labels = groups.at(group_idx).labels;
QList<Terminal *> slave_terms = m_element->terminals();
// Sort terminals by name (T1, T2, T3...) to match label order
std::sort(slave_terms.begin(), slave_terms.end(),
[](Terminal *a, Terminal *b) {
return a->name() < b->name();
});
for (int i = 0; i < slave_terms.size(); ++i)
// For PLC masters, use io.terminals as labels
const auto &plc_data = elmt->elementData().plcMasterData();
if (group_idx < plc_data.ios.size())
{
if (i < labels.size())
const QStringList &labels = plc_data.ios.at(group_idx).terminals;
QList<Terminal *> slave_terms = m_element->terminals();
std::sort(slave_terms.begin(), slave_terms.end(),
[](Terminal *a, Terminal *b) {
return a->baseName() < b->baseName();
});
for (int i = 0; i < slave_terms.size(); ++i)
{
slave_terms.at(i)->setUseMasterLabel(true);
slave_terms.at(i)->setMasterLabelIndex(i);
if (i < labels.size())
{
slave_terms.at(i)->setUseMasterLabel(true);
slave_terms.at(i)->setMasterLabelIndex(i);
}
}
}
}
else
{
const auto &groups = elmt->elementData().m_slave_contact_groups;
if (group_idx < groups.size())
{
const QStringList &labels = groups.at(group_idx).labels;
QList<Terminal *> slave_terms = m_element->terminals();
std::sort(slave_terms.begin(), slave_terms.end(),
[](Terminal *a, Terminal *b) {
return a->baseName() < b->baseName();
});
for (int i = 0; i < slave_terms.size(); ++i)
{
if (i < labels.size())
{
slave_terms.at(i)->setUseMasterLabel(true);
slave_terms.at(i)->setMasterLabelIndex(i);
}
}
}
}
@@ -443,6 +467,20 @@ void LinkElementCommand::makeLink(const QList<Element *> &element_list)
plcCrossRefText(elmt, m_element));
ctx.addValue(QETInformation::ELMT_LABEL,
elmt->actualLabel());
ctx.addValue(QETInformation::ELMT_PLC_TC,
QString::number(io.terminalCount));
for (int t = 0; t < io.terminalCount && t < 4; ++t)
{
QString val = (t < io.terminals.size())
? io.terminals.at(t) : QString();
ctx.addValue(
QStringList({
QETInformation::ELMT_PLC_T1,
QETInformation::ELMT_PLC_T2,
QETInformation::ELMT_PLC_T3,
QETInformation::ELMT_PLC_T4
}).at(t), val);
}
m_element->setElementInformations(ctx);
}
}
+55
View File
@@ -153,6 +153,12 @@ void QETUtils::pixelSizedFont(QFont &font)
namespace
{
/* Counters for fontFromString(), reset per project load so the editor
* can report how many stored font descriptions needed salvaging or were
* unreadable. Font parsing only happens on the main thread. */
int salvaged_font_count = 0;
int unreadable_font_count = 0;
/**
* Legacy (Qt 5) weight <- OpenType weight, closest match,
* same table Qt uses when parsing a 10/11 field string.
@@ -235,6 +241,10 @@ QString QETUtils::fontToString(const QFont &font)
*/
bool QETUtils::fontFromString(QFont &font, const QString &description)
{
if (description.trimmed().isEmpty()) {
return false;
}
QFont parsed(font);
if (parsed.fromString(description)) {
font = parsed;
@@ -271,8 +281,10 @@ bool QETUtils::fontFromString(QFont &font, const QString &description)
if (parsed.fromString(legacy)) {
font = parsed;
++salvaged_font_count;
return true;
}
++unreadable_font_count;
return false;
}
@@ -284,7 +296,50 @@ bool QETUtils::fontFromString(QFont &font, const QString &description)
if (count > 11
&& parsed.fromString(QStringList(l.mid(0, 11)).join(comma))) {
font = parsed;
++salvaged_font_count;
return true;
}
++unreadable_font_count;
return false;
}
/**
* @brief QETUtils::FontRestorationScope::FontRestorationScope
* Open a fresh counting window: the enclosing window's counts are kept
* aside and restored by the destructor, so a project load nested inside
* another one (through the event loop) reports its own numbers only.
*/
QETUtils::FontRestorationScope::FontRestorationScope() :
m_outer_salvaged(salvaged_font_count),
m_outer_unreadable(unreadable_font_count)
{
salvaged_font_count = 0;
unreadable_font_count = 0;
}
QETUtils::FontRestorationScope::~FontRestorationScope()
{
salvaged_font_count = m_outer_salvaged;
unreadable_font_count = m_outer_unreadable;
}
/**
* @brief QETUtils::FontRestorationScope::salvaged
* @return How many font descriptions fontFromString() restored from a
* foreign or corrupt format since this window was opened. Such descriptions
* are rewritten in the stable format on the next save.
*/
int QETUtils::FontRestorationScope::salvaged() const
{
return salvaged_font_count;
}
/**
* @brief QETUtils::FontRestorationScope::unreadable
* @return How many font descriptions fontFromString() could not restore at
* all since this window was opened (the caller's default font applies).
*/
int QETUtils::FontRestorationScope::unreadable() const
{
return unreadable_font_count;
}
+23
View File
@@ -35,6 +35,29 @@ namespace QETUtils
QString fontToString (const QFont &font);
bool fontFromString (QFont &font, const QString &description);
/**
RAII counting window for the font descriptions fontFromString()
salvages or fails to read: construction opens a fresh window, the
destructor restores the enclosing one. Windows nest strictly LIFO,
which covers project loads re-entered through the event loop
(DialogWaiting pumps it while the folios are built).
*/
class FontRestorationScope
{
public:
FontRestorationScope();
~FontRestorationScope();
FontRestorationScope(const FontRestorationScope &) = delete;
FontRestorationScope &operator=(const FontRestorationScope &) = delete;
int salvaged() const;
int unreadable() const;
private:
int m_outer_salvaged;
int m_outer_unreadable;
};
bool sortBeginIntString(const QString &str_a, const QString &str_b);
template <typename T>