Merge remote-tracking branch 'upstream/master' into wiring-manager

Conflicts resolved in 24 files. Notable decisions:

- ico/icon-themes.qrc: upstream's icon folder restructure kept; the 36
  cable icon entries re-added under the new qet/qet-dark path scheme
  (XML valid, every referenced file exists).
- Toolbar system: upstream's DiagramToolbarSettings now owns the
  toolbar filling. The cable tool is registered as
  diagrameditor.add_cable (generic group loop) and listed on the
  "diagram" toolbar right after the auto-break conductor action,
  separated by a divider, exactly like before the merge.
- Paste: the cable lines of a copy are handed to
  PasteNumberingImport::push() as extra commands, so paste + cable
  lines + the numbering question stay ONE undo step on both paste
  paths (DiagramView::paste and DiagramEventAddPaste::commit).
- PDF links: upstream's deterministic link sorting kept together with
  the cable cross-reference injector (injectLinkTo -> links list).
- Element renumbering: upstream's ElementAutoNumSchemeCommand::renumber
  replaces the old formula-based body of
  QETProject::renumberElementsBySchemeTitle; the cable numbering rule
  API (cableAutoNum, axis priority, rule name) is kept in full.
- Auto-numbering pages: upstream's scheme management kept, the Câbles
  tab added as index 4 (changeToCableTab), both entry points
  changeToCable() and changeToElement() kept side by side.
- SelectAutonumW: dirty tracking and the contextEdited signal moved
  into upstream's insertPartRow() row API, so single-rule mode, the
  rule-removable button and "confirm without change leaves the project
  alone" keep working.
- xrefpropertieswidget: upstream's offset clamping kept; the cable
  branch (Texte field, Police button, hidden display options) kept;
  the stack-overlapping checkbox is hidden for cables.

Verified after the merge: build green (515/515, no errors), ctest
85/86 (only the pre-existing headless tst_menubarkeyboard failure),
offscreen smoke exit 124.
This commit is contained in:
Kellermorph
2026-10-09 14:09:08 +02:00
1767 changed files with 194544 additions and 72846 deletions
+88 -57
View File
@@ -16,12 +16,14 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "diagrameventaddpaste.h"
#include "../autoNum/ui/pastenumberingimport.h"
#include "../cable/addcablecommand.h"
#include "../cable/cablecopy.h"
#include "../cable/cablepart.h"
#include "../cable/editcablecommand.h"
#include "../diagram.h"
#include "../foliogrid.h"
#include "../diagramcommands.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
@@ -33,42 +35,30 @@
#include <QApplication>
#include <QClipboard>
#include <QCoreApplication>
#include <QCursor>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsView>
#include <QKeyEvent>
#include <QStatusBar>
#include <utility>
/**
@brief batchWith
Keep the cable lines of a paste together with the rest of it, so one
Ctrl+Z takes the whole thing back in one go. Mirrors the helper
DiagramView uses for its one-step paste; BatchCommand's own name
says "Supprimer", so the name of what the user actually did is put
back onto the batch.
@param first the step already assembled, may be null
@param second the step to add; ownership goes to the result
@return both steps as one command, or the only one of them
*/
static QUndoCommand *batchWith(QUndoCommand *first, QUndoCommand *second)
{
if (!first) return second;
if (!second) return first;
auto *batched = new BatchCommand(first, second);
if (!first->text().isEmpty()) batched->setText(first->text());
return batched;
}
/**
@brief DiagramEventAddPaste::DiagramEventAddPaste
@param diagram : diagram to paste into
@param start_pos : where the pasted items first appear, in scene
coordinates -- normally the cursor
coordinates -- normally the cursor. Only used for UnderCursor; an
AtOrigin paste stays at the coordinates it was copied from.
@param placement : UnderCursor moves the group under the cursor
(default), AtOrigin leaves it at its original XML position and warps
the OS cursor to the group's origin so baseline and screen match.
*/
DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos) :
DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos,
PastePlacement placement) :
DiagramEventInterface(diagram)
{
m_placement = placement;
//DiagramEventInterface::init() is called by Diagram::setEventInterface
//only when it is replacing an earlier interface, so call it here as
//DiagramEventAddMacro does.
@@ -97,6 +87,7 @@ static QUndoCommand *batchWith(QUndoCommand *first, QUndoCommand *second)
//items -- and it travels as a whole, colour labels and all,
//whether or not the place it is going to holds a wire.
m_diagram->fromXml(document_xml, QPointF(), false, &m_content, &m_cables);
m_copied_schemes = PasteNumberingImport::copiedBy(document_xml);
if (db) {
db->blockSignals(false);
@@ -135,30 +126,38 @@ static QUndoCommand *batchWith(QUndoCommand *first, QUndoCommand *second)
}
const QPointF top_left = items_rect.topLeft();
QSettings settings;
const int xGrid = settings.value(QStringLiteral("diagrameditor/Xgrid"),
Diagram::xGrid).toInt();
const int yGrid = settings.value(QStringLiteral("diagrameditor/Ygrid"),
Diagram::yGrid).toInt();
const int xGrid = FolioGrid::step(settings.value(FolioGrid::x_key), Diagram::xGrid);
const int yGrid = FolioGrid::step(settings.value(FolioGrid::y_key), Diagram::yGrid);
const auto snapGrid = [xGrid, yGrid](const QPointF &p) -> QPointF {
return QPointF(
qRound(p.x() / xGrid) * xGrid,
qRound(p.y() / yGrid) * yGrid);
};
const QPointF grid_origin = snapGrid(start_pos);
const QPointF grid_origin = (m_placement == AtOrigin)
? snapGrid(top_left)
: snapGrid(start_pos);
//Land the pasted content under the cursor immediately, rather than
//leaving it at the copied source's own coordinates: fromXml() above
//loads items at their original position purely because it doesn't
//know the target yet, not because that is where a paste should end
//up. The previous approach instead left items there and warped the
//OS cursor to match -- QCursor::setPos() is silently ignored by
//many window managers and compositors (Wayland in particular), so
//on any of those the warp simply never happened and the paste was
//left wherever it had originally been copied from, which could be
//anywhere on the folio -- exactly the "far from the cursor" bug.
const QPointF initial_delta = grid_origin - snapGrid(top_left);
for (auto *item : movable) {
item->setPos(item->pos() + initial_delta);
if (m_placement == UnderCursor) {
//Land the pasted content under the cursor immediately, rather than
//leaving it at the copied source's own coordinates: fromXml() above
//loads items at their original position purely because it doesn't
//know the target yet, not because that is where a paste should end
//up. The previous approach instead left items there and warped the
//OS cursor to match -- QCursor::setPos() is silently ignored by
//many window managers and compositors (Wayland in particular), so
//on any of those the warp simply never happened and the paste was
//left wherever it had originally been copied from, which could be
//anywhere on the folio -- exactly the "far from the cursor" bug.
const QPointF initial_delta = grid_origin - snapGrid(top_left);
for (auto *item : movable) {
item->setPos(item->pos() + initial_delta);
}
} else {
//AtOrigin: the items keep the coordinates they were copied from,
//so the copy stands exactly where the original stands. No delta is
//applied; what has to move is the cursor -- it is warped to the
//group's grid-snapped origin below, once m_group_origin is set,
//so the baseline and the physical cursor position agree.
}
//Store each item's now-placed position. moveTo() applies a
@@ -176,10 +175,41 @@ static QUndoCommand *batchWith(QUndoCommand *first, QUndoCommand *second)
conductor->updatePath();
}
//The baseline is the group's grid-snapped origin, so moveTo()
//does not have to capture one from the first mouse movement.
m_initial_cursor = m_group_origin;
m_baseline_captured = true;
//The baseline is the group's grid-snapped origin. For UnderCursor
//the group was just put there, so the baseline is known outright.
//For AtOrigin the group stayed where it was copied from and the
//OS cursor is warped to match instead -- but only a warp that
//verifiably landed may be trusted as the baseline: QCursor::setPos()
//needs pointer focus inside the target window (on Wayland it goes
//through wp_pointer_warp_v1, and is a silent no-op without it), and
//a baseline that assumes a warp which did not happen would move the
//whole group by the warp-sized delta on the first mouse move. So
//when the warp cannot be confirmed, leave m_baseline_captured
//false: moveTo() re-baselines from the first real mouse position
//instead, which costs at most the first movement but never flings
//the group across the folio.
if (m_placement == AtOrigin && !m_diagram->views().isEmpty()) {
if (auto *view = m_diagram->views().at(0)) {
//An origin outside the visible area cannot be warped to
//(the compositor rejects targets outside the window) and
//would leave the paste invisible, so scroll it into view
//first.
if (!view->viewport()->rect().contains(view->mapFromScene(m_group_origin))) {
view->ensureVisible(items_rect, 50, 50);
}
const QPoint view_pos = view->mapFromScene(m_group_origin);
const QPoint global_pos = view->viewport()->mapToGlobal(view_pos);
QCursor::setPos(global_pos);
if ((QCursor::pos() - global_pos).manhattanLength() <= 4) {
m_initial_cursor = m_group_origin;
m_baseline_captured = true;
}
}
}
if (m_placement == UnderCursor) {
m_initial_cursor = m_group_origin;
m_baseline_captured = true;
}
m_diagram->clearSelection();
for (auto *item : movable) {
@@ -273,10 +303,8 @@ void DiagramEventAddPaste::showHint()
void DiagramEventAddPaste::moveTo(const QPointF &scene_pos)
{
QSettings settings;
const int xGrid = settings.value(QStringLiteral("diagrameditor/Xgrid"),
Diagram::xGrid).toInt();
const int yGrid = settings.value(QStringLiteral("diagrameditor/Ygrid"),
Diagram::yGrid).toInt();
const int xGrid = FolioGrid::step(settings.value(FolioGrid::x_key), Diagram::xGrid);
const int yGrid = FolioGrid::step(settings.value(FolioGrid::y_key), Diagram::yGrid);
const auto snapGrid = [xGrid, yGrid](const QPointF &p) -> QPointF {
return QPointF(
@@ -396,10 +424,10 @@ void DiagramEventAddPaste::commit()
m_content.conductors(DiagramContent::AnyConductor));
m_cables.clear();
QUndoCommand *command = nullptr;
if (m_content.count()) {
command = new PasteDiagramCommand(m_diagram, m_content);
}
//The cable lines the copy brings are not part of the paste
//itself: they are handed to the paste so that one Ctrl+Z takes
//the whole of it back in one go.
QList<QUndoCommand *> cable_commands;
for (const CableCopy::Wired &line : std::as_const(wired))
{
if (!line.part) continue;
@@ -407,12 +435,15 @@ void DiagramEventAddPaste::commit()
auto *added = new AddCableCommand(line.part->cable(), line.part.data(),
m_diagram, true, line.taken);
added->setText(QCoreApplication::translate("CableCopy", "Coller un câble"));
command = batchWith(command, added);
}
if (command) {
m_diagram->undoStack().push(command);
cable_commands << added;
}
//Asks whether to import the numberings the copy brings, if the
//project has not got them; the paste itself and the cable lines
//travel inside the same step of the undo stack.
PasteNumberingImport::push(m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
m_diagram, m_content, m_copied_schemes, cable_commands);
//The undo stack has just run its first redo, which may have
//renewed the identity of a pasted conductor: the cores are
//pointed at the wires again so their cable field survives it.