1 /*
2 Copyright 2006-2019 The QElectroTech Team
3 This file is part of QElectroTech.
4
5 QElectroTech is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 2 of the License, or
8 (at your option) any later version.
9
10 QElectroTech is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
17 */
18 #include "diagramview.h"
19 #include "diagram.h"
20 #include "qetgraphicsitem/conductor.h"
21 #include "diagramcommands.h"
22 #include "diagramposition.h"
23 #include "conductorpropertieswidget.h"
24 #include "qetgraphicsitem/conductortextitem.h"
25 #include "qetgraphicsitem/independenttextitem.h"
26 #include "qetgraphicsitem/diagramimageitem.h"
27 #include "templatelocation.h"
28 #include "qetproject.h"
29 #include "projectview.h"
30 #include "integrationmovetemplateshandler.h"
31 #include "qetdiagrameditor.h"
32 #include "qeticons.h"
33 #include "qetmessagebox.h"
34 #include <QGraphicsObject>
35 #include <QGraphicsPixmapItem>
36 #include <QGraphicsSceneMouseEvent>
37 #include "factory/elementfactory.h"
38 #include "diagrampropertiesdialog.h"
39 #include "dveventinterface.h"
40 #include "diagrameventaddelement.h"
41 #include "QPropertyUndoCommand/qpropertyundocommand.h"
42 #include "qetshapeitem.h"
43 #include "undocommand/deleteqgraphicsitemcommand.h"
44 #include "dynamicelementtextitem.h"
45 #include "multipastedialog.h"
46 #include "changetitleblockcommand.h"
47 #include "conductorcreator.h"
48
49 /**
50 Constructeur
51 @param diagram Schema a afficher ; si diagram vaut 0, un nouveau Diagram est utilise
52 @param parent Le QWidget parent de cette vue de schema
53 */
DiagramView(Diagram * diagram,QWidget * parent)54 DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
55 QGraphicsView (parent),
56 m_diagram (diagram)
57 {
58 grabGesture(Qt::PinchGesture);
59 setAttribute(Qt::WA_DeleteOnClose, true);
60 setInteractive(true);
61
62 QString whatsthis = tr(
63 "Ceci est la zone dans laquelle vous concevez vos schémas en y ajoutant"
64 " des éléments et en posant des conducteurs entre leurs bornes. Il est"
65 " également possible d'ajouter des textes indépendants.",
66 "\"What's this?\" tip"
67 );
68 setWhatsThis(whatsthis);
69
70 // active l'antialiasing
71 setRenderHint(QPainter::Antialiasing, true);
72 setRenderHint(QPainter::TextAntialiasing, true);
73 setRenderHint(QPainter::SmoothPixmapTransform, true);
74
75 setScene(m_diagram);
76 m_diagram -> undoStack().setClean();
77 setWindowIcon(QET::Icons::QETLogo);
78 setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
79 setResizeAnchor(QGraphicsView::AnchorUnderMouse);
80 setAlignment(Qt::AlignLeft | Qt::AlignTop);
81 setSelectionMode();
82 adjustSceneRect();
83 updateWindowTitle();
84 m_diagram->loadElmtFolioSeq();
85 m_diagram->loadCndFolioSeq();
86
87 m_paste_here = new QAction(QET::Icons::EditPaste, tr("Coller ici", "context menu action"), this);
88 connect(m_paste_here, SIGNAL(triggered()), this, SLOT(pasteHere()));
89
90 m_multi_paste = new QAction(QET::Icons::EditPaste, tr("Collage multiple"), this);
91 connect(m_multi_paste, &QAction::triggered, [this]() {
92 MultiPasteDialog d(this->m_diagram, this);
93 d.exec();
94 });
95
96 //setup three separators, to be use in context menu
97 for(int i=0 ; i<3 ; ++i)
98 {
99 m_separators << new QAction(this);
100 m_separators.last()->setSeparator(true);
101 }
102
103 connect(m_diagram, SIGNAL(showDiagram(Diagram*)), this, SIGNAL(showDiagram(Diagram*)));
104 connect(m_diagram, SIGNAL(selectionChanged()), this, SIGNAL(selectionChanged()));
105 connect(m_diagram, SIGNAL(sceneRectChanged(QRectF)), this, SLOT(adjustSceneRect()));
106 connect(&(m_diagram -> border_and_titleblock), SIGNAL(diagramTitleChanged(const QString &)), this, SLOT(updateWindowTitle()));
107 connect(diagram, SIGNAL(editElementRequired(ElementsLocation)), this, SIGNAL(editElementRequired(ElementsLocation)));
108 connect(diagram, SIGNAL(findElementRequired(ElementsLocation)), this, SIGNAL(findElementRequired(ElementsLocation)));
109
110 QShortcut *edit_conductor_color_shortcut = new QShortcut(QKeySequence(Qt::Key_F2), this);
111 connect(edit_conductor_color_shortcut, SIGNAL(activated()), this, SLOT(editSelectedConductorColor()));
112 }
113
114 /**
115 Destructeur
116 */
~DiagramView()117 DiagramView::~DiagramView() {
118 }
119
120 /**
121 Selectionne tous les objets du schema
122 */
selectAll()123 void DiagramView::selectAll() {
124 m_diagram -> selectAll();
125 }
126
127 /**
128 Deslectionne tous les objets selectionnes
129 */
selectNothing()130 void DiagramView::selectNothing() {
131 m_diagram -> deselectAll();
132 }
133
134 /**
135 Inverse l'etat de selection de tous les objets du schema
136 */
selectInvert()137 void DiagramView::selectInvert() {
138 m_diagram -> invertSelection();
139 }
140
141 /**
142 Accepte ou refuse le drag'n drop en fonction du type de donnees entrant
143 @param e le QDragEnterEvent correspondant au drag'n drop tente
144 */
dragEnterEvent(QDragEnterEvent * e)145 void DiagramView::dragEnterEvent(QDragEnterEvent *e) {
146 if (e -> mimeData() -> hasFormat("application/x-qet-element-uri")) {
147 e -> acceptProposedAction();
148 } else if (e -> mimeData() -> hasFormat("application/x-qet-titleblock-uri")) {
149 e -> acceptProposedAction();
150 } else if (e -> mimeData() -> hasText()) {
151 e -> acceptProposedAction();
152 } else {
153 e -> ignore();
154 }
155 }
156
157 /**
158 Accepte ou refuse le drag'n drop en fonction du type de donnees entrant
159 @param e le QDragMoveEvent correspondant au drag'n drop tente
160 */
dragMoveEvent(QDragMoveEvent * e)161 void DiagramView::dragMoveEvent(QDragMoveEvent *e) {
162 if (e -> mimeData() -> hasFormat("text/plain")) e -> acceptProposedAction();
163 else e-> ignore();
164 }
165
166 /**
167 Handle the drops accepted on diagram (elements and title block templates).
168 @param e the QDropEvent describing the current drag'n drop
169 */
dropEvent(QDropEvent * e)170 void DiagramView::dropEvent(QDropEvent *e) {
171
172 if (e -> mimeData() -> hasFormat("application/x-qet-element-uri")) {
173 handleElementDrop(e);
174 } else if (e -> mimeData() -> hasFormat("application/x-qet-titleblock-uri")) {
175 handleTitleBlockDrop(e);
176 } else if (e -> mimeData() -> hasText()) {
177 handleTextDrop(e);
178 }
179 }
180
181 /**
182 Handle the drop of an element.
183 @param e the QDropEvent describing the current drag'n drop
184 */
handleElementDrop(QDropEvent * event)185 void DiagramView::handleElementDrop(QDropEvent *event)
186 {
187 //Build an element from the text of the mime data
188 ElementsLocation location(event->mimeData()->text());
189
190 if ( !(location.isElement() && location.exist()) )
191 {
192 qDebug() << "DiagramView::handleElementDrop, location can't be use : " << location;
193 return;
194 }
195
196 diagram()->setEventInterface(new DiagramEventAddElement(location, diagram(), mapToScene(event->pos())));
197 //Set focus to the view to get event
198 this->setFocus();
199 }
200
201 /**
202 * @brief DiagramView::handleTitleBlockDrop
203 * Handle the dropEvent that contain data of a titleblock
204 * @param e
205 */
handleTitleBlockDrop(QDropEvent * e)206 void DiagramView::handleTitleBlockDrop(QDropEvent *e) {
207 // fetch the title block template location from the drop event
208 TitleBlockTemplateLocation tbt_loc;
209 tbt_loc.fromString(e->mimeData()->text());
210
211
212 if (tbt_loc.isValid())
213 {
214 // fetch the current title block properties
215 TitleBlockProperties titleblock_properties_before = m_diagram->border_and_titleblock.exportTitleBlock();
216
217 // check the provided template is not already applied
218 QETProject *tbt_parent_project = tbt_loc.parentProject();
219 if (tbt_parent_project && tbt_parent_project == m_diagram -> project())
220 {
221 // same parent project and same name = same title block template
222 if (tbt_loc.name() == titleblock_properties_before.template_name)
223 return;
224 }
225
226 // integrate the provided template into the project if needed
227 QString integrated_template_name = tbt_loc.name();
228 if (mustIntegrateTitleBlockTemplate(tbt_loc))
229 {
230 IntegrationMoveTitleBlockTemplatesHandler *handler = new IntegrationMoveTitleBlockTemplatesHandler(this);
231 //QString error_message;
232 integrated_template_name = m_diagram->project()->integrateTitleBlockTemplate(tbt_loc, handler);
233
234 if (integrated_template_name.isEmpty())
235 return;
236 }
237
238 // apply the provided title block template
239 if (titleblock_properties_before.template_name == integrated_template_name)
240 return;
241
242 TitleBlockProperties titleblock_properties_after = titleblock_properties_before;
243 titleblock_properties_after.template_name = integrated_template_name;
244 m_diagram->undoStack().push(new ChangeTitleBlockCommand(m_diagram, titleblock_properties_before, titleblock_properties_after));
245
246 adjustSceneRect();
247 }
248 }
249
250 /**
251 * @brief DiagramView::handleTextDrop
252 *handle the drop of text
253 * @param e the QDropEvent describing the current drag'n drop
254 */
handleTextDrop(QDropEvent * e)255 void DiagramView::handleTextDrop(QDropEvent *e) {
256 if (m_diagram -> isReadOnly() || (e -> mimeData() -> hasText() == false) ) return;
257
258 IndependentTextItem *iti = new IndependentTextItem (e -> mimeData() -> text());
259
260 if (e -> mimeData() -> hasHtml()) {
261 iti -> setHtml (e -> mimeData() -> text());
262 }
263
264 m_diagram -> undoStack().push(new AddItemCommand<IndependentTextItem *>(iti, m_diagram, mapToScene(e->pos())));
265 }
266
267 /**
268 Set the Diagram in visualisation mode
269 */
setVisualisationMode()270 void DiagramView::setVisualisationMode() {
271 setDragMode(ScrollHandDrag);
272 applyReadOnly();
273 setInteractive(false);
274 emit(modeChanged());
275 }
276
277 /**
278 Set the Diagram in Selection mode
279 */
setSelectionMode()280 void DiagramView::setSelectionMode() {
281 setDragMode(RubberBandDrag);
282 setInteractive(true);
283 applyReadOnly();
284 emit(modeChanged());
285 }
286
287 /**
288 * @brief DiagramView::zoom
289 * Zomm the view.
290 * A zoom_factor > 1 zoom in.
291 * A zoom_factor < 1 zoom out
292 * @param zoom_factor
293 */
zoom(const qreal zoom_factor)294 void DiagramView::zoom(const qreal zoom_factor)
295 {
296 if (zoom_factor >= 1){
297 scale(zoom_factor, zoom_factor);
298 }
299 else
300 {
301 QSettings settings;
302 if (settings.value("diagrameditor/zoom-out-beyond-of-folio", false).toBool() ||
303 (horizontalScrollBar()->maximum() || verticalScrollBar()->maximum()) )
304 if (zoom_factor >= 0){
305 scale(zoom_factor, zoom_factor);
306 }
307 }
308 m_diagram->adjustSceneRect();
309 adjustGridToZoom();
310 adjustSceneRect();
311 }
312
313 /**
314 Agrandit ou rectrecit le schema de facon a ce que tous les elements du
315 schema soient visibles a l'ecran. S'il n'y a aucun element sur le schema,
316 le zoom est reinitialise
317 */
zoomFit()318 void DiagramView::zoomFit() {
319 adjustSceneRect();
320 fitInView(m_diagram->sceneRect(), Qt::KeepAspectRatio);
321 adjustGridToZoom();
322 }
323
324 /**
325 Adjust zoom to fit all elements in the view, regardless of diagram borders.
326 */
zoomContent()327 void DiagramView::zoomContent() {
328 fitInView(m_diagram -> itemsBoundingRect(), Qt::KeepAspectRatio);
329 adjustGridToZoom();
330 }
331
332 /**
333 Reinitialise le zoom
334 */
zoomReset()335 void DiagramView::zoomReset() {
336 resetMatrix();
337 adjustGridToZoom();
338 }
339
340 /**
341 Copie les elements selectionnes du schema dans le presse-papier puis les supprime
342 */
cut()343 void DiagramView::cut() {
344 copy();
345 DiagramContent cut_content(m_diagram);
346 m_diagram -> clearSelection();
347 m_diagram -> undoStack().push(new CutDiagramCommand(m_diagram, cut_content));
348 }
349
350 /**
351 Copie les elements selectionnes du schema dans le presse-papier
352 */
copy()353 void DiagramView::copy() {
354 QClipboard *presse_papier = QApplication::clipboard();
355 QString contenu_presse_papier = m_diagram -> toXml(false).toString(4);
356 if (presse_papier -> supportsSelection()) presse_papier -> setText(contenu_presse_papier, QClipboard::Selection);
357 presse_papier -> setText(contenu_presse_papier);
358 }
359
360 /**
361 * @brief DiagramView::paste
362 * Import the element stored in the clipboard to the diagram.
363 * @param pos : top left corner of the bounding rect of imported elements
364 * @param clipboard_mode
365 */
paste(const QPointF & pos,QClipboard::Mode clipboard_mode)366 void DiagramView::paste(const QPointF &pos, QClipboard::Mode clipboard_mode) {
367 if (!isInteractive() || m_diagram -> isReadOnly()) return;
368
369 QString texte_presse_papier = QApplication::clipboard() -> text(clipboard_mode);
370 if ((texte_presse_papier).isEmpty()) return;
371
372 QDomDocument document_xml;
373 if (!document_xml.setContent(texte_presse_papier)) return;
374
375 DiagramContent content_pasted;
376 m_diagram->fromXml(document_xml, pos, false, &content_pasted);
377
378 //If something was really added to diagram, we create an undo object.
379 if (content_pasted.count())
380 {
381 m_diagram -> clearSelection();
382 m_diagram -> undoStack().push(new PasteDiagramCommand(m_diagram, content_pasted));
383 adjustSceneRect();
384 }
385 }
386
387 /**
388 Colle le contenu du presse-papier sur le schema a la position de la souris
389 */
pasteHere()390 void DiagramView::pasteHere() {
391 paste(mapToScene(m_paste_here_pos));
392 }
393
394 /**
395 Manage the events press click :
396 * click to add an independent text field
397 */
mousePressEvent(QMouseEvent * e)398 void DiagramView::mousePressEvent(QMouseEvent *e)
399 {
400 e->ignore();
401
402 if (m_fresh_focus_in)
403 {
404 switchToVisualisationModeIfNeeded(e);
405 m_fresh_focus_in = false;
406 }
407
408 if (m_event_interface && m_event_interface->mousePressEvent(e)) return;
409
410 //Start drag view when hold the middle button
411 if (e->button() == Qt::MidButton)
412 {
413 m_drag_last_pos = e->pos();
414 viewport()->setCursor(Qt::ClosedHandCursor);
415 e->accept();
416 return;
417 }
418
419 //There is a good luck that user want to do a free selection
420 //In this case we temporally disable the dragmode because if the QGraphicsScene don't accept the event,
421 //and the drag mode is set to rubberbanddrag, the QGraphicsView start rubber band drag, and accept the event.
422 if (e->button() == Qt::LeftButton &&
423 e->modifiers() == Qt::CTRL)
424 {
425 QGraphicsView::DragMode dm = dragMode();
426 setDragMode(QGraphicsView::NoDrag);
427 QGraphicsView::mousePressEvent(e);
428 setDragMode(dm);
429 } else {
430 QGraphicsView::mousePressEvent(e);
431 }
432
433 if (e->isAccepted()) {
434 return;
435 }
436
437 if (e->button() == Qt::LeftButton &&
438 e->modifiers() == Qt::CTRL)
439 {
440 m_free_rubberbanding = true;
441 m_free_rubberband = QPolygon();
442 e->accept();
443 return;
444 }
445
446 if (!e->isAccepted()) {
447 QGraphicsView::mousePressEvent(e);
448 }
449 }
450
451 /**
452 * @brief DiagramView::mouseMoveEvent
453 * Manage the event move mouse
454 */
mouseMoveEvent(QMouseEvent * e)455 void DiagramView::mouseMoveEvent(QMouseEvent *e)
456 {
457 if (m_event_interface && m_event_interface->mouseMoveEvent(e)) return;
458
459 //Drag the view
460 if (e->buttons() == Qt::MidButton)
461 {
462 QScrollBar *h = horizontalScrollBar();
463 QScrollBar *v = verticalScrollBar();
464 QPointF pos = m_drag_last_pos - e -> pos();
465 m_drag_last_pos = e -> pos();
466 h -> setValue(h -> value() + pos.x());
467 v -> setValue(v -> value() + pos.y());
468 adjustSceneRect();
469 }
470 else if (m_free_rubberbanding)
471 {
472 //Update old free rubberband
473 if (viewportUpdateMode() != QGraphicsView::NoViewportUpdate && !m_free_rubberband.isEmpty())
474 {
475 if (viewportUpdateMode() != QGraphicsView::FullViewportUpdate) {
476 viewport()->update(m_free_rubberband.boundingRect().toRect().adjusted(-10,-10,10,10));
477 }
478 else {
479 update();
480 }
481 }
482
483 //Stop polygon rubberbanding if user has let go of all buttons (even
484 //if we didn't get the release events)
485 if (!e->buttons()) {
486 m_free_rubberbanding = false;
487 m_free_rubberband = QPolygon();
488 return;
489 }
490 m_free_rubberband.append(mapToScene(e->pos()));
491 emit freeRubberBandChanged(m_free_rubberband);
492
493 if (viewportUpdateMode() != QGraphicsView::NoViewportUpdate)
494 {
495 if (viewportUpdateMode() != QGraphicsView::FullViewportUpdate) {
496 viewport()->update(mapFromScene(m_free_rubberband.boundingRect().adjusted(-10,-10,10,10)));
497 }
498 else {
499 update();
500 }
501 }
502
503 //Set the new selection area
504 QPainterPath selection_area;
505 selection_area.addPolygon(m_free_rubberband);
506 m_diagram->setSelectionArea(selection_area);
507 }
508
509 else QGraphicsView::mouseMoveEvent(e);
510 }
511
512 /**
513 * @brief DiagramView::mouseReleaseEvent
514 * Manage event release click mouse
515 */
mouseReleaseEvent(QMouseEvent * e)516 void DiagramView::mouseReleaseEvent(QMouseEvent *e)
517 {
518 if (m_event_interface && m_event_interface->mouseReleaseEvent(e)) return;
519
520 //Stop drag view
521 if (e -> button() == Qt::MidButton)
522 {
523 viewport()->setCursor(Qt::ArrowCursor);
524 }
525 else if (m_free_rubberbanding && !e->buttons())
526 {
527 if (viewportUpdateMode() != QGraphicsView::NoViewportUpdate)
528 {
529 if (viewportUpdateMode() != QGraphicsView::FullViewportUpdate)
530 {
531 QRectF r(mapFromScene(m_free_rubberband).boundingRect());
532 r.adjust(-10, -10, 10, 10);
533 viewport()->update(r.toRect());
534 }
535 else
536 {
537 update();
538 }
539 }
540
541 if (m_free_rubberband.count() > 3)
542 {
543 //Popup a menu with an action to create conductors between
544 //all selected terminals.
545 QAction *act = new QAction(tr("Connecter les bornes sélectionnées"), this);
546 QPolygonF polygon_ = m_free_rubberband;
547 connect(act, &QAction::triggered, [this, polygon_]()
548 {
549 ConductorCreator::create(m_diagram, polygon_);
550 diagram()->clearSelection();
551 });
552 QMenu *menu = new QMenu(this);
553 menu->addAction(act);
554 menu->popup(e->globalPos());
555 }
556
557 m_free_rubberbanding = false;
558 m_free_rubberband = QPolygon();
559 emit freeRubberBandChanged(m_free_rubberband);
560 e->accept();
561 }
562 else
563 QGraphicsView::mouseReleaseEvent(e);
564 }
565
566 /**
567 * @brief DiagramView::gestures
568 * @return
569 */
gestures() const570 bool DiagramView::gestures() const
571 {
572 QSettings settings;
573 return(settings.value("diagramview/gestures", false).toBool());
574 }
575
576 /**
577 Manage wheel event of mouse
578 @param e QWheelEvent
579 */
wheelEvent(QWheelEvent * event)580 void DiagramView::wheelEvent(QWheelEvent *event)
581 {
582 if (m_event_interface && m_event_interface->wheelEvent(event)) return;
583
584 //Zoom and scrolling
585 QPoint angle = event->angleDelta();
586
587 if (gestures()) //When gesture mode is enable, we suppose the wheel event are made from a trackpad.
588 {
589 if (event->modifiers() == Qt::ControlModifier) //zoom
590 {
591 qreal value = angle.y();
592 zoom(1 + value/1000);
593 }
594 else //scroll
595 {
596 horizontalScrollBar()->setValue(horizontalScrollBar()->value() - angle.x());
597 verticalScrollBar()->setValue(verticalScrollBar()->value() - angle.y());
598 }
599 }
600 else if (event->modifiers() == Qt::NoModifier) //Else we suppose the wheel event are made from a mouse.
601 {
602 qreal value = angle.y();
603 zoom(1 + value/1000);
604 }
605 else
606 QGraphicsView::wheelEvent(event);
607 }
608
609 /**
610 * @brief DiagramView::gestureEvent
611 * Use the pinch of the trackpad for zoom
612 * @param event
613 * @return
614 */
gestureEvent(QGestureEvent * event)615 bool DiagramView::gestureEvent(QGestureEvent *event)
616 {
617 if (QGesture *gesture = event->gesture(Qt::PinchGesture))
618 {
619 QPinchGesture *pinch = static_cast<QPinchGesture *>(gesture);
620 if (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged)
621 {
622 qreal value = gesture->property("scaleFactor").toReal();
623 value > 1 ? zoom(1.02) : zoom(0.98);
624 }
625 }
626 return true;
627 }
628
629
630
631 /**
632 Handles "Focus in" events. Reimplemented here to store the fact the focus
633 was freshly acquired again using the mouse. This information is later used
634 in DiagramView::mousePressEvent().
635 */
focusInEvent(QFocusEvent * e)636 void DiagramView::focusInEvent(QFocusEvent *e) {
637 if (e -> reason() == Qt::MouseFocusReason) {
638 m_fresh_focus_in = true;
639 }
640 }
641
642 /**
643 * @brief DiagramView::keyPressEvent
644 * Handles "key press" events. Reimplemented here to switch to visualisation
645 * mode if needed.
646 * @param e
647 */
keyPressEvent(QKeyEvent * e)648 void DiagramView::keyPressEvent(QKeyEvent *e)
649 {
650 if (m_event_interface && m_event_interface->keyPressEvent(e))
651 return;
652
653 ProjectView *current_project = this->diagramEditor()->currentProjectView();
654 DiagramContent dc(m_diagram);
655 switch(e -> key())
656 {
657 case Qt::Key_PageUp:
658 current_project->changeTabUp();
659 return;
660 case Qt::Key_PageDown:
661 current_project->changeTabDown();
662 return;
663 case Qt::Key_Home:
664 if (dc.selectedTexts().isEmpty()) {
665 if (
666 qgraphicsitem_cast<IndependentTextItem *>(m_diagram->focusItem()) ||
667 qgraphicsitem_cast<ConductorTextItem *>(m_diagram->focusItem()) ||
668 qgraphicsitem_cast<DiagramTextItem *>(m_diagram->focusItem())
669 )
670 break;
671 current_project->changeFirstTab();
672 return;
673 }
674 else break;
675 case Qt::Key_End:
676 if (dc.selectedTexts().isEmpty()) {
677 if (
678 qgraphicsitem_cast<IndependentTextItem *>(m_diagram->focusItem()) ||
679 qgraphicsitem_cast<ConductorTextItem *>(m_diagram->focusItem()) ||
680 qgraphicsitem_cast<DiagramTextItem *>(m_diagram->focusItem())
681 )
682 break;
683 current_project->changeLastTab();
684 return;
685 }
686 else break;
687 case Qt::Key_ZoomOut:
688 zoom(0.85);
689 return;
690 case Qt::Key_ZoomIn:
691 zoom(1.15);
692 return;
693 case Qt::Key_Minus: {
694 if (e->modifiers() & Qt::ControlModifier)
695 zoom(0.85);
696 }
697 break;
698 case Qt::Key_Plus: {
699 if (e->modifiers() & Qt::ControlModifier)
700 zoom(1.15);
701 }
702 break;
703 case Qt::Key_Up: {
704 if(!dc.items(DiagramContent::All).isEmpty() && !dc.hasTextEditing())
705 scrollOnMovement(e);
706 }
707 break;
708 case Qt::Key_Down: {
709 if(!dc.items(DiagramContent::All).isEmpty() && !dc.hasTextEditing())
710 scrollOnMovement(e);
711 }
712 break;
713 case Qt::Key_Left: {
714 if(!dc.items(DiagramContent::All).isEmpty() && !dc.hasTextEditing())
715 scrollOnMovement(e);
716 }
717 break;
718 case Qt::Key_Right: {
719 if(!dc.items(DiagramContent::All).isEmpty() && !dc.hasTextEditing())
720 scrollOnMovement(e);
721 }
722 break;
723 }
724
725 switchToVisualisationModeIfNeeded(e);
726 QGraphicsView::keyPressEvent(e);
727 }
728
729 /**
730 Handles "key release" events. Reimplemented here to switch to selection
731 mode if needed.
732 */
keyReleaseEvent(QKeyEvent * e)733 void DiagramView::keyReleaseEvent(QKeyEvent *e) {
734 if (m_event_interface && m_event_interface->KeyReleaseEvent(e)) return;
735
736 switchToSelectionModeIfNeeded(e);
737 QGraphicsView::keyReleaseEvent(e);
738 }
739
740 /**
741 Handles element movement when editor is zoomed in and scrolls vertical
742 and horizontal bar. If element is moved to the right side of the editor
743 or below the editor SceneRect is expanded
744 */
scrollOnMovement(QKeyEvent * e)745 void DiagramView::scrollOnMovement(QKeyEvent *e)
746 {
747 const QList<QGraphicsItem *> selected_elmts = DiagramContent(m_diagram).items(DiagramContent::All);
748 QRectF viewed_scene = viewedSceneRect();
749 for (QGraphicsItem *qgi : selected_elmts)
750 {
751 if (qgraphicsitem_cast<Conductor *>(qgi))
752 continue;
753 if(qgi->parentItem() && qgi->parentItem()->isSelected())
754 continue;
755
756 qreal x = qgi->pos().x();
757 qreal y = qgi->pos().y();
758 qreal bottom = viewed_scene.bottom();
759 qreal top = viewed_scene.top();
760 qreal left = viewed_scene.left();
761 qreal right = viewed_scene.right();
762 qreal elmt_top = y + qgi->boundingRect().top();
763 qreal elmt_bottom = y + qgi->boundingRect().bottom();
764 qreal elmt_right = x + qgi->boundingRect().right();
765 qreal elmt_left = x + qgi->boundingRect().left();
766
767 bool elmt_right_of_left_margin = elmt_left>=left;
768 bool elmt_left_of_right_margin = elmt_right<=right;
769 bool elmt_below_top_margin = elmt_top>=top;
770 bool elmt_above_bottom_margin = elmt_bottom<=bottom;
771
772 if (!(elmt_right_of_left_margin && elmt_left_of_right_margin) ||
773 !(elmt_below_top_margin && elmt_above_bottom_margin ) )
774 {
775 QScrollBar *h = horizontalScrollBar();
776 QScrollBar *v = verticalScrollBar();
777 int h_increment=0;
778 int v_increment=0;
779 if (e->key()==Qt::Key_Up && elmt_above_bottom_margin) {
780 v_increment = 2*qgi->boundingRect().top();
781 if (v_increment == 0) v_increment = -2*qgi->boundingRect().height();
782 }
783 else if(e->key()==Qt::Key_Down && elmt_below_top_margin) {
784 v_increment = 2*qgi->boundingRect().bottom();
785 if (v_increment == 0) v_increment = -2*qgi->boundingRect().height();
786 }
787 else if (e->key()==Qt::Key_Left && elmt_left_of_right_margin) {
788 h_increment = 2*qgi->boundingRect().left();
789 if (h_increment == 0) h_increment = -2*qgi->boundingRect().width();
790 }
791 else if (e->key()==Qt::Key_Right && elmt_right_of_left_margin) {
792 h_increment = 2*qgi->boundingRect().right();
793 if (h_increment == 0) h_increment = -2*qgi->boundingRect().width();
794 }
795 if (((elmt_right >= m_diagram->sceneRect().right() - qgi->boundingRect().right()) ||
796 (elmt_bottom >= m_diagram->sceneRect().bottom() - qgi->boundingRect().bottom())) &&
797 (e->key()==Qt::Key_Right || e->key()==Qt::Key_Down)){
798 m_diagram->adjustSceneRect();
799 }
800 h -> setValue(h -> value() + h_increment);
801 v -> setValue(v -> value() + v_increment);
802 }
803 }
804 }
805
806
807 /**
808 @return le titre de cette vue ; cela correspond au titre du schema
809 visualise precede de la mention "Schema". Si le titre du schema est vide,
810 la mention "Schema sans titre" est utilisee
811 @see Diagram::title()
812 */
title() const813 QString DiagramView::title() const {
814 QString view_title;
815 QString diagram_title(m_diagram -> title());
816 if (diagram_title.isEmpty()) {
817 view_title = tr("Sans titre", "what to display for untitled diagrams");
818 } else {
819 view_title = diagram_title;
820 }
821 return(view_title);
822 }
823
824 /**
825 * @brief DiagramView::editDiagramProperties
826 * Edit the properties of the viewed digram
827 */
editDiagramProperties()828 void DiagramView::editDiagramProperties() {
829 DiagramPropertiesDialog::diagramPropertiesDialog(m_diagram, diagramEditor());
830 }
831
832 /**
833 * @brief DiagramView::adjustSceneRect
834 * Calcul and set the area of the scene visualized by this view
835 */
adjustSceneRect()836 void DiagramView::adjustSceneRect()
837 {
838 QRectF scene_rect = m_diagram->sceneRect();
839 scene_rect.adjust(-Diagram::margin, -Diagram::margin, Diagram::margin, Diagram::margin);
840
841 QSettings settings;
842 if (settings.value("diagrameditor/zoom-out-beyond-of-folio", false).toBool())
843 {
844 //When zoom out beyong of folio is active,
845 //we always adjust the scene rect to be 1/3 bigger than the wiewport
846 QRectF vpbr = mapToScene(viewport()->rect()).boundingRect();
847 vpbr.adjust(0, 0, vpbr.width()/3, vpbr.height()/3);
848 scene_rect = scene_rect.united(vpbr);
849 }
850 setSceneRect(scene_rect);
851 }
852
853 /**
854 Met a jour le titre du widget
855 */
updateWindowTitle()856 void DiagramView::updateWindowTitle() {
857 emit(titleChanged(this, title()));
858 }
859
860 /**
861 Enables or disables the drawing grid according to the amount of pixels display
862 */
adjustGridToZoom()863 void DiagramView::adjustGridToZoom() {
864 QRectF viewed_scene = viewedSceneRect();
865 if (diagramEditor()->drawGrid())
866 m_diagram->setDisplayGrid(viewed_scene.width() < 2000 || viewed_scene.height() < 2000);
867 else
868 m_diagram->setDisplayGrid(false);
869 }
870
871 /**
872 @return le rectangle du schema (classe Diagram) visualise par ce DiagramView
873 */
viewedSceneRect() const874 QRectF DiagramView::viewedSceneRect() const {
875 // recupere la taille du widget viewport
876 QSize viewport_size = viewport() -> size();
877
878 // recupere la transformation viewport -> scene
879 QTransform view_to_scene = viewportTransform().inverted();
880
881 // mappe le coin superieur gauche et le coin inferieur droit de la viewport sur la scene
882 QPointF scene_left_top = view_to_scene.map(QPointF(0.0, 0.0));
883 QPointF scene_right_bottom = view_to_scene.map(QPointF(viewport_size.width(), viewport_size.height()));
884
885 // en deduit le rectangle visualise par la scene
886 return(QRectF(scene_left_top, scene_right_bottom));
887 }
888
889 /**
890 @param tbt_loc A title block template location
891 @return true if the title block template needs to be integrated in the
892 parent project before being applied to the current diagram, or false if it
893 can be directly applied
894 */
mustIntegrateTitleBlockTemplate(const TitleBlockTemplateLocation & tbt_loc) const895 bool DiagramView::mustIntegrateTitleBlockTemplate(const TitleBlockTemplateLocation &tbt_loc) const {
896 // 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
897 QETProject *tbt_parent_project = tbt_loc.parentProject();
898 if (!tbt_parent_project) return(true);
899
900 return(tbt_parent_project != m_diagram -> project());
901 }
902
903 /**
904 Fait en sorte que le schema ne soit editable que s'il n'est pas en lecture
905 seule
906 */
applyReadOnly()907 void DiagramView::applyReadOnly() {
908 if (!m_diagram) return;
909
910 bool is_writable = !m_diagram -> isReadOnly();
911 setInteractive(is_writable);
912 setAcceptDrops(is_writable);
913 }
914
915 /**
916 * @brief DiagramView::editSelectedConductorColor
917 * Edit the color of the selected conductor; does nothing if multiple conductors are selected
918 */
editSelectedConductorColor()919 void DiagramView::editSelectedConductorColor()
920 {
921 //retrieve selected content
922 DiagramContent selection(m_diagram);
923
924 // we'll focus on the selected conductor (we do not handle multiple conductors edition)
925 QList<Conductor *> selected_conductors = selection.conductors(DiagramContent::AnyConductor | DiagramContent::SelectedOnly);
926 if (selected_conductors.count() == 1) {
927 editConductorColor(selected_conductors.at(0));
928 }
929 }
930
931 /**
932 Edit the color of the given conductor
933 @param edited_conductor Conductor we want to change the color
934 */
editConductorColor(Conductor * edited_conductor)935 void DiagramView::editConductorColor(Conductor *edited_conductor)
936 {
937 if (m_diagram -> isReadOnly() || !edited_conductor) return;
938
939 // store the initial properties of the provided conductor
940 ConductorProperties initial_properties = edited_conductor -> properties();
941
942 // prepare a color dialog showing the initial conductor color
943 QColorDialog *color_dialog = new QColorDialog(this);
944 color_dialog -> setWindowTitle(tr("Choisir la nouvelle couleur de ce conducteur"));
945 #ifdef Q_OS_MAC
946 color_dialog -> setWindowFlags(Qt::Sheet);
947 #endif
948 color_dialog -> setCurrentColor(initial_properties.color);
949
950 // asks the user what color he wishes to apply
951 if (color_dialog -> exec() == QDialog::Accepted)
952 {
953 QColor new_color = color_dialog -> selectedColor();
954 if (new_color != initial_properties.color)
955 {
956 // the user chose a different color
957 QVariant old_value, new_value;
958 old_value.setValue(initial_properties);
959 initial_properties.color = new_color;
960 new_value.setValue(initial_properties);
961
962 QPropertyUndoCommand *undo = new QPropertyUndoCommand(edited_conductor, "properties", old_value, new_value);
963 undo->setText(tr("Modifier les propriétés d'un conducteur", "undo caption"));
964 diagram() -> undoStack().push(undo);
965 }
966 }
967 }
968
969 /**
970 Reinitialise le profil des conducteurs selectionnes
971 */
resetConductors()972 void DiagramView::resetConductors() {
973 if (m_diagram -> isReadOnly()) return;
974 // recupere les conducteurs selectionnes
975 QSet<Conductor *> selected_conductors = m_diagram -> selectedConductors();
976
977 // repere les conducteurs modifies (= profil non nul)
978 QHash<Conductor *, ConductorProfilesGroup> conductors_and_profiles;
979 foreach(Conductor *conductor, selected_conductors) {
980 ConductorProfilesGroup profile = conductor -> profiles();
981 if (
982 !profile[Qt::TopLeftCorner].isNull() ||\
983 !profile[Qt::TopRightCorner].isNull() ||\
984 !profile[Qt::BottomLeftCorner].isNull() ||\
985 !profile[Qt::BottomRightCorner].isNull()
986 ) {
987 conductors_and_profiles.insert(conductor, profile);
988 }
989 }
990
991 if (conductors_and_profiles.isEmpty()) return;
992 m_diagram -> undoStack().push(new ResetConductorCommand(conductors_and_profiles));
993 }
994
995 /**
996 Gere les evenements de la DiagramView
997 @param e Evenement
998 */
999 /**
1000 * @brief DiagramView::event
1001 * Manage the event on this diagram view.
1002 * -At first activation (QEvent::WindowActivate or QEvent::Show) we zoomFit.
1003 * -Convert event interpreted to mouse event to gesture event if needed.
1004 * -send Shortcut to view (by default send to QMenu /QAction)
1005 * @param e the event.
1006 * @return
1007 */
event(QEvent * e)1008 bool DiagramView::event(QEvent *e) {
1009 if (Q_UNLIKELY(m_first_activation)) {
1010 if (e -> type() == QEvent::Show) {
1011 zoomFit();
1012 m_first_activation = false;
1013 }
1014 }
1015 // By default touch events are converted to mouse events. So
1016 // after this event we will get a mouse event also but we want
1017 // to handle touch events as gestures only. So we need this safeguard
1018 // to block mouse events that are actually generated from touch.
1019 if (e->type() == QEvent::Gesture)
1020 return gestureEvent(static_cast<QGestureEvent *>(e));
1021
1022 // fait en sorte que les raccourcis clavier arrivent prioritairement sur la
1023 // vue plutot que de remonter vers les QMenu / QAction
1024 if (
1025 e -> type() == QEvent::ShortcutOverride &&
1026 selectedItemHasFocus()
1027 ) {
1028 e -> accept();
1029 return(true);
1030 }
1031 return(QGraphicsView::event(e));
1032 }
1033
1034 /**
1035 * @brief DiagramView::paintEvent
1036 * Reimplemented from QGraphicsView
1037 * @param event
1038 */
paintEvent(QPaintEvent * event)1039 void DiagramView::paintEvent(QPaintEvent *event)
1040 {
1041 QGraphicsView::paintEvent(event);
1042
1043 if (m_free_rubberbanding && m_free_rubberband.count() >= 3)
1044 {
1045 QPainter painter(viewport());
1046 painter.setRenderHint(QPainter::Antialiasing);
1047 QPen pen(Qt::darkGreen);
1048 pen.setWidth(1);
1049 painter.setPen(pen);
1050 QColor color(Qt::darkGreen);
1051 color.setAlpha(50);
1052 QBrush brush(color);
1053 painter.setBrush(brush);
1054 painter.drawPolygon(mapFromScene(m_free_rubberband));
1055 }
1056 }
1057
1058 /**
1059 Switch to visualisation mode if the user is pressing Ctrl and Shift.
1060 @return true if the view was switched to visualisation mode, false
1061 otherwise.
1062 */
switchToVisualisationModeIfNeeded(QInputEvent * e)1063 bool DiagramView::switchToVisualisationModeIfNeeded(QInputEvent *e) {
1064 if (isCtrlShifting(e) && !selectedItemHasFocus()) {
1065 if (dragMode() != QGraphicsView::ScrollHandDrag) {
1066 setVisualisationMode();
1067 return(true);
1068 }
1069 }
1070 return(false);
1071 }
1072
1073 /**
1074 Switch back to selection mode if the user is not pressing Ctrl and Shift.
1075 @return true if the view was switched to selection mode, false
1076 otherwise.
1077 */
switchToSelectionModeIfNeeded(QInputEvent * e)1078 bool DiagramView::switchToSelectionModeIfNeeded(QInputEvent *e) {
1079 if (!selectedItemHasFocus() && !isCtrlShifting(e)) {
1080 setSelectionMode();
1081 return(true);
1082 }
1083 return(false);
1084 }
1085
1086 /**
1087 @return true if the user is pressing Ctrl and Shift simultaneously.
1088 */
isCtrlShifting(QInputEvent * e)1089 bool DiagramView::isCtrlShifting(QInputEvent *e) {
1090 bool result = false;
1091 // note: QInputEvent::modifiers and QKeyEvent::modifiers() do not return the
1092 // same values, hence the casts
1093 if (e -> type() == QEvent::KeyPress || e -> type() == QEvent::KeyRelease) {
1094 if (QKeyEvent *ke = static_cast<QKeyEvent *>(e)) {
1095 result = (ke -> modifiers() == (Qt::ControlModifier | Qt::ShiftModifier));
1096 }
1097 } else if (e -> type() >= QEvent::MouseButtonPress && e -> type() <= QEvent::MouseMove) {
1098 if (QMouseEvent *me = static_cast<QMouseEvent *>(e)) {
1099 result = (me -> modifiers() == (Qt::ControlModifier | Qt::ShiftModifier));
1100 }
1101 }
1102 return(result);
1103 }
1104
1105 /**
1106 @return true if there is a selected item and that item has the focus.
1107 */
selectedItemHasFocus()1108 bool DiagramView::selectedItemHasFocus() {
1109 return(
1110 m_diagram -> hasFocus() &&
1111 m_diagram -> focusItem() &&
1112 m_diagram -> focusItem() -> isSelected()
1113 );
1114 }
1115
1116 /**
1117 * @brief DiagramView::editSelection
1118 * Edit the selected item if he can be edited and if only one item is selected
1119 */
editSelection()1120 void DiagramView::editSelection() {
1121 if (m_diagram -> isReadOnly() || m_diagram -> selectedItems().size() != 1 ) return;
1122
1123 QGraphicsItem *item = m_diagram->selectedItems().first();
1124
1125 //We use dynamic_cast instead of qgraphicsitem_cast for QetGraphicsItem
1126 //because they haven't got they own type().
1127 //Use qgraphicsitem_cast will have weird behavior for this class.
1128 if (IndependentTextItem *iti = qgraphicsitem_cast<IndependentTextItem *>(item))
1129 iti -> edit();
1130 else if (QetGraphicsItem *qgi = dynamic_cast<QetGraphicsItem *> (item))
1131 qgi -> editProperty();
1132 else if (Conductor *c = qgraphicsitem_cast<Conductor *>(item))
1133 c -> editProperty();
1134 }
1135
1136 /**
1137 * @brief DiagramView::setEventInterface
1138 * Set an event interface to diagram view.
1139 * If diagram view already have an event interface, he delete it before.
1140 * Diagram view take ownership of event interface and delete it when event interface is finish
1141 */
setEventInterface(DVEventInterface * event_interface)1142 void DiagramView::setEventInterface(DVEventInterface *event_interface)
1143 {
1144 if (m_event_interface) delete m_event_interface;
1145 m_event_interface = event_interface;
1146 connect(m_event_interface, &DVEventInterface::finish, this, [=](){delete this->m_event_interface; this->m_event_interface = nullptr;}, Qt::QueuedConnection);
1147 }
1148
1149 /**
1150 * @brief DiagramView::contextMenuActions
1151 * @return a list of actions currently available for a context menu.
1152 *
1153 */
contextMenuActions() const1154 QList<QAction *> DiagramView::contextMenuActions() const
1155 {
1156 QList<QAction *> list;
1157 if (QETDiagramEditor *qde = diagramEditor())
1158 {
1159 if (m_diagram->selectedItems().isEmpty())
1160 {
1161 list << m_paste_here;
1162 list << m_separators.at(0);
1163 list << qde->m_edit_diagram_properties;
1164 list << qde->m_row_column_actions_group.actions();
1165 }
1166 else
1167 {
1168 list << qde->m_cut;
1169 list << qde->m_copy;
1170 list << m_multi_paste;
1171 list << m_separators.at(0);
1172 list << qde->m_conductor_reset;
1173 list << m_separators.at(1);
1174 list << qde->m_selection_actions_group.actions();
1175 list << m_separators.at(2);
1176 list << qde->m_depth_action_group->actions();
1177 }
1178
1179 //Remove from the context menu the actions which are disabled.
1180 const QList<QAction *> actions = list;
1181 for(QAction *action : actions)
1182 {
1183 if (!action->isEnabled()) {
1184 list.removeAll(action);
1185 }
1186 }
1187 }
1188
1189 return list;
1190 }
1191
1192 /**
1193 * @brief DiagramView::contextMenuEvent
1194 * @param e
1195 */
contextMenuEvent(QContextMenuEvent * e)1196 void DiagramView::contextMenuEvent(QContextMenuEvent *e)
1197 {
1198 QGraphicsView::contextMenuEvent(e);
1199 if(e->isAccepted())
1200 return;
1201
1202 if (QGraphicsItem *qgi = m_diagram->itemAt(mapToScene(e->pos()), transform()))
1203 {
1204 if (!qgi -> isSelected()) {
1205 m_diagram->clearSelection();
1206 }
1207
1208 qgi->setSelected(true);
1209 }
1210
1211 if (m_diagram->selectedItems().isEmpty())
1212 {
1213 m_paste_here_pos = e->pos();
1214 m_paste_here->setEnabled(Diagram::clipboardMayContainDiagram());
1215 }
1216
1217 QList <QAction *> list = contextMenuActions();
1218 if(!list.isEmpty())
1219 {
1220 QMenu *context_menu = new QMenu(this);
1221 context_menu->addActions(list);
1222 context_menu->popup(e->globalPos());
1223 e->accept();
1224 }
1225 }
1226
1227 /**
1228 @return l'editeur de schemas parent ou 0
1229 */
diagramEditor() const1230 QETDiagramEditor *DiagramView::diagramEditor() const {
1231 // remonte la hierarchie des widgets
1232 QWidget *w = const_cast<DiagramView *>(this);
1233 while (w -> parentWidget() && !w -> isWindow()) {
1234 w = w -> parentWidget();
1235 }
1236 // la fenetre est supposee etre un QETDiagramEditor
1237 return(qobject_cast<QETDiagramEditor *>(w));
1238 }
1239
1240 /**
1241 * @brief DiagramView::mouseDoubleClickEvent
1242 * @param e
1243 */
mouseDoubleClickEvent(QMouseEvent * e)1244 void DiagramView::mouseDoubleClickEvent(QMouseEvent *e)
1245 {
1246 if (m_event_interface && m_event_interface -> mouseDoubleClickEvent(e)) return;
1247
1248 BorderTitleBlock &bi = m_diagram -> border_and_titleblock;
1249
1250 //Get the click pos on the diagram
1251 QPointF click_pos = viewportTransform().inverted().map(e -> pos());
1252
1253 if (bi.titleBlockRect().contains(click_pos) || bi.columnsRect().contains(click_pos) || bi.rowsRect().contains(click_pos)) {
1254 e->accept();
1255 editDiagramProperties();
1256 return;
1257 }
1258 QGraphicsView::mouseDoubleClickEvent(e);
1259 }
1260