mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-28 04:54:13 +02:00
1e74bcd9de
The Windows 11 style gives QPalette::Button a translucent colour (#FFFFFFB3). The rulers filled their background with it, and since they paint with WA_OpaquePaintEvent nothing clears them first: every zoom step blended the new labels over the old ones, leaving a fading trail. Fill with the button colour composed over the window colour instead, which is always opaque. Each ruler is now also hidden while the folio's own column (or row) header is wholly in sight, so zoomed out only the folio's headers show, not both. When a ruler comes or goes the drawing keeps its place on screen: the ruler covers or uncovers the edge of the viewport. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1683 lines
52 KiB
C++
1683 lines
52 KiB
C++
/*
|
|
Copyright 2006-2026 The QElectroTech Team
|
|
This file is part of QElectroTech.
|
|
|
|
QElectroTech is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
QElectroTech is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "diagramview.h"
|
|
#include "cellruler.h"
|
|
#include "lastusedstyle.h"
|
|
#include "qetproject.h"
|
|
#include "QPropertyUndoCommand/qpropertyundocommand.h"
|
|
#include "diagramcommands.h"
|
|
#include "diagramevent/diagrameventaddelement.h"
|
|
#include "diagramevent/diagrameventaddmacro.h"
|
|
#include "dvevent/dveventinterface.h"
|
|
#include "projectview.h"
|
|
#include "qetdiagrameditor.h"
|
|
#include "qetgraphicsitem/conductor.h"
|
|
#include "qetgraphicsitem/conductortextitem.h"
|
|
#include "qetgraphicsitem/independenttextitem.h"
|
|
#include "qeticons.h"
|
|
#include "titleblock/integrationmovetemplateshandler.h"
|
|
#include "ui/diagrampropertiesdialog.h"
|
|
#include "ui/multipastedialog.h"
|
|
#include "undocommand/changetitleblockcommand.h"
|
|
#include "utils/conductorcreator.h"
|
|
#include "undocommand/addgraphicsobjectcommand.h"
|
|
#include "diagram.h"
|
|
#include "ElementsCollection/xmlelementcollection.h"
|
|
#include "NameList/nameslist.h"
|
|
#include "elementdialog.h"
|
|
#include <QApplication>
|
|
#include <QDropEvent>
|
|
#include <QPainter>
|
|
#include <QPointer>
|
|
#include <algorithm>
|
|
|
|
/**
|
|
Constructeur
|
|
@param diagram Schema a afficher ; si diagram vaut 0, un nouveau Diagram est utilise
|
|
@param parent Le QWidget parent de cette vue de schema
|
|
*/
|
|
DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
|
|
PaletteGraphicsView (parent),
|
|
m_diagram (diagram)
|
|
{
|
|
grabGesture(Qt::PinchGesture);
|
|
setAttribute(Qt::WA_DeleteOnClose, true);
|
|
setInteractive(true);
|
|
|
|
QString whatsthis = tr(
|
|
"Ceci est la zone dans laquelle vous concevez vos schémas en y ajoutant"
|
|
" des éléments et en posant des conducteurs entre leurs bornes. Il est"
|
|
" également possible d'ajouter des textes indépendants.",
|
|
"\"What's this?\" tip"
|
|
);
|
|
setWhatsThis(whatsthis);
|
|
|
|
// active l'antialiasing
|
|
setRenderHint(QPainter::Antialiasing, true);
|
|
setRenderHint(QPainter::TextAntialiasing, true);
|
|
setRenderHint(QPainter::SmoothPixmapTransform, true);
|
|
|
|
setScene(m_diagram);
|
|
m_diagram -> undoStack().setClean();
|
|
setWindowIcon(QET::Icons::QETLogo);
|
|
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
|
|
setResizeAnchor(QGraphicsView::AnchorUnderMouse);
|
|
setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
|
setSelectionMode();
|
|
adjustSceneRect();
|
|
updateWindowTitle();
|
|
m_diagram->loadElmtFolioSeq();
|
|
m_diagram->loadCndFolioSeq();
|
|
|
|
m_paste_here = new QAction(QET::Icons::EditPaste, tr("Coller ici", "context menu action"), this);
|
|
connect(m_paste_here, &QAction::triggered, this, &DiagramView::pasteHere);
|
|
|
|
m_multi_paste = new QAction(QET::Icons::EditPaste, tr("Collage multiple"), this);
|
|
connect(m_multi_paste, &QAction::triggered, [this]() {
|
|
MultiPasteDialog d(this->m_diagram, this);
|
|
d.exec();
|
|
});
|
|
|
|
// Setup the action to create a template
|
|
m_create_template = new QAction(tr("Créer un template", "context menu action"), this);
|
|
connect(m_create_template, &QAction::triggered, this, &DiagramView::createTemplateFromSelection);
|
|
|
|
//setup three separators, to be use in context menu
|
|
for(int i=0 ; i<3 ; ++i)
|
|
{
|
|
m_separators << new QAction(this);
|
|
m_separators.last()->setSeparator(true);
|
|
}
|
|
|
|
connect(m_diagram, &Diagram::showDiagram, this, &DiagramView::showDiagram);
|
|
connect(m_diagram, &QGraphicsScene::sceneRectChanged, this, &DiagramView::adjustSceneRect);
|
|
connect(&(m_diagram -> border_and_titleblock), &BorderTitleBlock::informationChanged, this, &DiagramView::updateWindowTitle);
|
|
connect(diagram, &Diagram::findElementRequired, this, &DiagramView::findElementRequired);
|
|
|
|
m_top_ruler = new CellRuler(Qt::Horizontal, this);
|
|
m_side_ruler = new CellRuler(Qt::Vertical, this);
|
|
m_cell_rulers_shown = QSettings().value("diagrameditor/cell_rulers", false).toBool();
|
|
connect(&m_diagram->border_and_titleblock, &BorderTitleBlock::borderChanged, this, &DiagramView::updateCellRulers);
|
|
connect(&m_diagram->border_and_titleblock, &BorderTitleBlock::displayChanged, this, &DiagramView::updateCellRulers);
|
|
updateCellRulers();
|
|
|
|
QShortcut *edit_conductor_color_shortcut = new QShortcut(QKeySequence(Qt::Key_F2), this);
|
|
connect(edit_conductor_color_shortcut, &QShortcut::activated, [this]()
|
|
{
|
|
if (m_diagram->isReadOnly()) {
|
|
return;
|
|
}
|
|
|
|
DiagramContent dc(m_diagram);
|
|
auto cond_list = dc.conductors(DiagramContent::AnyConductor);
|
|
if (cond_list.size() != 1) {
|
|
return;
|
|
}
|
|
auto edited_conductor = cond_list.first();
|
|
|
|
// store the initial properties of the provided conductor
|
|
ConductorProperties initial_properties = edited_conductor->properties();
|
|
|
|
// prepare a color dialog showing the initial conductor color
|
|
QPointer<QColorDialog> color_dialog = new QColorDialog(this);
|
|
color_dialog->setWindowTitle(tr("Choisir la nouvelle couleur de ce conducteur"));
|
|
#ifdef Q_OS_MACOS
|
|
color_dialog -> setWindowFlags(Qt::Sheet);
|
|
#endif
|
|
color_dialog->setCurrentColor(initial_properties.color);
|
|
|
|
// asks the user what color he wishes to apply
|
|
if (color_dialog->exec() == QDialog::Accepted)
|
|
{
|
|
QColor new_color = color_dialog -> selectedColor();
|
|
if (new_color != initial_properties.color)
|
|
{
|
|
// the user chose a different color
|
|
QVariant old_value, new_value;
|
|
old_value.setValue(initial_properties);
|
|
initial_properties.color = new_color;
|
|
new_value.setValue(initial_properties);
|
|
|
|
QPropertyUndoCommand *undo = new QPropertyUndoCommand(edited_conductor, "properties", old_value, new_value);
|
|
undo->setText(tr("Modifier les propriétés d'un conducteur", "undo caption"));
|
|
m_diagram->undoStack().push(undo);
|
|
|
|
// remember it for the next conductor drawn this session,
|
|
// the way LastUsedStyle already does for shapes (#879)
|
|
LastUsedStyle::setConductorColor(new_color);
|
|
}
|
|
}
|
|
if (color_dialog)
|
|
delete color_dialog;
|
|
});
|
|
}
|
|
|
|
/**
|
|
Destructeur
|
|
*/
|
|
DiagramView::~DiagramView()
|
|
{}
|
|
|
|
/**
|
|
Accepte ou refuse le drag'n drop en fonction du type de donnees entrant
|
|
@param e le QDragEnterEvent correspondant au drag'n drop tente
|
|
*/
|
|
void DiagramView::dragEnterEvent(QDragEnterEvent *e) {
|
|
if (e -> mimeData() -> hasFormat("application/x-qet-element-uri")) {
|
|
e -> acceptProposedAction();
|
|
} else if (e -> mimeData() -> hasFormat("application/x-qet-titleblock-uri")) {
|
|
e -> acceptProposedAction();
|
|
} else if (e -> mimeData() -> hasText()) {
|
|
e -> acceptProposedAction();
|
|
} else {
|
|
e -> ignore();
|
|
}
|
|
}
|
|
|
|
/**
|
|
Accepte ou refuse le drag'n drop en fonction du type de donnees entrant
|
|
@param e le QDragMoveEvent correspondant au drag'n drop tente
|
|
*/
|
|
void DiagramView::dragMoveEvent(QDragMoveEvent *e) {
|
|
if (e -> mimeData() -> hasFormat("text/plain")) e -> acceptProposedAction();
|
|
else e-> ignore();
|
|
}
|
|
|
|
/**
|
|
Handle the drops accepted on diagram (elements and title block templates).
|
|
@param e the QDropEvent describing the current drag'n drop
|
|
*/
|
|
void DiagramView::dropEvent(QDropEvent *e) {
|
|
|
|
if (e -> mimeData() -> hasFormat("application/x-qet-element-uri")) {
|
|
handleElementDrop(e);
|
|
} else if (e -> mimeData() -> hasFormat("application/x-qet-titleblock-uri")) {
|
|
handleTitleBlockDrop(e);
|
|
} else if (e -> mimeData() -> hasText()) {
|
|
handleTextDrop(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief DiagramView::handleElementDrop
|
|
* Handle the drop of an element.
|
|
* @param event the QDropEvent describing the current drag'n drop
|
|
*/
|
|
void DiagramView::handleElementDrop(QDropEvent *event)
|
|
{
|
|
//Build an element from the text of the mime data
|
|
ElementsLocation location(event->mimeData()->text());
|
|
|
|
if ( !(location.isElement() && location.exist()) )
|
|
{
|
|
qDebug() << "DiagramView::handleElementDrop, location can't be use : " << location;
|
|
return;
|
|
}
|
|
|
|
QPointF drop_pos;
|
|
drop_pos = mapToScene(event->position().toPoint());
|
|
|
|
if (location.path().endsWith(".qetmak")) {
|
|
diagram()->setEventInterface(new DiagramEventAddMacro(location, diagram(), drop_pos));
|
|
} else {
|
|
diagram()->setEventInterface(new DiagramEventAddElement(location, diagram(), drop_pos));
|
|
}
|
|
|
|
//Set focus to the view to get event
|
|
this->setFocus();
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::handleTitleBlockDrop
|
|
Handle the dropEvent that contain data of a titleblock
|
|
@param e
|
|
*/
|
|
void DiagramView::handleTitleBlockDrop(QDropEvent *e) {
|
|
// fetch the title block template location from the drop event
|
|
TitleBlockTemplateLocation tbt_loc;
|
|
tbt_loc.fromString(e->mimeData()->text());
|
|
|
|
|
|
if (tbt_loc.isValid())
|
|
{
|
|
// fetch the current title block properties
|
|
TitleBlockProperties titleblock_properties_before = m_diagram->border_and_titleblock.exportTitleBlock();
|
|
|
|
// check the provided template is not already applied
|
|
QETProject *tbt_parent_project = tbt_loc.parentProject();
|
|
if (tbt_parent_project && tbt_parent_project == m_diagram -> project())
|
|
{
|
|
// same parent project and same name = same title block template
|
|
if (tbt_loc.name() == titleblock_properties_before.template_name)
|
|
return;
|
|
}
|
|
|
|
// integrate the provided template into the project if needed
|
|
QString integrated_template_name = tbt_loc.name();
|
|
if (mustIntegrateTitleBlockTemplate(tbt_loc))
|
|
{
|
|
IntegrationMoveTitleBlockTemplatesHandler *handler = new IntegrationMoveTitleBlockTemplatesHandler(this);
|
|
//QString error_message;
|
|
integrated_template_name = m_diagram->project()->integrateTitleBlockTemplate(tbt_loc, handler);
|
|
|
|
if (integrated_template_name.isEmpty())
|
|
return;
|
|
}
|
|
|
|
// apply the provided title block template
|
|
if (titleblock_properties_before.template_name == integrated_template_name)
|
|
return;
|
|
|
|
TitleBlockProperties titleblock_properties_after = titleblock_properties_before;
|
|
titleblock_properties_after.template_name = integrated_template_name;
|
|
m_diagram->undoStack().push(new ChangeTitleBlockCommand(m_diagram, titleblock_properties_before, titleblock_properties_after));
|
|
|
|
adjustSceneRect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::handleTextDrop
|
|
*handle the drop of text
|
|
@param e the QDropEvent describing the current drag'n drop
|
|
*/
|
|
void DiagramView::handleTextDrop(QDropEvent *e) {
|
|
if (m_diagram -> isReadOnly() || (e -> mimeData() -> hasText() == false) ) return;
|
|
|
|
IndependentTextItem *iti = new IndependentTextItem (e -> mimeData() -> text());
|
|
|
|
if (e -> mimeData() -> hasHtml()) {
|
|
iti -> setHtml (e -> mimeData() -> text());
|
|
}
|
|
|
|
m_diagram->undoStack().push(new AddGraphicsObjectCommand(
|
|
iti, m_diagram, mapToScene(e->position().toPoint())));
|
|
}
|
|
|
|
/**
|
|
Set the Diagram in visualisation mode
|
|
*/
|
|
void DiagramView::setVisualisationMode()
|
|
{
|
|
setDragMode(ScrollHandDrag);
|
|
applyReadOnly();
|
|
setInteractive(false);
|
|
emit(modeChanged());
|
|
}
|
|
|
|
/**
|
|
Set the Diagram in Selection mode
|
|
*/
|
|
void DiagramView::setSelectionMode()
|
|
{
|
|
setDragMode(RubberBandDrag);
|
|
setInteractive(true);
|
|
applyReadOnly();
|
|
emit(modeChanged());
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::zoom
|
|
Zomm the view.
|
|
A zoom_factor > 1 zoom in.
|
|
A zoom_factor < 1 zoom out
|
|
@param zoom_factor
|
|
*/
|
|
void DiagramView::zoom(const qreal zoom_factor)
|
|
{
|
|
// clamp the resulting scale so a repeated wheel-zoom cannot drive the view
|
|
// transform to floating-point overflow and crash the editor (issue #798)
|
|
const qreal target = transform().m11() * zoom_factor;
|
|
if (target < m_min_zoom || target > m_max_zoom) {
|
|
return;
|
|
}
|
|
|
|
if (zoom_factor >= 1){
|
|
scale(zoom_factor, zoom_factor);
|
|
}
|
|
else
|
|
{
|
|
QSettings settings;
|
|
if (settings.value("diagrameditor/zoom-out-beyond-of-folio", false).toBool() ||
|
|
(horizontalScrollBar()->maximum() || verticalScrollBar()->maximum()) )
|
|
if (zoom_factor >= 0){
|
|
scale(zoom_factor, zoom_factor);
|
|
}
|
|
}
|
|
m_diagram->adjustSceneRect();
|
|
adjustGridToZoom();
|
|
adjustSceneRect();
|
|
}
|
|
|
|
/**
|
|
Agrandit ou rectrecit le schema de facon a ce que tous les elements du
|
|
schema soient visibles a l'ecran. S'il n'y a aucun element sur le schema,
|
|
le zoom est reinitialise
|
|
*/
|
|
void DiagramView::zoomFit()
|
|
{
|
|
adjustSceneRect();
|
|
fitInView(m_diagram->sceneRect(), Qt::KeepAspectRatio);
|
|
adjustGridToZoom();
|
|
}
|
|
|
|
/**
|
|
Adjust zoom to fit all elements in the view, regardless of diagram borders.
|
|
*/
|
|
void DiagramView::zoomContent()
|
|
{
|
|
fitInView(m_diagram -> itemsBoundingRect(), Qt::KeepAspectRatio);
|
|
adjustGridToZoom();
|
|
}
|
|
|
|
/**
|
|
Reinitialise le zoom
|
|
*/
|
|
void DiagramView::zoomReset()
|
|
{
|
|
resetTransform();
|
|
adjustGridToZoom();
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::zoomToRect
|
|
Adjust zoom to fit \a rect, in scene coordinate, in the view.
|
|
@param rect
|
|
*/
|
|
void DiagramView::zoomToRect(const QRectF &rect)
|
|
{
|
|
fitInView(rect, Qt::KeepAspectRatio);
|
|
//Zooming in makes the scroll bars appear, which resizes the viewport
|
|
//from a queued call; that resize is anchored under the mouse and
|
|
//would scroll away from rect, so center again once it has run.
|
|
QMetaObject::invokeMethod(this, [this, rect]() {
|
|
centerOn(rect.center());
|
|
}, Qt::QueuedConnection);
|
|
adjustGridToZoom();
|
|
}
|
|
|
|
/**
|
|
Copie les elements selectionnes du schema dans le presse-papier puis les supprime
|
|
Copies the selected elements from the diagram to the clipboard and then deletes them
|
|
*/
|
|
void DiagramView::cut()
|
|
{
|
|
copy();
|
|
DiagramContent cut_content(m_diagram);
|
|
m_diagram -> clearSelection();
|
|
m_diagram -> undoStack().push(new CutDiagramCommand(m_diagram, cut_content));
|
|
}
|
|
|
|
/**
|
|
Copie les elements selectionnes du schema dans le presse-papier
|
|
Copies the selected elements from the diagram to the clipboard
|
|
*/
|
|
void DiagramView::copy()
|
|
{
|
|
QClipboard *clipboard = QApplication::clipboard();
|
|
QString content_clipboard = m_diagram -> toXml(false, true).toString(4);
|
|
if (clipboard -> supportsSelection()) clipboard -> setText(content_clipboard, QClipboard::Selection);
|
|
clipboard -> setText(content_clipboard);
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::paste
|
|
Import the element stored in the clipboard to the diagram.
|
|
@param pos : top left corner of the bounding rect of imported elements
|
|
@param clipboard_mode
|
|
*/
|
|
void DiagramView::paste(const QPointF &pos, QClipboard::Mode clipboard_mode) {
|
|
if (!isInteractive() || m_diagram -> isReadOnly()) return;
|
|
|
|
QString text_clipboard = QApplication::clipboard() -> text(clipboard_mode);
|
|
if ((text_clipboard).isEmpty()) return;
|
|
|
|
QDomDocument document_xml;
|
|
if (!document_xml.setContent(text_clipboard)) return;
|
|
|
|
DiagramContent content_pasted;
|
|
m_diagram->fromXml(document_xml, pos, false, &content_pasted);
|
|
|
|
//If something was really added to diagram, we create an undo object.
|
|
if (content_pasted.count())
|
|
{
|
|
m_diagram -> clearSelection();
|
|
m_diagram -> undoStack().push(new PasteDiagramCommand(m_diagram, content_pasted));
|
|
adjustSceneRect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
Colle le contenu du presse-papier sur le schema a la position de la souris
|
|
Pastes the contents of the clipboard into the diagram at the mouse position.
|
|
*/
|
|
void DiagramView::pasteHere()
|
|
{
|
|
paste(mapToScene(m_paste_here_pos));
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::duplicate
|
|
Copy the current selection and place the copy at @p stepOffset grid
|
|
steps from it, landing immediately rather than following the cursor
|
|
like Ctrl+V does (bugtracker #991). @p stepOffset comes from
|
|
DuplicateOffsetDialog: (1, 0) is one grid step right, (0, -1) is one
|
|
grid step up, and so on -- QET's own scene axes, X right and Y down.
|
|
|
|
No interactive placement step on purpose: the point of a duplicate
|
|
shortcut is unattended, repeatable stamping (configure the offset
|
|
once, then tap Ctrl+D to lay out a row), which following the cursor
|
|
would interrupt on every press. QET already reselects whatever a
|
|
paste just added (see PasteDiagramCommand::redo()), so the next
|
|
Ctrl+D naturally continues from the copy just placed, not the
|
|
original -- a press-and-hold row falls out of that for free, with no
|
|
special-casing needed here for "keep going from the last one".
|
|
|
|
The offset is applied by hand rather than by asking paste()/
|
|
Diagram::fromXml() to place the copy at a target position. Both of
|
|
those feed the position through Diagram::snapToGrid(), which reads
|
|
QApplication::keyboardModifiers() and rounds to the nearest PIXEL
|
|
instead of the grid whenever Ctrl is held -- and Ctrl is always held
|
|
here, this action's own shortcut being Ctrl+D. Measured the hard way
|
|
before settling on this: routing the offset through paste() first
|
|
produced copies off-grid on both axes, by an amount that tracked the
|
|
selection's own bounding-box geometry rather than being a fixed
|
|
error. fromXml() is instead called with no position at all, which
|
|
leaves every item at its source coordinates (landing the copy
|
|
exactly on top of the originals -- (0, 0) is not a position, this is
|
|
"keep the source coordinates"), and the offset is added directly
|
|
with setPos(). A plain addition cannot be off by a rounding rule
|
|
that never runs.
|
|
|
|
Conductors are not in the translated set: fromXml() itself does not
|
|
reposition them either -- they are loaded from XML after elements
|
|
are already in their final place and take their geometry from their
|
|
terminals, which have already moved with the elements that own
|
|
them. Likewise dynamic element texts are not translated separately:
|
|
they are children of their element and move with it under Qt's
|
|
normal parent-child transform.
|
|
*/
|
|
void DiagramView::duplicate(const QPoint &stepOffset)
|
|
{
|
|
if (!isInteractive() || m_diagram->isReadOnly()) return;
|
|
|
|
const QList<QGraphicsItem *> selection = m_diagram->selectedItems();
|
|
if (selection.isEmpty()) return;
|
|
|
|
QSettings settings;
|
|
const int x_grid = settings.value(QStringLiteral("diagrameditor/Xgrid"),
|
|
Diagram::xGrid).toInt();
|
|
const int y_grid = settings.value(QStringLiteral("diagrameditor/Ygrid"),
|
|
Diagram::yGrid).toInt();
|
|
const QPointF offset(stepOffset.x() * x_grid, stepOffset.y() * y_grid);
|
|
|
|
// Mirrors copy(), but does not touch the system clipboard: Ctrl+D
|
|
// should not clobber whatever the user last copied with Ctrl+C.
|
|
QDomDocument document = m_diagram->toXml(false, true);
|
|
|
|
DiagramContent pasted;
|
|
// No position argument -- see the function comment above for why
|
|
// the offset is not passed here.
|
|
m_diagram->fromXml(document, QPointF(), false, &pasted);
|
|
if (!pasted.count()) return;
|
|
|
|
const int movable = DiagramContent::Elements | DiagramContent::TextFields
|
|
| DiagramContent::Images | DiagramContent::Shapes
|
|
| DiagramContent::Tables | DiagramContent::TerminalStrip;
|
|
for (QGraphicsItem *item : pasted.items(movable))
|
|
item->setPos(item->pos() + offset);
|
|
|
|
m_diagram->clearSelection();
|
|
m_diagram->undoStack().push(new PasteDiagramCommand(m_diagram, pasted));
|
|
adjustSceneRect();
|
|
}
|
|
|
|
/**
|
|
Manage the events press click :
|
|
* click to add an independent text field
|
|
*/
|
|
void DiagramView::mousePressEvent(QMouseEvent *e)
|
|
{
|
|
e->ignore();
|
|
|
|
if (m_fresh_focus_in)
|
|
{
|
|
switchToVisualisationModeIfNeeded(e);
|
|
m_fresh_focus_in = false;
|
|
}
|
|
|
|
if (m_event_interface && m_event_interface->mousePressEvent(e)) return;
|
|
|
|
//Start drag view when hold the middle button
|
|
if (e->button() == Qt::MiddleButton)
|
|
{
|
|
m_drag_last_pos = e->pos();
|
|
viewport()->setCursor(Qt::ClosedHandCursor);
|
|
e->accept();
|
|
return;
|
|
}
|
|
|
|
//There is a good luck that user want to do a free selection
|
|
//In this case we temporally disable the dragmode because if the QGraphicsScene don't accept the event,
|
|
//and the drag mode is set to rubberbanddrag, the QGraphicsView start rubber band drag, and accept the event.
|
|
if (e->button() == Qt::LeftButton &&
|
|
e->modifiers() == Qt::CTRL)
|
|
{
|
|
QGraphicsView::DragMode dm = dragMode();
|
|
setDragMode(QGraphicsView::NoDrag);
|
|
QGraphicsView::mousePressEvent(e);
|
|
setDragMode(dm);
|
|
} else {
|
|
QGraphicsView::mousePressEvent(e);
|
|
}
|
|
|
|
if (e->isAccepted()) {
|
|
return;
|
|
}
|
|
|
|
if (e->button() == Qt::LeftButton &&
|
|
e->modifiers() == Qt::CTRL)
|
|
{
|
|
m_free_rubberbanding = true;
|
|
m_free_rubberband = QPolygon();
|
|
e->accept();
|
|
return;
|
|
}
|
|
|
|
if (!e->isAccepted()) {
|
|
QGraphicsView::mousePressEvent(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::mouseMoveEvent
|
|
Manage the event move mouse
|
|
*/
|
|
void DiagramView::mouseMoveEvent(QMouseEvent *e)
|
|
{
|
|
m_last_mouse_pos = e->pos();
|
|
setToolTip(tr("X: %1 Y: %2").arg(e->pos().x()).arg(e->pos().y()));
|
|
if (m_event_interface && m_event_interface->mouseMoveEvent(e)) return;
|
|
|
|
// Drag the view
|
|
if (e->buttons() == Qt::MiddleButton)
|
|
{
|
|
QScrollBar *h = horizontalScrollBar();
|
|
QScrollBar *v = verticalScrollBar();
|
|
QPointF pos = m_drag_last_pos - e -> pos();
|
|
m_drag_last_pos = e -> pos();
|
|
h -> setValue(h -> value() + pos.x());
|
|
v -> setValue(v -> value() + pos.y());
|
|
adjustSceneRect();
|
|
}
|
|
else if (m_free_rubberbanding)
|
|
{
|
|
//Update old free rubberband
|
|
if (viewportUpdateMode() != QGraphicsView::NoViewportUpdate && !m_free_rubberband.isEmpty())
|
|
{
|
|
if (viewportUpdateMode() != QGraphicsView::FullViewportUpdate) {
|
|
viewport()->update(m_free_rubberband.boundingRect().toRect().adjusted(-10,-10,10,10));
|
|
}
|
|
else {
|
|
update();
|
|
}
|
|
}
|
|
|
|
//Stop polygon rubberbanding if user has let go of all buttons (even
|
|
//if we didn't get the release events)
|
|
if (!e->buttons()) {
|
|
m_free_rubberbanding = false;
|
|
m_free_rubberband = QPolygon();
|
|
return;
|
|
}
|
|
m_free_rubberband.append(mapToScene(e->pos()));
|
|
emit freeRubberBandChanged(m_free_rubberband);
|
|
|
|
if (viewportUpdateMode() != QGraphicsView::NoViewportUpdate)
|
|
{
|
|
if (viewportUpdateMode() != QGraphicsView::FullViewportUpdate) {
|
|
viewport()->update(mapFromScene(m_free_rubberband.boundingRect().adjusted(-10,-10,10,10)));
|
|
}
|
|
else {
|
|
update();
|
|
}
|
|
}
|
|
|
|
//Set the new selection area
|
|
QPainterPath selection_area;
|
|
selection_area.addPolygon(m_free_rubberband);
|
|
m_diagram->setSelectionArea(selection_area);
|
|
}
|
|
|
|
else QGraphicsView::mouseMoveEvent(e);
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::mouseReleaseEvent
|
|
Manage event release click mouse
|
|
*/
|
|
void DiagramView::mouseReleaseEvent(QMouseEvent *e)
|
|
{
|
|
if (m_event_interface && m_event_interface->mouseReleaseEvent(e)) return;
|
|
|
|
// Stop drag view
|
|
if (e->button() == Qt::MiddleButton)
|
|
{
|
|
viewport()->setCursor(Qt::ArrowCursor);
|
|
}
|
|
else if (m_free_rubberbanding && !e->buttons())
|
|
{
|
|
if (viewportUpdateMode() != QGraphicsView::NoViewportUpdate)
|
|
{
|
|
if (viewportUpdateMode() != QGraphicsView::FullViewportUpdate)
|
|
{
|
|
QRectF r(mapFromScene(m_free_rubberband).boundingRect());
|
|
r.adjust(-10, -10, 10, 10);
|
|
viewport()->update(r.toRect());
|
|
}
|
|
else
|
|
{
|
|
update();
|
|
}
|
|
}
|
|
|
|
if (m_free_rubberband.count() > 3)
|
|
{
|
|
//Popup a menu with an action to create conductors between
|
|
//all selected terminals.
|
|
QAction *act = new QAction(tr("Connecter les bornes sélectionnées"), this);
|
|
QPolygonF polygon_ = m_free_rubberband;
|
|
connect(act, &QAction::triggered, [this, polygon_]()
|
|
{
|
|
ConductorCreator::create(m_diagram, polygon_);
|
|
diagram()->clearSelection();
|
|
});
|
|
QMenu *menu = new QMenu(this);
|
|
menu->addAction(act);
|
|
|
|
menu->popup(e->globalPosition().toPoint());
|
|
}
|
|
|
|
m_free_rubberbanding = false;
|
|
m_free_rubberband = QPolygon();
|
|
emit freeRubberBandChanged(m_free_rubberband);
|
|
e->accept();
|
|
}
|
|
else
|
|
QGraphicsView::mouseReleaseEvent(e);
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::gestures
|
|
@return
|
|
*/
|
|
bool DiagramView::gestures() const
|
|
{
|
|
QSettings settings;
|
|
return(settings.value("diagramview/gestures", false).toBool());
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::wheelEvent
|
|
Manage wheel event of mouse
|
|
@param event QWheelEvent
|
|
*/
|
|
void DiagramView::wheelEvent(QWheelEvent *event)
|
|
{
|
|
if (m_event_interface && m_event_interface->wheelEvent(event)) return;
|
|
|
|
//Zoom and scrolling
|
|
QPoint angle = event->angleDelta();
|
|
|
|
if (gestures()) //When gesture mode is enable, we suppose the wheel event are made from a trackpad.
|
|
{
|
|
if (event->modifiers() == Qt::ControlModifier) //zoom
|
|
{
|
|
qreal value = angle.y();
|
|
zoom(1 + value/1000);
|
|
}
|
|
else //scroll
|
|
{
|
|
horizontalScrollBar()->setValue(horizontalScrollBar()->value() - angle.x());
|
|
verticalScrollBar()->setValue(verticalScrollBar()->value() - angle.y());
|
|
}
|
|
}
|
|
else if (event->modifiers() == Qt::NoModifier) //Else we suppose the wheel event are made from a mouse.
|
|
{
|
|
qreal value = angle.y();
|
|
zoom(1 + value/1000);
|
|
}
|
|
else
|
|
QGraphicsView::wheelEvent(event);
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::gestureEvent
|
|
Use the pinch of the trackpad for zoom
|
|
@param event
|
|
@return
|
|
*/
|
|
bool DiagramView::gestureEvent(QGestureEvent *event)
|
|
{
|
|
if (QGesture *gesture = event->gesture(Qt::PinchGesture))
|
|
{
|
|
QPinchGesture *pinch = static_cast<QPinchGesture *>(gesture);
|
|
if (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged)
|
|
{
|
|
qreal value = gesture->property("scaleFactor").toReal();
|
|
value > 1 ? zoom(1.02) : zoom(0.98);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
Handles "Focus in" events. Reimplemented here to store the fact the focus
|
|
was freshly acquired again using the mouse. This information is later used
|
|
in DiagramView::mousePressEvent().
|
|
*/
|
|
void DiagramView::focusInEvent(QFocusEvent *e) {
|
|
if (e -> reason() == Qt::MouseFocusReason) {
|
|
m_fresh_focus_in = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::focusNextPrevChild
|
|
By default, QWidget intercepts Tab/Shift+Tab to move keyboard focus to
|
|
the next/previous widget before a key press event is ever generated,
|
|
which would silently swallow the diagram's Tab-based item-selection
|
|
cycling (see Diagram::event()). Returning false here disables that
|
|
automatic focus-chain traversal for this view, so Tab/Shift+Tab reach
|
|
keyPressEvent() (and from there, the scene) as ordinary key presses
|
|
instead.
|
|
@return always false
|
|
*/
|
|
bool DiagramView::focusNextPrevChild(bool next)
|
|
{
|
|
//Escape asked for focus to leave; allow exactly this one traversal.
|
|
if (m_releasing_focus) {
|
|
m_releasing_focus = false;
|
|
return QGraphicsView::focusNextPrevChild(next);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::keyPressEvent
|
|
Handles "key press" events. Reimplemented here to switch to visualisation
|
|
mode if needed.
|
|
@param e
|
|
*/
|
|
void DiagramView::keyPressEvent(QKeyEvent *e)
|
|
{
|
|
if (m_event_interface && m_event_interface->keyPressEvent(e))
|
|
return;
|
|
|
|
ProjectView *current_project = this->diagramEditor()->currentProjectView();
|
|
DiagramContent dc(m_diagram);
|
|
switch(e -> key())
|
|
{
|
|
case Qt::Key_Escape:
|
|
//Tab cycles the folio's items rather than moving focus (see
|
|
//focusNextPrevChild above), so without this there would be no
|
|
//way off the canvas for someone working without a mouse.
|
|
//Escape steps back out: first it drops the selection, then it
|
|
//hands focus to the next widget.
|
|
if (m_diagram && m_diagram->eventInterfaceIsRunning()) {
|
|
QGraphicsView::keyPressEvent(e); // let the active tool see it
|
|
return;
|
|
}
|
|
if (m_diagram && !m_diagram->selectedItems().isEmpty()) {
|
|
m_diagram->clearSelection();
|
|
} else {
|
|
m_releasing_focus = true;
|
|
focusNextChild();
|
|
}
|
|
return;
|
|
case Qt::Key_PageUp:
|
|
current_project->changeTabUp();
|
|
return;
|
|
case Qt::Key_PageDown:
|
|
current_project->changeTabDown();
|
|
return;
|
|
case Qt::Key_Home:
|
|
if (dc.selectedTexts().isEmpty()) {
|
|
if (
|
|
qgraphicsitem_cast<IndependentTextItem *>(m_diagram->focusItem()) ||
|
|
qgraphicsitem_cast<ConductorTextItem *>(m_diagram->focusItem()) ||
|
|
qgraphicsitem_cast<DiagramTextItem *>(m_diagram->focusItem())
|
|
)
|
|
break;
|
|
current_project->changeFirstTab();
|
|
return;
|
|
}
|
|
else break;
|
|
case Qt::Key_End:
|
|
if (dc.selectedTexts().isEmpty()) {
|
|
if (
|
|
qgraphicsitem_cast<IndependentTextItem *>(m_diagram->focusItem()) ||
|
|
qgraphicsitem_cast<ConductorTextItem *>(m_diagram->focusItem()) ||
|
|
qgraphicsitem_cast<DiagramTextItem *>(m_diagram->focusItem())
|
|
)
|
|
break;
|
|
current_project->changeLastTab();
|
|
return;
|
|
}
|
|
else break;
|
|
case Qt::Key_ZoomOut:
|
|
zoom(0.85);
|
|
return;
|
|
case Qt::Key_ZoomIn:
|
|
zoom(1.15);
|
|
return;
|
|
case Qt::Key_Minus: {
|
|
if (e->modifiers() & Qt::ControlModifier)
|
|
zoom(0.85);
|
|
}
|
|
break;
|
|
case Qt::Key_Plus: {
|
|
if (e->modifiers() & Qt::ControlModifier)
|
|
zoom(1.15);
|
|
}
|
|
break;
|
|
case Qt::Key_Up: {
|
|
if(!dc.items(DiagramContent::All).isEmpty() && !dc.hasTextEditing())
|
|
scrollOnMovement(e);
|
|
}
|
|
break;
|
|
case Qt::Key_Down: {
|
|
if(!dc.items(DiagramContent::All).isEmpty() && !dc.hasTextEditing())
|
|
scrollOnMovement(e);
|
|
}
|
|
break;
|
|
case Qt::Key_Left: {
|
|
if(!dc.items(DiagramContent::All).isEmpty() && !dc.hasTextEditing())
|
|
scrollOnMovement(e);
|
|
}
|
|
break;
|
|
case Qt::Key_Right: {
|
|
if(!dc.items(DiagramContent::All).isEmpty() && !dc.hasTextEditing())
|
|
scrollOnMovement(e);
|
|
}
|
|
break;
|
|
}
|
|
|
|
switchToVisualisationModeIfNeeded(e);
|
|
QGraphicsView::keyPressEvent(e);
|
|
}
|
|
|
|
/**
|
|
Handles "key release" events. Reimplemented here to switch to selection
|
|
mode if needed.
|
|
*/
|
|
void DiagramView::keyReleaseEvent(QKeyEvent *e) {
|
|
if (m_event_interface && m_event_interface->KeyReleaseEvent(e)) return;
|
|
|
|
switchToSelectionModeIfNeeded(e);
|
|
QGraphicsView::keyReleaseEvent(e);
|
|
}
|
|
|
|
/**
|
|
Handles element movement when editor is zoomed in and scrolls vertical
|
|
and horizontal bar. If element is moved to the right side of the editor
|
|
or below the editor SceneRect is expanded
|
|
*/
|
|
void DiagramView::scrollOnMovement(QKeyEvent *e)
|
|
{
|
|
const QList<QGraphicsItem *> selected_elmts = DiagramContent(m_diagram).items(DiagramContent::All);
|
|
QRectF viewed_scene = viewedSceneRect();
|
|
for (QGraphicsItem *qgi : selected_elmts)
|
|
{
|
|
if (qgraphicsitem_cast<Conductor *>(qgi))
|
|
continue;
|
|
if (qgi->parentItem() && qgi->parentItem()->isSelected())
|
|
continue;
|
|
|
|
qreal x = qgi->pos().x();
|
|
qreal y = qgi->pos().y();
|
|
qreal bottom = viewed_scene.bottom();
|
|
qreal top = viewed_scene.top();
|
|
qreal left = viewed_scene.left();
|
|
qreal right = viewed_scene.right();
|
|
qreal elmt_top = y + qgi->boundingRect().top();
|
|
qreal elmt_bottom = y + qgi->boundingRect().bottom();
|
|
qreal elmt_right = x + qgi->boundingRect().right();
|
|
qreal elmt_left = x + qgi->boundingRect().left();
|
|
if (qgi->parentItem()) {
|
|
elmt_top += qgi->parentItem()->y();
|
|
elmt_bottom += qgi->parentItem()->y();
|
|
elmt_right += qgi->parentItem()->x();
|
|
elmt_left += qgi->parentItem()->x();
|
|
}
|
|
|
|
bool elmt_right_of_left_margin = elmt_left>=left;
|
|
bool elmt_left_of_right_margin = elmt_right<=right;
|
|
bool elmt_below_top_margin = elmt_top>=top;
|
|
bool elmt_above_bottom_margin = elmt_bottom<=bottom;
|
|
|
|
if (!(elmt_right_of_left_margin && elmt_left_of_right_margin) ||
|
|
!(elmt_below_top_margin && elmt_above_bottom_margin ) )
|
|
{
|
|
QScrollBar *h = horizontalScrollBar();
|
|
QScrollBar *v = verticalScrollBar();
|
|
int h_increment=0;
|
|
int v_increment=0;
|
|
if (e->key()==Qt::Key_Up && elmt_above_bottom_margin) {
|
|
v_increment = 2*qgi->boundingRect().top();
|
|
if (v_increment == 0) v_increment = -2*qgi->boundingRect().height();
|
|
}
|
|
else if(e->key()==Qt::Key_Down && elmt_below_top_margin) {
|
|
v_increment = 2*qgi->boundingRect().bottom();
|
|
if (v_increment == 0) v_increment = -2*qgi->boundingRect().height();
|
|
}
|
|
else if (e->key()==Qt::Key_Left && elmt_left_of_right_margin) {
|
|
h_increment = 2*qgi->boundingRect().left();
|
|
if (h_increment == 0) h_increment = -2*qgi->boundingRect().width();
|
|
}
|
|
else if (e->key()==Qt::Key_Right && elmt_right_of_left_margin) {
|
|
h_increment = 2*qgi->boundingRect().right();
|
|
if (h_increment == 0) h_increment = -2*qgi->boundingRect().width();
|
|
}
|
|
if (((elmt_right >= m_diagram->sceneRect().right() - qgi->boundingRect().right()) ||
|
|
(elmt_bottom >= m_diagram->sceneRect().bottom() - qgi->boundingRect().bottom())) &&
|
|
(e->key()==Qt::Key_Right || e->key()==Qt::Key_Down)){
|
|
m_diagram->adjustSceneRect();
|
|
}
|
|
h -> setValue(h -> value() + h_increment);
|
|
v -> setValue(v -> value() + v_increment);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
@return le titre de cette vue ; cela correspond au titre du schema
|
|
visualise precede de la mention "Schema". Si le titre du schema est vide,
|
|
la mention "Schema sans titre" est utilisee
|
|
@see Diagram::title()
|
|
*/
|
|
QString DiagramView::title() const
|
|
{
|
|
QString view_title;
|
|
QString diagram_title(m_diagram -> title());
|
|
if (diagram_title.isEmpty()) {
|
|
view_title = tr("Sans titre", "what to display for untitled diagrams");
|
|
} else {
|
|
view_title = diagram_title;
|
|
}
|
|
return(view_title);
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::editDiagramProperties
|
|
Edit the properties of the viewed digram
|
|
*/
|
|
void DiagramView::editDiagramProperties()
|
|
{
|
|
DiagramPropertiesDialog::diagramPropertiesDialog(m_diagram, diagramEditor());
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::adjustSceneRect
|
|
Calcul and set the area of the scene visualized by this view
|
|
*/
|
|
void DiagramView::adjustSceneRect()
|
|
{
|
|
QRectF scene_rect = m_diagram->sceneRect();
|
|
scene_rect.adjust(-Diagram::margin, -Diagram::margin, Diagram::margin, Diagram::margin);
|
|
|
|
QSettings settings;
|
|
if (settings.value("diagrameditor/zoom-out-beyond-of-folio", false).toBool())
|
|
{
|
|
//When zoom out beyond of folio is active,
|
|
//we always adjust the scene rect to be 1/3 bigger than the wiewport
|
|
QRectF vpbr = mapToScene(viewport()->rect()).boundingRect();
|
|
vpbr.adjust(0, 0, vpbr.width()/3, vpbr.height()/3);
|
|
scene_rect = scene_rect.united(vpbr);
|
|
}
|
|
setSceneRect(scene_rect);
|
|
}
|
|
|
|
/**
|
|
Met a jour le titre du widget
|
|
*/
|
|
void DiagramView::updateWindowTitle()
|
|
{
|
|
emit(titleChanged(this, title()));
|
|
}
|
|
|
|
/**
|
|
Enables or disables the drawing grid according to the amount of pixels display
|
|
*/
|
|
void DiagramView::adjustGridToZoom()
|
|
{
|
|
QRectF viewed_scene = viewedSceneRect();
|
|
if (diagramEditor()->drawGrid())
|
|
m_diagram->setDisplayGrid(viewed_scene.width() < 2000 || viewed_scene.height() < 2000);
|
|
else
|
|
m_diagram->setDisplayGrid(false);
|
|
}
|
|
|
|
/**
|
|
@return le rectangle du schema (classe Diagram) visualise par ce DiagramView
|
|
*/
|
|
QRectF DiagramView::viewedSceneRect() const
|
|
{
|
|
// recupere la taille du widget viewport
|
|
QSize viewport_size = viewport() -> size();
|
|
|
|
// recupere la transformation viewport -> scene
|
|
QTransform view_to_scene = viewportTransform().inverted();
|
|
|
|
// mappe le coin superieur gauche et le coin inferieur droit de la viewport sur la scene
|
|
QPointF scene_left_top = view_to_scene.map(QPointF(0.0, 0.0));
|
|
QPointF scene_right_bottom = view_to_scene.map(QPointF(viewport_size.width(), viewport_size.height()));
|
|
|
|
// en deduit le rectangle visualise par la scene
|
|
return(QRectF(scene_left_top, scene_right_bottom));
|
|
}
|
|
|
|
/**
|
|
@param tbt_loc A title block template location
|
|
@return true if the title block template needs to be integrated in the
|
|
parent project before being applied to the current diagram, or false if it
|
|
can be directly applied
|
|
*/
|
|
bool DiagramView::mustIntegrateTitleBlockTemplate(const TitleBlockTemplateLocation &tbt_loc) const
|
|
{
|
|
// unlike elements, the integration of title block templates is mandatory, so we simply check whether the parent project of the template is also the parent project of the diagram
|
|
QETProject *tbt_parent_project = tbt_loc.parentProject();
|
|
if (!tbt_parent_project) return(true);
|
|
|
|
return(tbt_parent_project != m_diagram -> project());
|
|
}
|
|
|
|
/**
|
|
Fait en sorte que le schema ne soit editable que s'il n'est pas en lecture
|
|
seule
|
|
*/
|
|
void DiagramView::applyReadOnly()
|
|
{
|
|
if (!m_diagram) return;
|
|
|
|
bool is_writable = !m_diagram -> isReadOnly();
|
|
setInteractive(is_writable);
|
|
setAcceptDrops(is_writable);
|
|
}
|
|
|
|
/**
|
|
Reinitialise le profil des conducteurs selectionnes
|
|
*/
|
|
void DiagramView::resetConductors()
|
|
{
|
|
if (m_diagram -> isReadOnly()) return;
|
|
// recupere les conducteurs selectionnes
|
|
QSet<Conductor *> selected_conductors = m_diagram -> selectedConductors();
|
|
|
|
// repere les conducteurs modifies (= profil non nul)
|
|
QHash<Conductor *, ConductorProfilesGroup> conductors_and_profiles;
|
|
foreach(Conductor *conductor, selected_conductors) {
|
|
ConductorProfilesGroup profile = conductor -> profiles();
|
|
if (
|
|
!profile[Qt::TopLeftCorner].isNull() ||\
|
|
!profile[Qt::TopRightCorner].isNull() ||\
|
|
!profile[Qt::BottomLeftCorner].isNull() ||\
|
|
!profile[Qt::BottomRightCorner].isNull()
|
|
) {
|
|
conductors_and_profiles.insert(conductor, profile);
|
|
}
|
|
}
|
|
|
|
if (conductors_and_profiles.isEmpty()) return;
|
|
m_diagram -> undoStack().push(new ResetConductorCommand(conductors_and_profiles));
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::event
|
|
Manage the event on this diagram view.
|
|
-At first activation (QEvent::WindowActivate or QEvent::Show) we zoomFit.
|
|
-Convert event interpreted to mouse event to gesture event if needed.
|
|
-send Shortcut to view (by default send to QMenu /QAction)
|
|
Gere les evenements de la DiagramView
|
|
@param e : the event.
|
|
@return
|
|
*/
|
|
bool DiagramView::event(QEvent *e) {
|
|
if (Q_UNLIKELY(m_first_activation)) {
|
|
if (e -> type() == QEvent::Show) {
|
|
zoomFit();
|
|
m_first_activation = false;
|
|
}
|
|
}
|
|
// By default touch events are converted to mouse events. So
|
|
// after this event we will get a mouse event also but we want
|
|
// to handle touch events as gestures only. So we need this safeguard
|
|
// to block mouse events that are actually generated from touch.
|
|
if (e->type() == QEvent::Gesture)
|
|
return gestureEvent(static_cast<QGestureEvent *>(e));
|
|
|
|
// fait en sorte que les raccourcis clavier arrivent prioritairement sur la
|
|
// vue plutot que de remonter vers les QMenu / QAction
|
|
if (
|
|
e -> type() == QEvent::ShortcutOverride &&
|
|
selectedItemHasFocus()
|
|
) {
|
|
e -> accept();
|
|
return(true);
|
|
}
|
|
return(QGraphicsView::event(e));
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::paintingInverted
|
|
Reimplemented from PaletteGraphicsView: tell the diagram it is being
|
|
drawn for an inverted display, so it softens its grid.
|
|
*/
|
|
void DiagramView::paintingInverted(bool inverted)
|
|
{
|
|
m_diagram->setInvertedLightness(inverted);
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::setCellRulersShown
|
|
Show or hide the rulers that keep the column numbers and the row
|
|
letters of the folio border in sight along the edges of this view.
|
|
@param shown
|
|
*/
|
|
void DiagramView::setCellRulersShown(bool shown)
|
|
{
|
|
m_cell_rulers_shown = shown;
|
|
updateCellRulers();
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::updateCellRulers
|
|
Show each ruler when the rulers are wanted, the folio shows the
|
|
matching header and that header is not already wholly in sight, and
|
|
give it room in the margins of the view. The drawing does not move on
|
|
screen when a ruler comes or goes: the ruler covers or uncovers the
|
|
edge of the viewport, as if it lay over it.
|
|
*/
|
|
void DiagramView::updateCellRulers()
|
|
{
|
|
const BorderTitleBlock &border = m_diagram->border_and_titleblock;
|
|
const QRectF in_sight = mapToScene(viewport()->rect()).boundingRect();
|
|
const QRectF columns = border.columnsRect();
|
|
const QRectF rows = border.rowsRect();
|
|
const bool top = m_cell_rulers_shown
|
|
&& border.borderIsDisplayed() && border.columnsAreDisplayed()
|
|
&& (columns.top() < in_sight.top() || columns.bottom() > in_sight.bottom());
|
|
const bool side = m_cell_rulers_shown
|
|
&& border.borderIsDisplayed() && border.rowsAreDisplayed()
|
|
&& (rows.left() < in_sight.left() || rows.right() > in_sight.right());
|
|
const int thickness = m_top_ruler->thickness();
|
|
|
|
m_top_ruler->setVisible(top);
|
|
m_side_ruler->setVisible(side);
|
|
m_side_ruler->setLeadingSpace(top ? thickness : 0);
|
|
|
|
const QMargins margins(side ? thickness : 0, top ? thickness : 0, 0, 0);
|
|
if (margins != viewportMargins()) {
|
|
const QPointF origin = mapToScene(viewport()->rect().center());
|
|
const QPoint before = viewport()->mapToGlobal(mapFromScene(origin));
|
|
setViewportMargins(margins);
|
|
const QPoint moved = viewport()->mapToGlobal(mapFromScene(origin)) - before;
|
|
horizontalScrollBar()->setValue(horizontalScrollBar()->value() + moved.x());
|
|
verticalScrollBar()->setValue(verticalScrollBar()->value() + moved.y());
|
|
}
|
|
placeCellRulers();
|
|
m_top_ruler->update();
|
|
m_side_ruler->update();
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::placeCellRulers
|
|
Lay the rulers along the top and the left edges of the viewport, the
|
|
side ruler covering the corner too when both are shown.
|
|
*/
|
|
void DiagramView::placeCellRulers()
|
|
{
|
|
if (!m_top_ruler) {
|
|
return;
|
|
}
|
|
const QRect viewport_rect = viewport()->geometry();
|
|
const int thickness = m_top_ruler->thickness();
|
|
const int corner = m_top_ruler->isHidden() ? 0 : thickness;
|
|
m_top_ruler->setGeometry(viewport_rect.left(), viewport_rect.top() - thickness,
|
|
viewport_rect.width(), thickness);
|
|
m_side_ruler->setGeometry(viewport_rect.left() - thickness, viewport_rect.top() - corner,
|
|
thickness, viewport_rect.height() + corner);
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::viewportEvent
|
|
Keep the rulers along the viewport when it resizes, which it also does
|
|
without the view resizing, when the scroll bars come and go.
|
|
@param event
|
|
@return what QGraphicsView::viewportEvent() returns
|
|
*/
|
|
bool DiagramView::viewportEvent(QEvent *event)
|
|
{
|
|
if (event->type() == QEvent::Resize) {
|
|
placeCellRulers();
|
|
}
|
|
return PaletteGraphicsView::viewportEvent(event);
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::paintEvent
|
|
Reimplemented from QGraphicsView
|
|
@param event
|
|
*/
|
|
void DiagramView::paintEvent(QPaintEvent *event)
|
|
{
|
|
PaletteGraphicsView::paintEvent(event);
|
|
|
|
//Scrolling and zooming both repaint the viewport: follow them.
|
|
//Showing or hiding a ruler resizes the viewport, which cannot be
|
|
//done while it paints.
|
|
if (viewportTransform() != m_rulers_transform) {
|
|
m_rulers_transform = viewportTransform();
|
|
m_top_ruler->update();
|
|
m_side_ruler->update();
|
|
if (m_cell_rulers_shown) {
|
|
QMetaObject::invokeMethod(this, &DiagramView::updateCellRulers,
|
|
Qt::QueuedConnection);
|
|
}
|
|
}
|
|
|
|
if (m_free_rubberbanding && m_free_rubberband.count() >= 3)
|
|
{
|
|
QPainter painter(viewport());
|
|
painter.setRenderHint(QPainter::Antialiasing);
|
|
QPen pen(Qt::darkGreen);
|
|
pen.setWidth(1);
|
|
painter.setPen(pen);
|
|
QColor color(Qt::darkGreen);
|
|
color.setAlpha(50);
|
|
QBrush brush(color);
|
|
painter.setBrush(brush);
|
|
painter.drawPolygon(mapFromScene(m_free_rubberband));
|
|
}
|
|
}
|
|
|
|
/**
|
|
Switch to visualisation mode if the user is pressing Ctrl and Shift.
|
|
@return true if the view was switched to visualisation mode, false
|
|
otherwise.
|
|
*/
|
|
bool DiagramView::switchToVisualisationModeIfNeeded(QInputEvent *e) {
|
|
if (isCtrlShifting(e) && !selectedItemHasFocus()) {
|
|
if (dragMode() != QGraphicsView::ScrollHandDrag) {
|
|
setVisualisationMode();
|
|
return(true);
|
|
}
|
|
}
|
|
return(false);
|
|
}
|
|
|
|
/**
|
|
Switch back to selection mode if the user is not pressing Ctrl and Shift.
|
|
@return true if the view was switched to selection mode, false
|
|
otherwise.
|
|
*/
|
|
bool DiagramView::switchToSelectionModeIfNeeded(QInputEvent *e) {
|
|
if (!selectedItemHasFocus() && !isCtrlShifting(e)) {
|
|
setSelectionMode();
|
|
return(true);
|
|
}
|
|
return(false);
|
|
}
|
|
|
|
/**
|
|
@return true if the user is pressing Ctrl and Shift simultaneously.
|
|
*/
|
|
bool DiagramView::isCtrlShifting(QInputEvent *e) {
|
|
bool result = false;
|
|
// note: QInputEvent::modifiers and QKeyEvent::modifiers() do not return the
|
|
// same values, hence the casts
|
|
if (e -> type() == QEvent::KeyPress || e -> type() == QEvent::KeyRelease) {
|
|
if (QKeyEvent *ke = static_cast<QKeyEvent *>(e)) {
|
|
result = (ke -> modifiers() == (Qt::ControlModifier | Qt::ShiftModifier));
|
|
}
|
|
} else if (e -> type() >= QEvent::MouseButtonPress && e -> type() <= QEvent::MouseMove) {
|
|
if (QMouseEvent *me = static_cast<QMouseEvent *>(e)) {
|
|
result = (me -> modifiers() == (Qt::ControlModifier | Qt::ShiftModifier));
|
|
}
|
|
}
|
|
return(result);
|
|
}
|
|
|
|
/**
|
|
@return true if there is a selected item and that item has the focus.
|
|
*/
|
|
bool DiagramView::selectedItemHasFocus()
|
|
{
|
|
return(
|
|
m_diagram -> hasFocus() &&
|
|
m_diagram -> focusItem() &&
|
|
m_diagram -> focusItem() -> isSelected()
|
|
);
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::editSelection
|
|
Edit the selected item if he can be edited and if only one item is selected
|
|
*/
|
|
void DiagramView::editSelection()
|
|
{
|
|
if (m_diagram -> isReadOnly() || m_diagram -> selectedItems().size() != 1 ) return;
|
|
|
|
QGraphicsItem *item = m_diagram->selectedItems().first();
|
|
|
|
//We use dynamic_cast instead of qgraphicsitem_cast for QetGraphicsItem
|
|
//because they haven't got their own type().
|
|
//Use qgraphicsitem_cast will have weird behavior for this class.
|
|
if (IndependentTextItem *iti = qgraphicsitem_cast<IndependentTextItem *>(item))
|
|
iti -> edit();
|
|
else if (QetGraphicsItem *qgi = dynamic_cast<QetGraphicsItem *> (item))
|
|
qgi -> editProperty();
|
|
else if (Conductor *c = qgraphicsitem_cast<Conductor *>(item))
|
|
c -> editProperty();
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::setEventInterface
|
|
Set an event interface to diagram view.
|
|
If diagram view already have an event interface, he delete it before.
|
|
Diagram view take ownership of event interface and delete it when event interface is finish
|
|
*/
|
|
void DiagramView::setEventInterface(DVEventInterface *event_interface)
|
|
{
|
|
if (m_event_interface) delete m_event_interface;
|
|
m_event_interface = event_interface;
|
|
connect(m_event_interface, &DVEventInterface::finish, this, [=](){delete this->m_event_interface; this->m_event_interface = nullptr;}, Qt::QueuedConnection);
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::contextMenuActions
|
|
@return a list of actions currently available for a context menu.
|
|
|
|
*/
|
|
QList<QAction *> DiagramView::contextMenuActions() const
|
|
{
|
|
QList<QAction *> list;
|
|
if (QETDiagramEditor *qde = diagramEditor())
|
|
{
|
|
if (m_diagram->selectedItems().isEmpty())
|
|
{
|
|
//Drawing comes first. The row and column actions change
|
|
//the folio's layout and are rarely wanted, so they sit one
|
|
//level down where a stray click cannot reach them.
|
|
list << m_paste_here;
|
|
list << m_separators.at(0);
|
|
list << qde->m_add_item_menu->menuAction();
|
|
list << m_separators.at(1);
|
|
list << qde->m_edit_diagram_properties;
|
|
list << qde->m_row_column_menu->menuAction();
|
|
}
|
|
else
|
|
{
|
|
list << qde->m_cut;
|
|
list << qde->m_copy;
|
|
list << m_multi_paste;
|
|
list << m_separators.at(0);
|
|
list << m_create_template; // Add the create template action
|
|
list << qde->m_conductor_reset;
|
|
list << m_separators.at(1);
|
|
list << qde->m_selection_actions_group.actions();
|
|
list << m_separators.at(2);
|
|
list << qde->m_depth_action_group->actions();
|
|
}
|
|
|
|
//Remove from the context menu the actions which are disabled,
|
|
//and the submenus in which every action is disabled.
|
|
const QList<QAction *> actions = list;
|
|
for(QAction *action : actions)
|
|
{
|
|
bool usable = action->isEnabled();
|
|
if (usable && action->menu())
|
|
{
|
|
const QList<QAction *> sub_actions = action->menu()->actions();
|
|
usable = std::any_of(sub_actions.cbegin(), sub_actions.cend(),
|
|
[](QAction *a) { return a->isEnabled(); });
|
|
}
|
|
if (!usable) {
|
|
list.removeAll(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::contextMenuEvent
|
|
@param e
|
|
*/
|
|
void DiagramView::contextMenuEvent(QContextMenuEvent *e)
|
|
{
|
|
QPoint menu_pos = e->pos();
|
|
QPoint menu_global_pos = e->globalPos();
|
|
|
|
//A context menu raised from the keyboard (the Menu key, or
|
|
//Shift+F10) carries no useful position: Qt does not aim it at the
|
|
//selection. Two things then went wrong. QGraphicsView handed the
|
|
//event to whichever item held focus, which answered with its own
|
|
//generic Undo/Cut/Copy menu and accepted it, so the folio's real
|
|
//menu was never built; and had it got past that, itemAt() below
|
|
//would have looked up an unrelated point.
|
|
//
|
|
//So a keyboard-raised menu is built here directly rather than being
|
|
//offered to the items first, and aimed at the selection when there
|
|
//is one. The keyboard then gets the folio's menu, which is what a
|
|
//right-click gets.
|
|
const bool from_keyboard = e->reason() == QContextMenuEvent::Keyboard;
|
|
|
|
if (from_keyboard)
|
|
{
|
|
//Aim at the selection when there is one, so the menu appears
|
|
//beside what it acts on. With nothing selected there is nothing
|
|
//to aim at, so use the middle of the view -- the folio's own
|
|
//menu is still the right menu to show.
|
|
const auto selection = m_diagram->selectedItems();
|
|
if (!selection.isEmpty())
|
|
{
|
|
QRectF selection_rect;
|
|
for (auto *item : selection) {
|
|
selection_rect |= item->sceneBoundingRect();
|
|
}
|
|
menu_pos = mapFromScene(selection_rect.center());
|
|
}
|
|
else
|
|
{
|
|
menu_pos = viewport()->rect().center();
|
|
}
|
|
menu_global_pos = viewport()->mapToGlobal(menu_pos);
|
|
}
|
|
else
|
|
{
|
|
QGraphicsView::contextMenuEvent(e);
|
|
if(e->isAccepted())
|
|
return;
|
|
|
|
if (auto qgi = m_diagram->itemAt(mapToScene(menu_pos), transform()))
|
|
{
|
|
if (!qgi->isSelected()) {
|
|
m_diagram->clearSelection();
|
|
}
|
|
|
|
// At this step qgi can be deleted for example if qgi is a QetGraphicsHandlerItem.
|
|
// When we call clearSelection the parent item of the handler
|
|
// is deselected and so delete all handlers, in this case,
|
|
// qgi become a dangling pointer.
|
|
// we need to call again itemAt.
|
|
if (auto item_ = m_diagram->itemAt(mapToScene(menu_pos), transform())) {
|
|
item_->setSelected(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_diagram->selectedItems().isEmpty())
|
|
{
|
|
m_paste_here_pos = menu_pos;
|
|
m_paste_here->setEnabled(Diagram::clipboardMayContainDiagram());
|
|
}
|
|
|
|
QList <QAction *> list = contextMenuActions();
|
|
if(!list.isEmpty())
|
|
{
|
|
QMenu *context_menu = new QMenu(this);
|
|
context_menu->addActions(list);
|
|
context_menu->popup(menu_global_pos);
|
|
e->accept();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief DiagramView::createTemplateFromSelection
|
|
* Triggered from the context menu to create a new template (macro) from the current selection.
|
|
*/
|
|
void DiagramView::createTemplateFromSelection()
|
|
{
|
|
QList<QGraphicsItem *> selected_elements = m_diagram->selectedItems();
|
|
|
|
if (selected_elements.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
qDebug() << "Ready to create a template from" << selected_elements.size() << "elements!";
|
|
|
|
// Open the dialog to let the user select where to save the .qetmak file
|
|
ElementsLocation template_location = ElementDialog::getSaveTemplateLocation(this);
|
|
|
|
// Check if the user clicked 'Cancel' or closed the window
|
|
if (template_location.isNull()) {
|
|
qDebug() << "User canceled template creation.";
|
|
return;
|
|
}
|
|
|
|
qDebug() << "Will save template to:" << template_location.path();
|
|
|
|
QDomDocument content_xml = m_diagram->toXml(false, true);
|
|
|
|
QDomDocument macro_doc;
|
|
QDomElement root = macro_doc.createElement("qet_macro");
|
|
macro_doc.appendChild(root);
|
|
|
|
QDomElement collection_node = macro_doc.createElement("collection");
|
|
root.appendChild(collection_node);
|
|
|
|
QSet<QString> processed_types;
|
|
|
|
QDomNodeList element_nodes = content_xml.elementsByTagName("element");
|
|
for (int i = 0; i < element_nodes.count(); ++i) {
|
|
QDomElement elmt_node = element_nodes.at(i).toElement();
|
|
QString old_type = elmt_node.attribute("type");
|
|
|
|
if (old_type.isEmpty()) continue;
|
|
|
|
ElementsLocation loc(old_type, m_diagram->project());
|
|
|
|
QString clean_path = loc.collectionPath(false);
|
|
|
|
QString new_type = "macro://" + clean_path;
|
|
elmt_node.setAttribute("type", new_type);
|
|
|
|
if (!processed_types.contains(clean_path)) {
|
|
processed_types.insert(clean_path);
|
|
|
|
QDomElement definition_node = loc.xml();
|
|
|
|
if (!definition_node.isNull()) {
|
|
QDomElement collection_elmt = macro_doc.createElement("element");
|
|
collection_elmt.setAttribute("path", clean_path);
|
|
|
|
QDomNode imported_def = macro_doc.importNode(definition_node, true);
|
|
collection_elmt.appendChild(imported_def);
|
|
|
|
collection_node.appendChild(collection_elmt);
|
|
} else {
|
|
qDebug() << "Warning: could not load XML definition for" << old_type;
|
|
}
|
|
}
|
|
}
|
|
|
|
QDomElement content_container = macro_doc.createElement("diagram_content");
|
|
root.appendChild(content_container);
|
|
|
|
QDomNode imported_node = macro_doc.importNode(content_xml.documentElement(), true);
|
|
content_container.appendChild(imported_node);
|
|
|
|
QString full_path = template_location.fileSystemPath();
|
|
|
|
QDir().mkpath(QFileInfo(full_path).absolutePath());
|
|
|
|
QFile file(full_path);
|
|
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
|
QTextStream out(&file);
|
|
out << macro_doc.toString(4);
|
|
file.close();
|
|
qDebug() << "Template successfully saved to:" << full_path;
|
|
|
|
QMessageBox::information(this, tr("Modèle enregistré"),
|
|
tr("Le modèle a été enregistré avec succès sous :\n%1").arg(full_path));
|
|
} else {
|
|
qDebug() << "Error: Could not open file for writing:" << full_path;
|
|
QMessageBox::critical(this, tr("Erreur"), tr("Le fichier n'a pas pu être écrit."));
|
|
}
|
|
}
|
|
|
|
/**
|
|
@return l'editeur de schemas parent ou 0
|
|
*/
|
|
QETDiagramEditor *DiagramView::diagramEditor() const
|
|
{
|
|
// remonte la hierarchie des widgets
|
|
QWidget *w = const_cast<DiagramView *>(this);
|
|
while (w -> parentWidget() && !w -> isWindow()) {
|
|
w = w -> parentWidget();
|
|
}
|
|
// la fenetre est supposee etre un QETDiagramEditor
|
|
return(qobject_cast<QETDiagramEditor *>(w));
|
|
}
|
|
|
|
/**
|
|
@brief DiagramView::mouseDoubleClickEvent
|
|
@param e
|
|
*/
|
|
void DiagramView::mouseDoubleClickEvent(QMouseEvent *e)
|
|
{
|
|
if (m_event_interface && m_event_interface -> mouseDoubleClickEvent(e)) return;
|
|
|
|
BorderTitleBlock &bi = m_diagram -> border_and_titleblock;
|
|
|
|
//Get the click pos on the diagram
|
|
QPointF click_pos = viewportTransform().inverted().map(e -> pos());
|
|
|
|
if (bi.titleBlockRect().contains(click_pos) || bi.columnsRect().contains(click_pos) || bi.rowsRect().contains(click_pos)) {
|
|
e->accept();
|
|
editDiagramProperties();
|
|
return;
|
|
}
|
|
QGraphicsView::mouseDoubleClickEvent(e);
|
|
}
|