diff --git a/ico/format-apply.svg b/ico/format-apply.svg new file mode 100644 index 000000000..79320bed8 --- /dev/null +++ b/ico/format-apply.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/ico/format-copy.svg b/ico/format-copy.svg new file mode 100644 index 000000000..033e2eeb3 --- /dev/null +++ b/ico/format-copy.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/lang/qet_de.ts b/lang/qet_de.ts index a98faa095..00e7ead74 100644 --- a/lang/qet_de.ts +++ b/lang/qet_de.ts @@ -11805,6 +11805,41 @@ Verfügbare Optionen: QETDiagramEditor + + + Copy text format + Beschriftungsart kopieren + + + + Apply text format + Beschriftungsart zuweisen + + + + Copy the text format of the selected element as a template + status bar tip + Kopiert das Beschriftungsformat des ausgewählten Elements als Vorlage + + + + Apply the copied text format to the selected elements + status bar tip + Weist das kopierte Beschriftungsformat den markierten Elementen zu + + + + Text format of "%1" copied + Beschriftungsart von „%1“ kopiert + + + + Text format applied to %n element(s) + + Beschriftungsart auf %n Element zugewiesen + Beschriftungsart auf %n Elemente zugewiesen + + @@ -15138,6 +15173,11 @@ Was möchten Sie tun? QObject + + + Apply text format + Beschriftungsart zuweisen + diff --git a/lang/qet_fr.ts b/lang/qet_fr.ts index 4f4b125bf..c01a76d89 100644 --- a/lang/qet_fr.ts +++ b/lang/qet_fr.ts @@ -11817,6 +11817,41 @@ Options disponibles : QETDiagramEditor + + + Copy text format + Copier le style de texte + + + + Apply text format + Appliquer le style de texte + + + + Copy the text format of the selected element as a template + status bar tip + Copie le format de texte de l'élément sélectionné comme modèle + + + + Apply the copied text format to the selected elements + status bar tip + Applique le format de texte copié aux éléments sélectionnés + + + + Text format of "%1" copied + Style de texte de « %1 » copié + + + + Text format applied to %n element(s) + + Style de texte appliqué à %n élément + Style de texte appliqué à %n éléments + + @@ -15154,6 +15189,11 @@ Que désirez vous faire ? QObject + + + Apply text format + Appliquer le style de texte + %n elements diff --git a/qelectrotech.qrc b/qelectrotech.qrc index cee4ba607..4b3ecff23 100644 --- a/qelectrotech.qrc +++ b/qelectrotech.qrc @@ -467,5 +467,7 @@ licenses/QElectroTech.LICENSE licenses/QET-Elements.NOTICE licenses/QET-Elements.LICENSE + ico/format-copy.svg + ico/format-apply.svg diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index 548606aa0..3cc9a7352 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -2119,6 +2119,8 @@ QList DiagramView::contextMenuActions() const list << qde->m_cut; list << qde->m_copy; list << m_multi_paste; + list << qde->m_copy_text_format; + list << qde->m_apply_text_format; list << qde->m_paste_element_info; list << m_separators.at(0); list << m_create_template; // Add the create template action diff --git a/sources/elementtextpattern.cpp b/sources/elementtextpattern.cpp index dfb594ba0..0e9be6bf5 100644 --- a/sources/elementtextpattern.cpp +++ b/sources/elementtextpattern.cpp @@ -26,6 +26,7 @@ #include "ui/importelementtextpatterndialog.h" #include "undocommand/addelementtextcommand.h" #include "undocommand/deleteqgraphicsitemcommand.h" +#include "shownkinds.h" #include #include @@ -272,3 +273,635 @@ void ImportElementTextPattern::apply(QString name, bool erase) const undo_stack.endMacro(); } + +//============================================================================= +// ElementTextFormatPattern & ApplyElementTextFormatCommand (Format Painter) +//============================================================================= + +#include "terminal.h" + +ElementTextFormatPattern ElementTextFormatPattern::s_current_pattern; +bool ElementTextFormatPattern::s_has_pattern = false; + +bool ElementTextFormatPattern::hasPattern() +{ + return s_has_pattern && s_current_pattern.isValid(); +} + +const ElementTextFormatPattern &ElementTextFormatPattern::currentPattern() +{ + return s_current_pattern; +} + +void ElementTextFormatPattern::setCurrentPattern(const ElementTextFormatPattern &pattern) +{ + s_current_pattern = pattern; + s_has_pattern = pattern.isValid(); +} + +ElementTextFormatPattern ElementTextFormatPattern::fromElement(Element *element) +{ + ElementTextFormatPattern pattern; + if (!element) return pattern; + + pattern.m_source_element_name = element->name(); + + // 1. Capture groups + for (ElementTextItemGroup *group : element->textGroups()) { + TextGroupFormatItem gItem; + gItem.name = group->name(); + gItem.pos = group->pos(); + gItem.rotation = group->rotation(); + gItem.alignment = group->alignment(); + gItem.verticalAdjustment = group->verticalAdjustment(); + gItem.frame = group->frame(); + gItem.holdToBottomPage = group->holdToBottomPage(); + pattern.m_groups.append(gItem); + } + + // 2. Capture all dynamic texts + int idx = 0; + // Ungrouped texts + for (DynamicElementTextItem *deti : element->dynamicTextItems()) { + DynamicTextFormatItem tItem; + tItem.textFrom = deti->textFrom(); + tItem.infoName = deti->infoName(); + tItem.compositeText = deti->toPlainText(); + tItem.font = deti->font(); + tItem.color = deti->color(); + tItem.alignment = deti->alignment(); + tItem.pos = deti->pos(); + tItem.rotation = deti->rotation(); + tItem.keepVisualRotation = deti->keepVisualRotation(); + tItem.rotationPointCenter = deti->rotationPointCenter(); + tItem.frame = deti->frame(); + tItem.textWidth = deti->textWidth(); + tItem.visible = ShownKinds::wantsVisible(deti); + tItem.groupName = QString(); + tItem.index = idx++; + tItem.isTerminalText = isTerminalTextItem(deti, element); + pattern.m_text_items.append(tItem); + } + + // Grouped texts + for (ElementTextItemGroup *group : element->textGroups()) { + for (DynamicElementTextItem *deti : group->texts()) { + DynamicTextFormatItem tItem; + tItem.textFrom = deti->textFrom(); + tItem.infoName = deti->infoName(); + tItem.compositeText = deti->toPlainText(); + tItem.font = deti->font(); + tItem.color = deti->color(); + tItem.alignment = deti->alignment(); + tItem.pos = deti->pos(); + tItem.rotation = deti->rotation(); + tItem.keepVisualRotation = deti->keepVisualRotation(); + tItem.rotationPointCenter = deti->rotationPointCenter(); + tItem.frame = deti->frame(); + tItem.textWidth = deti->textWidth(); + tItem.visible = ShownKinds::wantsVisible(deti); + tItem.groupName = group->name(); + tItem.index = idx++; + tItem.isTerminalText = isTerminalTextItem(deti, element); + pattern.m_text_items.append(tItem); + } + } + + return pattern; +} + +bool ElementTextFormatPattern::hasTerminalTextFormat() const +{ + for (const DynamicTextFormatItem &item : m_text_items) { + if (item.isTerminalText) return true; + } + return false; +} + +DynamicTextFormatItem ElementTextFormatPattern::terminalTextFormat() const +{ + for (const DynamicTextFormatItem &item : m_text_items) { + if (item.isTerminalText) return item; + } + return DynamicTextFormatItem(); +} + +bool ElementTextFormatPattern::isTerminalTextItem(const DynamicElementTextItem *deti, const Element *element) +{ + if (!deti || !element) return false; + if (deti->textFrom() != DynamicElementTextItem::UserText) return false; + + const QString text = deti->toPlainText().trimmed(); + if (text.isEmpty()) return false; + + const QList terms = element->terminals(); + for (const Terminal *t : terms) { + if (!t) continue; + if (t->name().trimmed() == text) { + return true; + } + } + + // Secondary check: text is short (<= 6 chars, typical pin designation) + // and located close to a terminal (within 40 pixels) + if (text.length() <= 6) { + for (const Terminal *t : terms) { + if (!t) continue; + QPointF diff = deti->pos() - t->pos(); + if ((diff.x() * diff.x() + diff.y() * diff.y()) <= (40.0 * 40.0)) { + return true; + } + } + } + + return false; +} + +ApplyElementTextFormatCommand::ApplyElementTextFormatCommand( + Element *element, + const ElementTextFormatPattern &pattern, + QUndoCommand *parent) + : ApplyElementTextFormatCommand(QList{element}, pattern, parent) +{ +} + +ApplyElementTextFormatCommand::ApplyElementTextFormatCommand( + const QList &elements, + const ElementTextFormatPattern &pattern, + QUndoCommand *parent) + : QUndoCommand(parent) +{ + setText(QObject::tr("Apply text format")); + + for (Element *element : elements) { + if (!element) continue; + + ElementFormatChange elChange; + elChange.element = element; + + // Collect all existing texts on target element + QList targetTexts; + targetTexts << element->dynamicTextItems(); + for (ElementTextItemGroup *g : element->textGroups()) { + targetTexts << g->texts(); + } + + // Setup group changes + for (const TextGroupFormatItem &gItem : pattern.groups()) { + ElementFormatChange::GroupChange gc; + gc.name = gItem.name; + ElementTextItemGroup *existingGroup = element->textGroup(gItem.name); + if (existingGroup) { + gc.group = existingGroup; + gc.wasCreated = false; + gc.oldPos = existingGroup->pos(); + gc.oldRotation = existingGroup->rotation(); + gc.oldAlignment = existingGroup->alignment(); + gc.oldVAdjust = existingGroup->verticalAdjustment(); + gc.oldFrame = existingGroup->frame(); + gc.oldHoldBottom = existingGroup->holdToBottomPage(); + } else { + gc.group = nullptr; + gc.wasCreated = true; + } + gc.newPos = gItem.pos; + gc.newRotation = gItem.rotation; + gc.newAlignment = gItem.alignment; + gc.newVAdjust = gItem.verticalAdjustment; + gc.newFrame = gItem.frame; + gc.newHoldBottom = gItem.holdToBottomPage; + elChange.groupChanges.append(gc); + } + + // Text matching + QSet matchedTarget; + QSet matchedPatternIndices; + + // Identify terminal / pole texts on the target element + QSet targetTerminalTexts; + for (DynamicElementTextItem *tDeti : targetTexts) { + if (ElementTextFormatPattern::isTerminalTextItem(tDeti, element)) { + targetTerminalTexts.insert(tDeti); + } + } + + // Handle Terminal / Pole texts: + // They belong to physical terminals/poles of the element (e.g. 1..6, A1/A2, 13/14). + // Their positions, rotations, frames and visibilities are NEVER modified! + // If the pattern provides a terminal text style (font, color), apply it uniformly to all terminal texts. + const bool hasTermFmt = pattern.hasTerminalTextFormat(); + const DynamicTextFormatItem termFmt = hasTermFmt ? pattern.terminalTextFormat() : DynamicTextFormatItem(); + + for (DynamicElementTextItem *tDeti : targetTerminalTexts) { + matchedTarget.insert(tDeti); // Exclude from general user text matching + + // Only record a change if font or color actually changes + if (hasTermFmt && (tDeti->font() != termFmt.font || tDeti->color() != termFmt.color)) { + ElementFormatChange::TextChange tc; + tc.deti = tDeti; + tc.wasCreated = false; + tc.oldFont = tDeti->font(); + tc.oldColor = tDeti->color(); + tc.oldAlignment = tDeti->alignment(); + tc.oldPos = tDeti->pos(); + tc.oldRotation = tDeti->rotation(); + tc.oldKeepVisualRotation = tDeti->keepVisualRotation(); + tc.oldRotationPointCenter = tDeti->rotationPointCenter(); + tc.oldFrame = tDeti->frame(); + tc.oldTextWidth = tDeti->textWidth(); + tc.oldVisible = ShownKinds::wantsVisible(tDeti); + tc.oldGroupName = tDeti->parentGroup() ? tDeti->parentGroup()->name() : QString(); + + tc.newFont = termFmt.font; + tc.newColor = termFmt.color; + // Positions, alignments, rotations, frames and visibilities are strictly preserved on terminals! + tc.newAlignment = tc.oldAlignment; + tc.newPos = tc.oldPos; + tc.newRotation = tc.oldRotation; + tc.newKeepVisualRotation = tc.oldKeepVisualRotation; + tc.newRotationPointCenter = tc.oldRotationPointCenter; + tc.newFrame = tc.oldFrame; + tc.newTextWidth = tc.oldTextWidth; + tc.newVisible = tc.oldVisible; // Always retain visibility! + tc.newGroupName = tc.oldGroupName; + + elChange.textChanges.append(tc); + } + } + + // Pass 1: ElementInfo with same infoName + for (int i = 0; i < pattern.textItems().size(); ++i) { + const DynamicTextFormatItem &pItem = pattern.textItems().at(i); + if (pItem.textFrom == DynamicElementTextItem::ElementInfo && !pItem.infoName.isEmpty()) { + for (DynamicElementTextItem *tDeti : targetTexts) { + if (!matchedTarget.contains(tDeti) && + tDeti->textFrom() == DynamicElementTextItem::ElementInfo && + tDeti->infoName() == pItem.infoName) { + + matchedTarget.insert(tDeti); + matchedPatternIndices.insert(i); + + ElementFormatChange::TextChange tc; + tc.deti = tDeti; + tc.wasCreated = false; + tc.oldFont = tDeti->font(); + tc.oldColor = tDeti->color(); + tc.oldAlignment = tDeti->alignment(); + tc.oldPos = tDeti->pos(); + tc.oldRotation = tDeti->rotation(); + tc.oldKeepVisualRotation = tDeti->keepVisualRotation(); + tc.oldRotationPointCenter = tDeti->rotationPointCenter(); + tc.oldFrame = tDeti->frame(); + tc.oldTextWidth = tDeti->textWidth(); + tc.oldVisible = ShownKinds::wantsVisible(tDeti); + tc.oldGroupName = tDeti->parentGroup() ? tDeti->parentGroup()->name() : QString(); + + tc.newFont = pItem.font; + tc.newColor = pItem.color; + tc.newAlignment = pItem.alignment; + tc.newPos = pItem.pos; + tc.newRotation = pItem.rotation; + tc.newKeepVisualRotation = pItem.keepVisualRotation; + tc.newRotationPointCenter = pItem.rotationPointCenter; + tc.newFrame = pItem.frame; + tc.newTextWidth = pItem.textWidth; + tc.newVisible = pItem.visible; + tc.newGroupName = pItem.groupName; + + elChange.textChanges.append(tc); + break; + } + } + } + } + + // Pass 2: UserText (non-terminal texts only) + for (int i = 0; i < pattern.textItems().size(); ++i) { + if (matchedPatternIndices.contains(i)) continue; + const DynamicTextFormatItem &pItem = pattern.textItems().at(i); + if (pItem.isTerminalText) continue; // Skip terminal texts in generic user text matching + if (pItem.textFrom == DynamicElementTextItem::UserText) { + DynamicElementTextItem *foundDeti = nullptr; + for (DynamicElementTextItem *tDeti : targetTexts) { + if (!matchedTarget.contains(tDeti) && + tDeti->textFrom() == DynamicElementTextItem::UserText && + !pItem.compositeText.isEmpty() && + tDeti->toPlainText() == pItem.compositeText) { + foundDeti = tDeti; + break; + } + } + if (!foundDeti) { + for (DynamicElementTextItem *tDeti : targetTexts) { + if (!matchedTarget.contains(tDeti) && + tDeti->textFrom() == DynamicElementTextItem::UserText) { + foundDeti = tDeti; + break; + } + } + } + if (foundDeti) { + matchedTarget.insert(foundDeti); + matchedPatternIndices.insert(i); + + ElementFormatChange::TextChange tc; + tc.deti = foundDeti; + tc.wasCreated = false; + tc.oldFont = foundDeti->font(); + tc.oldColor = foundDeti->color(); + tc.oldAlignment = foundDeti->alignment(); + tc.oldPos = foundDeti->pos(); + tc.oldRotation = foundDeti->rotation(); + tc.oldKeepVisualRotation = foundDeti->keepVisualRotation(); + tc.oldRotationPointCenter = foundDeti->rotationPointCenter(); + tc.oldFrame = foundDeti->frame(); + tc.oldTextWidth = foundDeti->textWidth(); + tc.oldVisible = ShownKinds::wantsVisible(foundDeti); + tc.oldGroupName = foundDeti->parentGroup() ? foundDeti->parentGroup()->name() : QString(); + + tc.newFont = pItem.font; + tc.newColor = pItem.color; + tc.newAlignment = pItem.alignment; + tc.newPos = pItem.pos; + tc.newRotation = pItem.rotation; + tc.newKeepVisualRotation = pItem.keepVisualRotation; + tc.newRotationPointCenter = pItem.rotationPointCenter; + tc.newFrame = pItem.frame; + tc.newTextWidth = pItem.textWidth; + tc.newVisible = pItem.visible; + tc.newGroupName = pItem.groupName; + + elChange.textChanges.append(tc); + } + } + } + + // Pass 3: CompositeText or other unmatched (non-terminal) + for (int i = 0; i < pattern.textItems().size(); ++i) { + if (matchedPatternIndices.contains(i)) continue; + const DynamicTextFormatItem &pItem = pattern.textItems().at(i); + if (pItem.isTerminalText) continue; + DynamicElementTextItem *foundDeti = nullptr; + for (DynamicElementTextItem *tDeti : targetTexts) { + if (!matchedTarget.contains(tDeti) && tDeti->textFrom() == pItem.textFrom) { + foundDeti = tDeti; + break; + } + } + if (foundDeti) { + matchedTarget.insert(foundDeti); + matchedPatternIndices.insert(i); + + ElementFormatChange::TextChange tc; + tc.deti = foundDeti; + tc.wasCreated = false; + tc.oldFont = foundDeti->font(); + tc.oldColor = foundDeti->color(); + tc.oldAlignment = foundDeti->alignment(); + tc.oldPos = foundDeti->pos(); + tc.oldRotation = foundDeti->rotation(); + tc.oldKeepVisualRotation = foundDeti->keepVisualRotation(); + tc.oldRotationPointCenter = foundDeti->rotationPointCenter(); + tc.oldFrame = foundDeti->frame(); + tc.oldTextWidth = foundDeti->textWidth(); + tc.oldVisible = ShownKinds::wantsVisible(foundDeti); + tc.oldGroupName = foundDeti->parentGroup() ? foundDeti->parentGroup()->name() : QString(); + + tc.newFont = pItem.font; + tc.newColor = pItem.color; + tc.newAlignment = pItem.alignment; + tc.newPos = pItem.pos; + tc.newRotation = pItem.rotation; + tc.newKeepVisualRotation = pItem.keepVisualRotation; + tc.newRotationPointCenter = pItem.rotationPointCenter; + tc.newFrame = pItem.frame; + tc.newTextWidth = pItem.textWidth; + tc.newVisible = pItem.visible; + tc.newGroupName = pItem.groupName; + + elChange.textChanges.append(tc); + } + } + + // Pass 4: Pattern ElementInfo items that don't exist in target element + for (int i = 0; i < pattern.textItems().size(); ++i) { + if (matchedPatternIndices.contains(i)) continue; + const DynamicTextFormatItem &pItem = pattern.textItems().at(i); + if (pItem.textFrom == DynamicElementTextItem::ElementInfo && !pItem.infoName.isEmpty()) { + ElementFormatChange::TextChange tc; + tc.deti = nullptr; + tc.wasCreated = true; + tc.newFont = pItem.font; + tc.newColor = pItem.color; + tc.newAlignment = pItem.alignment; + tc.newPos = pItem.pos; + tc.newRotation = pItem.rotation; + tc.newKeepVisualRotation = pItem.keepVisualRotation; + tc.newRotationPointCenter = pItem.rotationPointCenter; + tc.newFrame = pItem.frame; + tc.newTextWidth = pItem.textWidth; + tc.newVisible = pItem.visible; + tc.newGroupName = pItem.groupName; + tc.oldGroupName = pItem.infoName; // infoName stored temporarily in oldGroupName + elChange.textChanges.append(tc); + } + } + + // Pass 5 (Destructive hiding of unmatched texts) is completely removed: + // Non-matching target texts (such as additional poles on 3-pole/4-pole elements) + // are never hidden and retain their exact positions and visibilities! + + m_changes.append(elChange); + } +} + +ApplyElementTextFormatCommand::~ApplyElementTextFormatCommand() +{ + for (ElementFormatChange &elChange : m_changes) { + Element *element = elChange.element.data(); + for (ElementFormatChange::TextChange &tc : elChange.textChanges) { + if (tc.wasCreated && tc.deti) { + DynamicElementTextItem *deti = tc.deti.data(); + if (!deti) continue; + if (deti->parentGroup()) continue; + if (!element || !element->dynamicTextItems().contains(deti)) { + delete deti; + } + } + } + for (ElementFormatChange::GroupChange &gc : elChange.groupChanges) { + if (gc.wasCreated && gc.group) { + ElementTextItemGroup *group = gc.group.data(); + if (!group) continue; + if (!element || !element->textGroups().contains(group)) { + delete group; + } + } + } + } +} + +void ApplyElementTextFormatCommand::redo() +{ + for (ElementFormatChange &elChange : m_changes) { + Element *element = elChange.element.data(); + if (!element) continue; + + // 1. Create and update groups + for (ElementFormatChange::GroupChange &gc : elChange.groupChanges) { + ElementTextItemGroup *group = gc.group.data(); + if (!group) { + group = element->textGroup(gc.name); + if (!group) { + group = element->addTextGroup(gc.name); + } + gc.group = group; + } else { + if (!element->textGroups().contains(group)) { + element->addTextGroup(group); + } + } + if (group) { + group->setPos(gc.newPos); + group->setRotation(gc.newRotation); + group->setAlignment(gc.newAlignment); + group->setVerticalAdjustment(gc.newVAdjust); + group->setFrame(gc.newFrame); + group->setHoldToBottomPage(gc.newHoldBottom); + } + } + + // 2. Apply texts + for (ElementFormatChange::TextChange &tc : elChange.textChanges) { + DynamicElementTextItem *deti = tc.deti.data(); + if (tc.wasCreated) { + if (!deti) { + deti = new DynamicElementTextItem(element); + deti->setTextFrom(DynamicElementTextItem::ElementInfo); + deti->setInfoName(tc.oldGroupName); + element->addDynamicTextItem(deti); + tc.deti = deti; + } else { + deti->setParentItem(element); + if (!element->dynamicTextItems().contains(deti)) { + element->addDynamicTextItem(deti); + } + } + } + if (!deti) continue; + + // Group membership + if (!tc.newGroupName.isEmpty()) { + ElementTextItemGroup *targetGroup = element->textGroup(tc.newGroupName); + if (targetGroup) { + if (deti->parentGroup() != targetGroup) { + if (deti->parentGroup()) { + element->removeTextFromGroup(deti, deti->parentGroup()); + } + element->addTextToGroup(deti, targetGroup); + } + } + } else { + if (deti->parentGroup()) { + element->removeTextFromGroup(deti, deti->parentGroup()); + } + } + + // Apply properties + deti->setFont(tc.newFont); + deti->setColor(tc.newColor); + deti->setAlignment(tc.newAlignment); + deti->setPos(tc.newPos); + deti->setRotation(tc.newRotation); + deti->setKeepVisualRotation(tc.newKeepVisualRotation); + deti->setRotationPointCenter(tc.newRotationPointCenter); + deti->setFrame(tc.newFrame); + deti->setTextWidth(tc.newTextWidth); + ShownKinds::setVisible(deti, tc.newVisible); + deti->update(); + } + + element->update(); + if (element->diagram()) { + element->diagram()->update(); + } + } +} + +void ApplyElementTextFormatCommand::undo() +{ + for (ElementFormatChange &elChange : m_changes) { + Element *element = elChange.element.data(); + if (!element) continue; + + // 1. Restore texts + for (ElementFormatChange::TextChange &tc : elChange.textChanges) { + DynamicElementTextItem *deti = tc.deti.data(); + if (!deti) continue; + + if (tc.wasCreated) { + if (deti->parentGroup()) { + element->removeTextFromGroup(deti, deti->parentGroup()); + } + element->removeDynamicTextItem(deti); + if (deti->scene()) { + deti->scene()->removeItem(deti); + } + continue; + } + + // Restore group membership + if (!tc.oldGroupName.isEmpty()) { + ElementTextItemGroup *origGroup = element->textGroup(tc.oldGroupName); + if (origGroup) { + if (deti->parentGroup() != origGroup) { + if (deti->parentGroup()) { + element->removeTextFromGroup(deti, deti->parentGroup()); + } + element->addTextToGroup(deti, origGroup); + } + } + } else { + if (deti->parentGroup()) { + element->removeTextFromGroup(deti, deti->parentGroup()); + } + } + + // Restore properties + deti->setFont(tc.oldFont); + deti->setColor(tc.oldColor); + deti->setAlignment(tc.oldAlignment); + deti->setPos(tc.oldPos); + deti->setRotation(tc.oldRotation); + deti->setKeepVisualRotation(tc.oldKeepVisualRotation); + deti->setRotationPointCenter(tc.oldRotationPointCenter); + deti->setFrame(tc.oldFrame); + deti->setTextWidth(tc.oldTextWidth); + ShownKinds::setVisible(deti, tc.oldVisible); + deti->update(); + } + + // 2. Restore groups + for (ElementFormatChange::GroupChange &gc : elChange.groupChanges) { + ElementTextItemGroup *group = gc.group.data(); + if (!group) continue; + + if (gc.wasCreated) { + element->removeTextGroup(group); + continue; + } + + group->setPos(gc.oldPos); + group->setRotation(gc.oldRotation); + group->setAlignment(gc.oldAlignment); + group->setVerticalAdjustment(gc.oldVAdjust); + group->setFrame(gc.oldFrame); + group->setHoldToBottomPage(gc.oldHoldBottom); + } + + element->update(); + if (element->diagram()) { + element->diagram()->update(); + } + } +} diff --git a/sources/elementtextpattern.h b/sources/elementtextpattern.h index fb5eaf0e9..b1178b039 100644 --- a/sources/elementtextpattern.h +++ b/sources/elementtextpattern.h @@ -20,9 +20,17 @@ #include #include +#include +#include +#include +#include +#include +#include +#include "qetgraphicsitem/dynamicelementtextitem.h" class Element; class QWidget; +class ElementTextItemGroup; class ExportElementTextPattern { @@ -55,4 +63,153 @@ class ImportElementTextPattern Element *m_element = nullptr; }; +/** + * @brief Description of formatting and layout for an element text item. + */ +struct DynamicTextFormatItem { + DynamicElementTextItem::TextFrom textFrom = DynamicElementTextItem::UserText; + QString infoName; + QString compositeText; + QFont font; + QColor color = Qt::black; + Qt::Alignment alignment = (Qt::AlignLeft | Qt::AlignTop); + QPointF pos; + qreal rotation = 0; + bool keepVisualRotation = false; + bool rotationPointCenter = false; + bool frame = false; + qreal textWidth = -1; + bool visible = true; + QString groupName; + int index = 0; + bool isTerminalText = false; +}; + +/** + * @brief Description of formatting and layout for an element text group. + */ +struct TextGroupFormatItem { + QString name; + QPointF pos; + qreal rotation = 0; + Qt::Alignment alignment = Qt::AlignLeft; + int verticalAdjustment = 0; + bool frame = false; + bool holdToBottomPage = false; +}; + +/** + * @brief Stores the formatting pattern (font, size, style, colors, alignment, positions, offsets, frame) + * captured from a source Element. Acts as the Format Painter template clipboard. + */ +class ElementTextFormatPattern +{ + public: + ElementTextFormatPattern() = default; + + static ElementTextFormatPattern fromElement(Element *element); + static bool hasPattern(); + static const ElementTextFormatPattern ¤tPattern(); + static void setCurrentPattern(const ElementTextFormatPattern &pattern); + static bool isTerminalTextItem(const DynamicElementTextItem *deti, const Element *element); + + bool isValid() const { return !m_text_items.isEmpty(); } + QString sourceElementName() const { return m_source_element_name; } + + const QList &textItems() const { return m_text_items; } + const QList &groups() const { return m_groups; } + + bool hasTerminalTextFormat() const; + DynamicTextFormatItem terminalTextFormat() const; + + private: + QString m_source_element_name; + QList m_text_items; + QList m_groups; + + static ElementTextFormatPattern s_current_pattern; + static bool s_has_pattern; +}; + +/** + * @brief Undo command to apply a text formatting pattern to one or multiple elements. + * Field content (labels, designations, comments, article numbers) is preserved unchanged. + */ +class ApplyElementTextFormatCommand : public QUndoCommand +{ + public: + ApplyElementTextFormatCommand(const QList &elements, + const ElementTextFormatPattern &pattern, + QUndoCommand *parent = nullptr); + ApplyElementTextFormatCommand(Element *element, + const ElementTextFormatPattern &pattern, + QUndoCommand *parent = nullptr); + ~ApplyElementTextFormatCommand() override; + + void undo() override; + void redo() override; + + private: + struct ElementFormatChange { + QPointer element; + + struct TextChange { + QPointer deti; + bool wasCreated = false; + + // Old state + QFont oldFont; + QColor oldColor; + Qt::Alignment oldAlignment; + QPointF oldPos; + qreal oldRotation = 0; + bool oldKeepVisualRotation = false; + bool oldRotationPointCenter = false; + bool oldFrame = false; + qreal oldTextWidth = -1; + bool oldVisible = true; + QString oldGroupName; + + // New state + QFont newFont; + QColor newColor; + Qt::Alignment newAlignment; + QPointF newPos; + qreal newRotation = 0; + bool newKeepVisualRotation = false; + bool newRotationPointCenter = false; + bool newFrame = false; + qreal newTextWidth = -1; + bool newVisible = true; + QString newGroupName; + }; + QList textChanges; + + struct GroupChange { + QString name; + QPointer group; + bool wasCreated = false; + + // Old state + QPointF oldPos; + qreal oldRotation = 0; + Qt::Alignment oldAlignment = Qt::AlignLeft; + int oldVAdjust = 0; + bool oldFrame = false; + bool oldHoldBottom = false; + + // New state + QPointF newPos; + qreal newRotation = 0; + Qt::Alignment newAlignment = Qt::AlignLeft; + int newVAdjust = 0; + bool newFrame = false; + bool newHoldBottom = false; + }; + QList groupChanges; + }; + + QList m_changes; +}; + #endif // ELEMENTTEXTCONFIGURATION_H diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index a25842303..a55248f09 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -53,6 +53,7 @@ #include "cable/editcablecommand.h" #include "diagramevent/diagrameventaddpaste.h" #include "diagramview.h" +#include "elementtextpattern.h" #include "elementspanelwidget.h" #include "factory/elementpicturefactory.h" #include "factory/qetgraphicstablefactory.h" @@ -469,6 +470,19 @@ void QETDiagramEditor::setUpActions() connect(m_paste_element_info, &QAction::triggered, this, &QETDiagramEditor::pasteElementInformations); + //Format painter actions + m_copy_text_format = new QAction(QET::Icons::CopyElementTextFormat, tr("Copy text format"), this); + m_apply_text_format = new QAction(QET::Icons::ApplyElementTextFormat, tr("Apply text format"), this); + + ShortcutManager::instance().registerAction(m_copy_text_format, "diagrameditor.copy_text_format", tr("Diagram editors"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C)); + ShortcutManager::instance().registerAction(m_apply_text_format, "diagrameditor.apply_text_format", tr("Diagram editors"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_V)); + + m_copy_text_format -> setStatusTip(tr("Copy the text format of the selected element as a template", "status bar tip")); + m_apply_text_format -> setStatusTip(tr("Apply the copied text format to the selected elements", "status bar tip")); + + connect(m_copy_text_format, &QAction::triggered, this, &QETDiagramEditor::copyTextFormat); + connect(m_apply_text_format, &QAction::triggered, this, &QETDiagramEditor::applyTextFormat); + //Duplicate: copy the selection and place it at a configured, //grid-step offset immediately -- no interactive follow-the- //cursor step, unlike Ctrl+V above. That is deliberate (#991): @@ -1531,6 +1545,8 @@ void QETDiagramEditor::setUpMenu() menu_edition -> addAction(m_cut); menu_edition -> addAction(m_copy); menu_edition -> addAction(m_paste); + menu_edition -> addAction(m_copy_text_format); + menu_edition -> addAction(m_apply_text_format); menu_edition -> addAction(m_paste_origin); menu_edition -> addAction(m_paste_element_info); menu_edition -> addAction(m_duplicate); @@ -2871,6 +2887,8 @@ void QETDiagramEditor::slot_updateComplexActions() // number of selected elements int selected_elements_count = dc.count(DiagramContent::Elements); m_find_element->setEnabled(selected_elements_count == 1); + m_copy_text_format->setEnabled(selected_elements_count >= 1); + m_apply_text_format->setEnabled(!ro && selected_elements_count >= 1 && ElementTextFormatPattern::hasPattern()); m_mirror_horizontal->setEnabled(!ro && selected_elements_count); m_mirror_vertical->setEnabled(!ro && selected_elements_count); @@ -4690,3 +4708,56 @@ void QETDiagramEditor::updateTextGridButton() //A divisor the menu does not offer, set by hand in the config file m_text_grid_button->setText(tr("Texts %1").arg(TextGrid::ratioLabel(divisor))); } +/** + * @brief QETDiagramEditor::copyTextFormat + * Copies the text/label layout and formatting from the currently selected element as a template. + */ +void QETDiagramEditor::copyTextFormat() +{ + DiagramView *dv = currentDiagramView(); + if (!dv || !dv->diagram()) return; + + Element *sourceElmt = nullptr; + for (QGraphicsItem *item : dv->diagram()->selectedItems()) { + if (Element *elmt = qgraphicsitem_cast(item)) { + sourceElmt = elmt; + break; + } + } + + if (!sourceElmt) return; + + ElementTextFormatPattern pattern = ElementTextFormatPattern::fromElement(sourceElmt); + if (pattern.isValid()) { + ElementTextFormatPattern::setCurrentPattern(pattern); + slot_updateComplexActions(); + statusBar()->showMessage(tr("Text format of \"%1\" copied").arg(sourceElmt->name()), 3000); + } +} + +/** + * @brief QETDiagramEditor::applyTextFormat + * Applies the stored text formatting template to all selected elements in the active diagram. + */ +void QETDiagramEditor::applyTextFormat() +{ + if (!ElementTextFormatPattern::hasPattern()) return; + + DiagramView *dv = currentDiagramView(); + if (!dv || !dv->diagram()) return; + if (dv->diagram()->isReadOnly()) return; + + QList targetElements; + for (QGraphicsItem *item : dv->diagram()->selectedItems()) { + if (Element *elmt = qgraphicsitem_cast(item)) { + targetElements.append(elmt); + } + } + + if (targetElements.isEmpty()) return; + + const ElementTextFormatPattern &pattern = ElementTextFormatPattern::currentPattern(); + dv->diagram()->undoStack().push(new ApplyElementTextFormatCommand(targetElements, pattern)); + + statusBar()->showMessage(tr("Text format applied to %n element(s)", "", targetElements.size()), 3000); +} diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index 2ff471b3f..268e47e4a 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -202,6 +202,8 @@ class QETDiagramEditor : public QETMainWindow void showError(const QETResult &); void showError(const QString &); void subWindowActivated(QMdiSubWindow *subWindows); + void copyTextFormat(); + void applyTextFormat(); private slots: void updateTextGridButton(); @@ -215,7 +217,9 @@ class QETDiagramEditor : public QETMainWindow *m_cut, ///< Cut selection to clipboard *m_copy, ///< Copy selection to clipboard *m_paste_element_info, ///< Copy the information of the copied element onto the selected elements (#1375) - *m_insert_last_element = nullptr; ///< Place the last placed element again + *m_insert_last_element = nullptr, ///< Place the last placed element again + *m_copy_text_format = nullptr, ///< Copy label/text formatting template + *m_apply_text_format = nullptr; ///< Apply label/text formatting template QActionGroup m_row_column_actions_group, /// Action related to add/remove rows/column in diagram diff --git a/sources/qeticons.cpp b/sources/qeticons.cpp index e64fdcae6..783a8072c 100644 --- a/sources/qeticons.cpp +++ b/sources/qeticons.cpp @@ -75,6 +75,8 @@ namespace QET { QIcon EditDelete; QIcon EditOpacity; QIcon EditPaste; + QIcon CopyElementTextFormat; + QIcon ApplyElementTextFormat; QIcon EditRedo; QIcon EditRename; QIcon EditSelectAll; @@ -464,6 +466,8 @@ void QET::Icons::initIcons() EditDelete = QIcon::fromTheme("edit-delete"); EditOpacity = QIcon::fromTheme("edit-opacity"); EditPaste = QIcon::fromTheme("edit-paste"); + CopyElementTextFormat = QIcon(":/ico/format-copy.svg"); + ApplyElementTextFormat = QIcon(":/ico/format-apply.svg"); if (rtl) { EditRedo.addPixmap(QIcon::fromTheme("edit-redo").pixmap(16).transformed(reverse)); EditRedo.addPixmap(QIcon::fromTheme("edit-redo").pixmap(22).transformed(reverse)); diff --git a/sources/qeticons.h b/sources/qeticons.h index 35aedfcc5..2465922ac 100644 --- a/sources/qeticons.h +++ b/sources/qeticons.h @@ -82,6 +82,8 @@ namespace QET { extern QIcon EditDelete; extern QIcon EditOpacity; extern QIcon EditPaste; + extern QIcon CopyElementTextFormat; + extern QIcon ApplyElementTextFormat; extern QIcon EditRedo; extern QIcon EditRename; extern QIcon EditSelectAll;