1 /*
2  * LibrePCB - Professional EDA for everyone!
3  * Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
4  * https://librepcb.org/
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 /*******************************************************************************
21  *  Includes
22  ******************************************************************************/
23 #include "packageeditorstate_select.h"
24 
25 #include "../dialogs/footprintpadpropertiesdialog.h"
26 #include "../footprintclipboarddata.h"
27 #include "../packageeditorwidget.h"
28 #include "cmd/cmddragselectedfootprintitems.h"
29 #include "cmd/cmdpastefootprintitems.h"
30 #include "cmd/cmdremoveselectedfootprintitems.h"
31 
32 #include <librepcb/common/dialogs/circlepropertiesdialog.h>
33 #include <librepcb/common/dialogs/dxfimportdialog.h>
34 #include <librepcb/common/dialogs/holepropertiesdialog.h>
35 #include <librepcb/common/dialogs/polygonpropertiesdialog.h>
36 #include <librepcb/common/dialogs/stroketextpropertiesdialog.h>
37 #include <librepcb/common/geometry/cmd/cmdpolygonedit.h>
38 #include <librepcb/common/graphics/circlegraphicsitem.h>
39 #include <librepcb/common/graphics/graphicsscene.h>
40 #include <librepcb/common/graphics/graphicsview.h>
41 #include <librepcb/common/graphics/holegraphicsitem.h>
42 #include <librepcb/common/graphics/polygongraphicsitem.h>
43 #include <librepcb/common/graphics/stroketextgraphicsitem.h>
44 #include <librepcb/common/import/dxfreader.h>
45 #include <librepcb/common/undostack.h>
46 #include <librepcb/library/pkg/footprintgraphicsitem.h>
47 #include <librepcb/library/pkg/footprintpadgraphicsitem.h>
48 #include <librepcb/library/pkg/package.h>
49 
50 #include <QtCore>
51 
52 /*******************************************************************************
53  *  Namespace
54  ******************************************************************************/
55 namespace librepcb {
56 namespace library {
57 namespace editor {
58 
59 /*******************************************************************************
60  *  Constructors / Destructor
61  ******************************************************************************/
62 
PackageEditorState_Select(Context & context)63 PackageEditorState_Select::PackageEditorState_Select(Context& context) noexcept
64   : PackageEditorState(context),
65     mState(SubState::IDLE),
66     mStartPos(),
67     mCurrentSelectionIndex(0),
68     mSelectedPolygon(nullptr),
69     mSelectedPolygonVertices(),
70     mCmdPolygonEdit() {
71 }
72 
~PackageEditorState_Select()73 PackageEditorState_Select::~PackageEditorState_Select() noexcept {
74   Q_ASSERT(mCmdDragSelectedItems.isNull());
75 }
76 
77 /*******************************************************************************
78  *  Event Handlers
79  ******************************************************************************/
80 
processGraphicsSceneMouseMoved(QGraphicsSceneMouseEvent & e)81 bool PackageEditorState_Select::processGraphicsSceneMouseMoved(
82     QGraphicsSceneMouseEvent& e) noexcept {
83   Point currentPos = Point::fromPx(e.scenePos());
84 
85   switch (mState) {
86     case SubState::SELECTING: {
87       setSelectionRect(mStartPos, currentPos);
88       return true;
89     }
90     case SubState::MOVING:
91     case SubState::PASTING: {
92       if (!mCmdDragSelectedItems) {
93         mCmdDragSelectedItems.reset(
94             new CmdDragSelectedFootprintItems(mContext));
95       }
96       Point delta = (currentPos - mStartPos).mappedToGrid(getGridInterval());
97       mCmdDragSelectedItems->setDeltaToStartPos(delta);
98       return true;
99     }
100     case SubState::MOVING_POLYGON_VERTEX: {
101       if (!mSelectedPolygon) {
102         return false;
103       }
104       if (!mCmdPolygonEdit) {
105         mCmdPolygonEdit.reset(new CmdPolygonEdit(*mSelectedPolygon));
106       }
107       QVector<Vertex> vertices = mSelectedPolygon->getPath().getVertices();
108       foreach (int i, mSelectedPolygonVertices) {
109         if ((i >= 0) && (i < vertices.count())) {
110           vertices[i].setPos(currentPos.mappedToGrid(getGridInterval()));
111         }
112       }
113       mCmdPolygonEdit->setPath(Path(vertices), true);
114       return true;
115     }
116     default: { return false; }
117   }
118 }
119 
processGraphicsSceneLeftMouseButtonPressed(QGraphicsSceneMouseEvent & e)120 bool PackageEditorState_Select::processGraphicsSceneLeftMouseButtonPressed(
121     QGraphicsSceneMouseEvent& e) noexcept {
122   switch (mState) {
123     case SubState::IDLE: {
124       // update start position of selection or movement
125       mStartPos = Point::fromPx(e.scenePos());
126       // get items under cursor
127       QList<QGraphicsItem*> items = findItemsAtPosition(mStartPos);
128       if (findPolygonVerticesAtPosition(mStartPos) && (!mContext.readOnly)) {
129         mState = SubState::MOVING_POLYGON_VERTEX;
130       } else if (items.isEmpty()) {
131         // start selecting
132         clearSelectionRect(true);
133         mState = SubState::SELECTING;
134       } else {
135         // check if the top most item under the cursor is already selected
136         QGraphicsItem* topMostItem = items.first();
137         bool itemAlreadySelected = topMostItem->isSelected();
138 
139         if (e.modifiers().testFlag(Qt::ControlModifier)) {
140           // Toggle selection when CTRL is pressed
141           if (dynamic_cast<FootprintPadGraphicsItem*>(topMostItem)) {
142             // workaround for selection of a SymbolPinGraphicsItem
143             dynamic_cast<FootprintPadGraphicsItem*>(topMostItem)
144                 ->setSelected(!itemAlreadySelected);
145           } else {
146             topMostItem->setSelected(!itemAlreadySelected);
147           }
148         } else if (e.modifiers().testFlag(Qt::ShiftModifier)) {
149           // Cycle Selection, when holding shift
150           mCurrentSelectionIndex += 1;
151           mCurrentSelectionIndex %= items.count();
152           clearSelectionRect(true);
153           QGraphicsItem* item = items[mCurrentSelectionIndex];
154           if (dynamic_cast<FootprintPadGraphicsItem*>(item)) {
155             // workaround for selection of a SymbolPinGraphicsItem
156             dynamic_cast<FootprintPadGraphicsItem*>(item)->setSelected(true);
157           } else {
158             item->setSelected(true);
159           }
160         } else if (!itemAlreadySelected) {
161           // Only select the topmost item when clicking an unselected item
162           // without CTRL
163           clearSelectionRect(true);
164           if (dynamic_cast<FootprintPadGraphicsItem*>(topMostItem)) {
165             // workaround for selection of a SymbolPinGraphicsItem
166             dynamic_cast<FootprintPadGraphicsItem*>(topMostItem)
167                 ->setSelected(true);
168           } else {
169             topMostItem->setSelected(true);
170           }
171         }
172 
173         // Start moving, if not read only.
174         if (!mContext.readOnly) {
175           Q_ASSERT(!mCmdDragSelectedItems);
176           mState = SubState::MOVING;
177         }
178       }
179       return true;
180     }
181     case SubState::PASTING: {
182       try {
183         Q_ASSERT(mCmdDragSelectedItems);
184         mContext.undoStack.appendToCmdGroup(mCmdDragSelectedItems.take());
185         mContext.undoStack.commitCmdGroup();
186         mState = SubState::IDLE;
187         clearSelectionRect(true);
188       } catch (const Exception& e) {
189         QMessageBox::critical(&mContext.editorWidget, tr("Error"), e.getMsg());
190       }
191       return true;
192     }
193     default: { return false; }
194   }
195 }
196 
processGraphicsSceneLeftMouseButtonReleased(QGraphicsSceneMouseEvent & e)197 bool PackageEditorState_Select::processGraphicsSceneLeftMouseButtonReleased(
198     QGraphicsSceneMouseEvent& e) noexcept {
199   Q_UNUSED(e);
200   switch (mState) {
201     case SubState::SELECTING: {
202       clearSelectionRect(false);
203       mState = SubState::IDLE;
204       return true;
205     }
206     case SubState::MOVING: {
207       if (mCmdDragSelectedItems) {
208         try {
209           mContext.undoStack.execCmd(mCmdDragSelectedItems.take());
210         } catch (const Exception& e) {
211           QMessageBox::critical(&mContext.editorWidget, tr("Error"),
212                                 e.getMsg());
213         }
214       }
215       mState = SubState::IDLE;
216       return true;
217     }
218     case SubState::MOVING_POLYGON_VERTEX: {
219       if (mCmdPolygonEdit) {
220         try {
221           mContext.undoStack.execCmd(mCmdPolygonEdit.take());
222         } catch (const Exception& e) {
223           QMessageBox::critical(&mContext.editorWidget, tr("Error"),
224                                 e.getMsg());
225         }
226       }
227       mState = SubState::IDLE;
228       return true;
229     }
230     default: { return false; }
231   }
232 }
233 
234 bool PackageEditorState_Select::
processGraphicsSceneLeftMouseButtonDoubleClicked(QGraphicsSceneMouseEvent & e)235     processGraphicsSceneLeftMouseButtonDoubleClicked(
236         QGraphicsSceneMouseEvent& e) noexcept {
237   if (mState == SubState::IDLE) {
238     return openPropertiesDialogOfItemAtPos(Point::fromPx(e.scenePos()));
239   } else {
240     return false;
241   }
242 }
243 
processGraphicsSceneRightMouseButtonReleased(QGraphicsSceneMouseEvent & e)244 bool PackageEditorState_Select::processGraphicsSceneRightMouseButtonReleased(
245     QGraphicsSceneMouseEvent& e) noexcept {
246   switch (mState) {
247     case SubState::IDLE: {
248       return openContextMenuAtPos(Point::fromPx(e.scenePos()));
249     }
250     case SubState::MOVING:
251     case SubState::PASTING: {
252       return rotateSelectedItems(Angle::deg90());
253     }
254     default: { return false; }
255   }
256 }
257 
processSelectAll()258 bool PackageEditorState_Select::processSelectAll() noexcept {
259   switch (mState) {
260     case SubState::IDLE: {
261       if (auto item = mContext.currentGraphicsItem) {
262         // Set a selection rect slightly larger than the total items bounding
263         // rect to get all items selected.
264         item->setSelectionRect(
265             item->boundingRect().adjusted(-100, -100, 100, 100));
266         return true;
267       }
268       return false;
269     }
270     default: { return false; }
271   }
272 }
273 
processCut()274 bool PackageEditorState_Select::processCut() noexcept {
275   switch (mState) {
276     case SubState::IDLE: {
277       if (copySelectedItemsToClipboard()) {
278         return removeSelectedItems();
279       } else {
280         return false;
281       }
282     }
283     default: { return false; }
284   }
285 }
286 
processCopy()287 bool PackageEditorState_Select::processCopy() noexcept {
288   switch (mState) {
289     case SubState::IDLE: {
290       return copySelectedItemsToClipboard();
291     }
292     default: { return false; }
293   }
294 }
295 
processPaste()296 bool PackageEditorState_Select::processPaste() noexcept {
297   switch (mState) {
298     case SubState::IDLE: {
299       try {
300         // Get footprint items from clipboard, if none provided.
301         std::unique_ptr<FootprintClipboardData> data =
302             FootprintClipboardData::fromMimeData(
303                 qApp->clipboard()->mimeData());  // can throw
304         if (data) {
305           return startPaste(std::move(data), tl::nullopt);
306         }
307       } catch (const Exception& e) {
308         QMessageBox::critical(&mContext.editorWidget, tr("Error"), e.getMsg());
309         processAbortCommand();
310         return false;
311       }
312     }
313     default: { break; }
314   }
315 
316   return false;
317 }
318 
processRotateCw()319 bool PackageEditorState_Select::processRotateCw() noexcept {
320   switch (mState) {
321     case SubState::IDLE:
322     case SubState::MOVING:
323     case SubState::PASTING: {
324       return rotateSelectedItems(-Angle::deg90());
325     }
326     default: { return false; }
327   }
328 }
329 
processRotateCcw()330 bool PackageEditorState_Select::processRotateCcw() noexcept {
331   switch (mState) {
332     case SubState::IDLE:
333     case SubState::MOVING:
334     case SubState::PASTING: {
335       return rotateSelectedItems(Angle::deg90());
336     }
337     default: { return false; }
338   }
339 }
340 
processMirror()341 bool PackageEditorState_Select::processMirror() noexcept {
342   switch (mState) {
343     case SubState::IDLE:
344     case SubState::MOVING:
345     case SubState::PASTING: {
346       return mirrorSelectedItems(Qt::Horizontal, false);
347     }
348     default: { return false; }
349   }
350 }
351 
processFlip()352 bool PackageEditorState_Select::processFlip() noexcept {
353   switch (mState) {
354     case SubState::IDLE:
355     case SubState::MOVING:
356     case SubState::PASTING: {
357       return mirrorSelectedItems(Qt::Horizontal, true);
358     }
359     default: { return false; }
360   }
361 }
362 
processRemove()363 bool PackageEditorState_Select::processRemove() noexcept {
364   switch (mState) {
365     case SubState::IDLE: {
366       return removeSelectedItems();
367     }
368     default: { return false; }
369   }
370 }
371 
processImportDxf()372 bool PackageEditorState_Select::processImportDxf() noexcept {
373   try {
374     if (!mContext.currentFootprint) {
375       return false;
376     }
377 
378     // Ask for file path and import options.
379     DxfImportDialog dialog(getAllowedCircleAndPolygonLayers(),
380                            GraphicsLayerName(GraphicsLayer::sTopDocumentation),
381                            true, getDefaultLengthUnit(),
382                            "package_editor/dxf_import_dialog",
383                            &mContext.editorWidget);
384     FilePath fp = dialog.chooseFile();  // Opens the file chooser dialog.
385     if ((!fp.isValid()) || (dialog.exec() != QDialog::Accepted)) {
386       return false;  // Aborted.
387     }
388 
389     // Read DXF file.
390     DxfReader import;
391     import.setScaleFactor(dialog.getScaleFactor());
392     import.parse(fp);  // can throw
393 
394     // Build elements to import. ALthough this has nothing to do with the
395     // clipboard, we use FootprintClipboardData since it works very well :-)
396     std::unique_ptr<FootprintClipboardData> data(
397         new FootprintClipboardData(mContext.currentFootprint->getUuid(),
398                                    mContext.package.getPads(), Point(0, 0)));
399     for (const auto& path : import.getPolygons()) {
400       data->getPolygons().append(
401           std::make_shared<Polygon>(Uuid::createRandom(), dialog.getLayerName(),
402                                     dialog.getLineWidth(), false, false, path));
403     }
404     for (const auto& circle : import.getCircles()) {
405       if (dialog.getImportCirclesAsDrills()) {
406         data->getHoles().append(std::make_shared<Hole>(
407             Uuid::createRandom(), circle.position, circle.diameter));
408       } else {
409         data->getPolygons().append(std::make_shared<Polygon>(
410             Uuid::createRandom(), dialog.getLayerName(), dialog.getLineWidth(),
411             false, false,
412             Path::circle(circle.diameter).translated(circle.position)));
413       }
414     }
415 
416     // Abort with error if nothing was imported.
417     if (data->getItemCount() == 0) {
418       DxfImportDialog::throwNoObjectsImportedError();  // will throw
419     }
420 
421     // Sanity check that the chosen layer is really visible, but this should
422     // always be the case anyway.
423     const GraphicsLayer* polygonLayer =
424         mContext.layerProvider.getLayer(*dialog.getLayerName());
425     const GraphicsLayer* holeLayer =
426         mContext.layerProvider.getLayer(GraphicsLayer::sBoardDrillsNpth);
427     if ((!polygonLayer) || (!polygonLayer->isVisible()) || (!holeLayer) ||
428         (!holeLayer->isVisible())) {
429       throw LogicError(__FILE__, __LINE__, "Layer is not visible!");  // no tr()
430     }
431 
432     // Start the paste tool.
433     return startPaste(std::move(data),
434                       dialog.getPlacementPosition());  // can throw
435   } catch (const Exception& e) {
436     QMessageBox::critical(&mContext.editorWidget, tr("Error"), e.getMsg());
437     processAbortCommand();
438     return false;
439   }
440 }
441 
processAbortCommand()442 bool PackageEditorState_Select::processAbortCommand() noexcept {
443   switch (mState) {
444     case SubState::MOVING: {
445       mCmdDragSelectedItems.reset();
446       mState = SubState::IDLE;
447       return true;
448     }
449     case SubState::MOVING_POLYGON_VERTEX: {
450       mCmdPolygonEdit.reset();
451       mState = SubState::IDLE;
452       return true;
453     }
454     case SubState::PASTING: {
455       try {
456         mCmdDragSelectedItems.reset();
457         mContext.undoStack.abortCmdGroup();
458         mState = SubState::IDLE;
459         return true;
460       } catch (const Exception& e) {
461         QMessageBox::critical(&mContext.editorWidget, tr("Error"), e.getMsg());
462         return false;
463       }
464     }
465     default: { return false; }
466   }
467 }
468 
469 /*******************************************************************************
470  *  Private Methods
471  ******************************************************************************/
472 
openContextMenuAtPos(const Point & pos)473 bool PackageEditorState_Select::openContextMenuAtPos(
474     const Point& pos) noexcept {
475   if (mState != SubState::IDLE) return false;
476 
477   QMenu menu;
478   if (findPolygonVerticesAtPosition(pos)) {
479     // special menu for polygon vertices
480     QAction* aRemove =
481         menu.addAction(QIcon(":/img/actions/delete.png"), tr("Remove Vertex"));
482     connect(aRemove, &QAction::triggered,
483             [this]() { removeSelectedPolygonVertices(); });
484     int remainingVertices = mSelectedPolygon->getPath().getVertices().count() -
485         mSelectedPolygonVertices.count();
486     aRemove->setEnabled((remainingVertices >= 2) && (!mContext.readOnly));
487   } else {
488     // handle item selection
489     QGraphicsItem* selectedItem = nullptr;
490     QList<QGraphicsItem*> items = findItemsAtPosition(pos);
491     if (items.isEmpty()) return false;
492     foreach (QGraphicsItem* item, items) {
493       if (item->isSelected()) {
494         selectedItem = item;
495       }
496     }
497     if (!selectedItem) {
498       clearSelectionRect(true);
499       selectedItem = items.first();
500       if (dynamic_cast<FootprintPadGraphicsItem*>(selectedItem)) {
501         // workaround for selection of a SymbolPinGraphicsItem
502         dynamic_cast<FootprintPadGraphicsItem*>(selectedItem)
503             ->setSelected(true);
504       } else {
505         selectedItem->setSelected(true);
506       }
507     }
508     Q_ASSERT(selectedItem);
509     Q_ASSERT(selectedItem->isSelected());
510 
511     // if a polygon line is under the cursor, add the "Add Vertex" menu item
512     if (PolygonGraphicsItem* i =
513             dynamic_cast<PolygonGraphicsItem*>(selectedItem)) {
514       Polygon* polygon = &i->getPolygon();
515       int index = i->getLineIndexAtPosition(pos);
516       if (index >= 0) {
517         QAction* aAddVertex =
518             menu.addAction(QIcon(":/img/actions/add.png"), tr("Add Vertex"));
519         aAddVertex->setEnabled(!mContext.readOnly);
520         connect(aAddVertex, &QAction::triggered,
521                 [=]() { startAddingPolygonVertex(*polygon, index, pos); });
522         menu.addSeparator();
523       }
524     }
525 
526     // build the context menu
527     QAction* aRotateCCW =
528         menu.addAction(QIcon(":/img/actions/rotate_left.png"), tr("Rotate"));
529     aRotateCCW->setEnabled(!mContext.readOnly);
530     connect(aRotateCCW, &QAction::triggered,
531             [this]() { rotateSelectedItems(Angle::deg90()); });
532     QAction* aMirrorH = menu.addAction(
533         QIcon(":/img/actions/flip_horizontal.png"), tr("Mirror"));
534     aMirrorH->setEnabled(!mContext.readOnly);
535     connect(aMirrorH, &QAction::triggered,
536             [this]() { mirrorSelectedItems(Qt::Horizontal, false); });
537     QAction* aFlipH =
538         menu.addAction(QIcon(":/img/actions/swap.png"), tr("Flip"));
539     aFlipH->setEnabled(!mContext.readOnly);
540     connect(aFlipH, &QAction::triggered,
541             [this]() { mirrorSelectedItems(Qt::Horizontal, true); });
542     QAction* aRemove =
543         menu.addAction(QIcon(":/img/actions/delete.png"), tr("Remove"));
544     aRemove->setEnabled(!mContext.readOnly);
545     connect(aRemove, &QAction::triggered, [this]() { removeSelectedItems(); });
546     menu.addSeparator();
547     if (CmdDragSelectedFootprintItems(mContext).hasOffTheGridElements()) {
548       QAction* aSnapToGrid =
549           menu.addAction(QIcon(":/img/actions/grid.png"), tr("Snap To Grid"));
550       aSnapToGrid->setEnabled(!mContext.readOnly);
551       connect(aSnapToGrid, &QAction::triggered, this,
552               &PackageEditorState_Select::snapSelectedItemsToGrid);
553       menu.addSeparator();
554     }
555     QAction* aProperties =
556         menu.addAction(QIcon(":/img/actions/settings.png"), tr("Properties"));
557     connect(aProperties, &QAction::triggered, [this, &selectedItem]() {
558       openPropertiesDialogOfItem(selectedItem);
559     });
560   }
561 
562   // execute the context menu
563   menu.exec(QCursor::pos());
564   return true;
565 }
566 
openPropertiesDialogOfItem(QGraphicsItem * item)567 bool PackageEditorState_Select::openPropertiesDialogOfItem(
568     QGraphicsItem* item) noexcept {
569   if (!item) return false;
570 
571   if (FootprintPadGraphicsItem* pad =
572           dynamic_cast<FootprintPadGraphicsItem*>(item)) {
573     Q_ASSERT(pad);
574     FootprintPadPropertiesDialog dialog(
575         mContext.package, *mContext.currentFootprint, pad->getPad(),
576         mContext.undoStack, getDefaultLengthUnit(),
577         "package_editor/footprint_pad_properties_dialog",
578         &mContext.editorWidget);
579     dialog.setReadOnly(mContext.readOnly);
580     dialog.exec();
581     return true;
582   } else if (StrokeTextGraphicsItem* text =
583                  dynamic_cast<StrokeTextGraphicsItem*>(item)) {
584     Q_ASSERT(text);
585     StrokeTextPropertiesDialog dialog(
586         text->getText(), mContext.undoStack, getAllowedTextLayers(),
587         getDefaultLengthUnit(), "package_editor/stroke_text_properties_dialog",
588         &mContext.editorWidget);
589     dialog.setReadOnly(mContext.readOnly);
590     dialog.exec();
591     return true;
592   } else if (PolygonGraphicsItem* polygon =
593                  dynamic_cast<PolygonGraphicsItem*>(item)) {
594     Q_ASSERT(polygon);
595     PolygonPropertiesDialog dialog(
596         polygon->getPolygon(), mContext.undoStack,
597         getAllowedCircleAndPolygonLayers(), getDefaultLengthUnit(),
598         "package_editor/polygon_properties_dialog", &mContext.editorWidget);
599     dialog.setReadOnly(mContext.readOnly);
600     dialog.exec();
601     return true;
602   } else if (CircleGraphicsItem* circle =
603                  dynamic_cast<CircleGraphicsItem*>(item)) {
604     Q_ASSERT(circle);
605     CirclePropertiesDialog dialog(
606         circle->getCircle(), mContext.undoStack,
607         getAllowedCircleAndPolygonLayers(), getDefaultLengthUnit(),
608         "package_editor/circle_properties_dialog", &mContext.editorWidget);
609     dialog.setReadOnly(mContext.readOnly);
610     dialog.exec();
611     return true;
612   } else if (HoleGraphicsItem* hole = dynamic_cast<HoleGraphicsItem*>(item)) {
613     Q_ASSERT(hole);
614     // Note: The const_cast<> is a bit ugly, but it was by far the easiest
615     // way and is safe since here we know that we're allowed to modify the hole.
616     HolePropertiesDialog dialog(const_cast<Hole&>(hole->getHole()),
617                                 mContext.undoStack, getDefaultLengthUnit(),
618                                 "package_editor/hole_properties_dialog",
619                                 &mContext.editorWidget);
620     dialog.setReadOnly(mContext.readOnly);
621     dialog.exec();
622     return true;
623   }
624   return false;
625 }
626 
openPropertiesDialogOfItemAtPos(const Point & pos)627 bool PackageEditorState_Select::openPropertiesDialogOfItemAtPos(
628     const Point& pos) noexcept {
629   QList<QGraphicsItem*> items = findItemsAtPosition(pos);
630   if (items.isEmpty()) return false;
631   return openPropertiesDialogOfItem(items.first());
632 }
633 
copySelectedItemsToClipboard()634 bool PackageEditorState_Select::copySelectedItemsToClipboard() noexcept {
635   if ((!mContext.currentFootprint) || (!mContext.currentGraphicsItem)) {
636     return false;
637   }
638 
639   try {
640     Point cursorPos = mContext.graphicsView.mapGlobalPosToScenePos(
641         QCursor::pos(), true, false);
642     FootprintClipboardData data(mContext.currentFootprint->getUuid(),
643                                 mContext.package.getPads(), cursorPos);
644     foreach (const QSharedPointer<FootprintPadGraphicsItem>& pad,
645              mContext.currentGraphicsItem->getSelectedPads()) {
646       Q_ASSERT(pad);
647       data.getFootprintPads().append(
648           std::make_shared<FootprintPad>(pad->getPad()));
649     }
650     foreach (const QSharedPointer<CircleGraphicsItem>& circle,
651              mContext.currentGraphicsItem->getSelectedCircles()) {
652       Q_ASSERT(circle);
653       data.getCircles().append(std::make_shared<Circle>(circle->getCircle()));
654     }
655     foreach (const QSharedPointer<PolygonGraphicsItem>& polygon,
656              mContext.currentGraphicsItem->getSelectedPolygons()) {
657       Q_ASSERT(polygon);
658       data.getPolygons().append(
659           std::make_shared<Polygon>(polygon->getPolygon()));
660     }
661     foreach (const QSharedPointer<StrokeTextGraphicsItem>& text,
662              mContext.currentGraphicsItem->getSelectedStrokeTexts()) {
663       Q_ASSERT(text);
664       data.getStrokeTexts().append(
665           std::make_shared<StrokeText>(text->getText()));
666     }
667     foreach (const QSharedPointer<HoleGraphicsItem>& hole,
668              mContext.currentGraphicsItem->getSelectedHoles()) {
669       Q_ASSERT(hole);
670       data.getHoles().append(std::make_shared<Hole>(hole->getHole()));
671     }
672     if (data.getItemCount() > 0) {
673       qApp->clipboard()->setMimeData(
674           data.toMimeData(mContext.layerProvider).release());
675     }
676   } catch (const Exception& e) {
677     QMessageBox::critical(&mContext.editorWidget, tr("Error"), e.getMsg());
678   }
679   return true;
680 }
681 
startPaste(std::unique_ptr<FootprintClipboardData> data,const tl::optional<Point> & fixedPosition)682 bool PackageEditorState_Select::startPaste(
683     std::unique_ptr<FootprintClipboardData> data,
684     const tl::optional<Point>& fixedPosition) {
685   Q_ASSERT(data);
686 
687   // Abort if no footprint is selected.
688   if ((!mContext.currentFootprint) || (!mContext.currentGraphicsItem)) {
689     return false;
690   }
691 
692   // Start undo command group.
693   clearSelectionRect(true);
694   mContext.undoStack.beginCmdGroup(tr("Paste Footprint Elements"));
695   mState = SubState::PASTING;
696 
697   // Paste items.
698   mStartPos =
699       mContext.graphicsView.mapGlobalPosToScenePos(QCursor::pos(), true, false);
700   Point offset = fixedPosition
701       ? (*fixedPosition)
702       : (mStartPos - data->getCursorPos()).mappedToGrid(getGridInterval());
703   QScopedPointer<CmdPasteFootprintItems> cmd(new CmdPasteFootprintItems(
704       mContext.package, *mContext.currentFootprint,
705       *mContext.currentGraphicsItem, std::move(data), offset));
706   if (mContext.undoStack.appendToCmdGroup(cmd.take())) {  // can throw
707     if (fixedPosition) {
708       // Fixed position provided (no interactive placement), finish tool.
709       mContext.undoStack.commitCmdGroup();
710       mState = SubState::IDLE;
711       clearSelectionRect(true);
712     } else {
713       // Start moving the selected items.
714       mCmdDragSelectedItems.reset(new CmdDragSelectedFootprintItems(mContext));
715     }
716     return true;
717   } else {
718     // No items pasted -> abort.
719     mContext.undoStack.abortCmdGroup();  // can throw
720     mState = SubState::IDLE;
721     return false;
722   }
723 }
724 
rotateSelectedItems(const Angle & angle)725 bool PackageEditorState_Select::rotateSelectedItems(
726     const Angle& angle) noexcept {
727   try {
728     if (mCmdDragSelectedItems) {
729       mCmdDragSelectedItems->rotate(angle);
730     } else {
731       QScopedPointer<CmdDragSelectedFootprintItems> cmd(
732           new CmdDragSelectedFootprintItems(mContext));
733       cmd->rotate(angle);
734       mContext.undoStack.execCmd(cmd.take());
735     }
736   } catch (const Exception& e) {
737     QMessageBox::critical(&mContext.editorWidget, tr("Error"), e.getMsg());
738   }
739   return true;  // TODO: return false if no items were selected
740 }
741 
mirrorSelectedItems(Qt::Orientation orientation,bool flipLayers)742 bool PackageEditorState_Select::mirrorSelectedItems(Qt::Orientation orientation,
743                                                     bool flipLayers) noexcept {
744   try {
745     if (mCmdDragSelectedItems) {
746       mCmdDragSelectedItems->mirrorGeometry(Qt::Horizontal);
747       if (flipLayers) {
748         mCmdDragSelectedItems->mirrorLayer();
749       }
750     } else {
751       QScopedPointer<CmdDragSelectedFootprintItems> cmd(
752           new CmdDragSelectedFootprintItems(mContext));
753       cmd->mirrorGeometry(orientation);
754       if (flipLayers) {
755         cmd->mirrorLayer();
756       }
757       mContext.undoStack.execCmd(cmd.take());
758     }
759   } catch (const Exception& e) {
760     QMessageBox::critical(&mContext.editorWidget, tr("Error"), e.getMsg());
761   }
762   return true;  // TODO: return false if no items were selected
763 }
764 
snapSelectedItemsToGrid()765 bool PackageEditorState_Select::snapSelectedItemsToGrid() noexcept {
766   try {
767     QScopedPointer<CmdDragSelectedFootprintItems> cmdMove(
768         new CmdDragSelectedFootprintItems(mContext));
769     cmdMove->snapToGrid();
770     mContext.undoStack.execCmd(cmdMove.take());
771   } catch (const Exception& e) {
772     QMessageBox::critical(&mContext.editorWidget, tr("Error"), e.getMsg());
773   }
774   return true;  // TODO: return false if no items were selected
775 }
776 
removeSelectedItems()777 bool PackageEditorState_Select::removeSelectedItems() noexcept {
778   try {
779     mContext.undoStack.execCmd(new CmdRemoveSelectedFootprintItems(mContext));
780   } catch (const Exception& e) {
781     QMessageBox::critical(&mContext.editorWidget, tr("Error"), e.getMsg());
782   }
783   return true;  // TODO: return false if no items were selected
784 }
785 
removeSelectedPolygonVertices()786 void PackageEditorState_Select::removeSelectedPolygonVertices() noexcept {
787   if ((!mSelectedPolygon) || mSelectedPolygonVertices.isEmpty()) {
788     return;
789   }
790 
791   try {
792     Path path;
793     for (int i = 0; i < mSelectedPolygon->getPath().getVertices().count();
794          ++i) {
795       if (!mSelectedPolygonVertices.contains(i)) {
796         path.getVertices().append(mSelectedPolygon->getPath().getVertices()[i]);
797       }
798     }
799     if (mSelectedPolygon->getPath().isClosed() &&
800         path.getVertices().count() > 2) {
801       path.close();
802     }
803     if (path.isClosed() && (path.getVertices().count() == 3)) {
804       path.getVertices().removeLast();  // Avoid overlapping lines
805     }
806     if (path.getVertices().count() < 2) {
807       return;  // Do not allow to create invalid polygons!
808     }
809     QScopedPointer<CmdPolygonEdit> cmd(new CmdPolygonEdit(*mSelectedPolygon));
810     cmd->setPath(path, false);
811     mContext.undoStack.execCmd(cmd.take());
812   } catch (const Exception& e) {
813     QMessageBox::critical(&mContext.editorWidget, tr("Error"), e.getMsg());
814   }
815 }
816 
startAddingPolygonVertex(Polygon & polygon,int vertex,const Point & pos)817 void PackageEditorState_Select::startAddingPolygonVertex(
818     Polygon& polygon, int vertex, const Point& pos) noexcept {
819   try {
820     Q_ASSERT(vertex > 0);  // it must be the vertex *after* the clicked line
821     Path path = polygon.getPath();
822     Point newPos = pos.mappedToGrid(getGridInterval());
823     Angle newAngle = path.getVertices()[vertex - 1].getAngle();
824     path.getVertices().insert(vertex, Vertex(newPos, newAngle));
825     mCmdPolygonEdit.reset(new CmdPolygonEdit(polygon));
826     mCmdPolygonEdit->setPath(path, true);
827 
828     mSelectedPolygon = &polygon;
829     mSelectedPolygonVertices = {vertex};
830     mStartPos = pos;
831     mState = SubState::MOVING_POLYGON_VERTEX;
832   } catch (const Exception& e) {
833     QMessageBox::critical(&mContext.editorWidget, tr("Error"), e.getMsg());
834   }
835 }
836 
setSelectionRect(const Point & p1,const Point & p2)837 void PackageEditorState_Select::setSelectionRect(const Point& p1,
838                                                  const Point& p2) noexcept {
839   mContext.graphicsScene.setSelectionRect(p1, p2);
840   mContext.currentGraphicsItem->setSelectionRect(
841       QRectF(p1.toPxQPointF(), p2.toPxQPointF()));
842 }
843 
clearSelectionRect(bool updateItemsSelectionState)844 void PackageEditorState_Select::clearSelectionRect(
845     bool updateItemsSelectionState) noexcept {
846   mContext.graphicsScene.setSelectionRect(Point(), Point());
847   if (updateItemsSelectionState) {
848     mContext.graphicsScene.setSelectionArea(QPainterPath());
849   }
850 }
851 
findItemsAtPosition(const Point & pos)852 QList<QGraphicsItem*> PackageEditorState_Select::findItemsAtPosition(
853     const Point& pos) noexcept {
854   QList<QSharedPointer<FootprintPadGraphicsItem>> pads;
855   QList<QSharedPointer<CircleGraphicsItem>> circles;
856   QList<QSharedPointer<PolygonGraphicsItem>> polygons;
857   QList<QSharedPointer<StrokeTextGraphicsItem>> texts;
858   QList<QSharedPointer<HoleGraphicsItem>> holes;
859   int count = mContext.currentGraphicsItem->getItemsAtPosition(
860       pos, &pads, &circles, &polygons, &texts, &holes);
861   QList<QGraphicsItem*> result = {};
862   foreach (QSharedPointer<FootprintPadGraphicsItem> pad, pads) {
863     result.append(pad.data());
864   }
865   foreach (QSharedPointer<CircleGraphicsItem> cirlce, circles) {
866     result.append(cirlce.data());
867   }
868   foreach (QSharedPointer<PolygonGraphicsItem> polygon, polygons) {
869     result.append(polygon.data());
870   }
871   foreach (QSharedPointer<StrokeTextGraphicsItem> text, texts) {
872     result.append(text.data());
873   }
874   foreach (QSharedPointer<HoleGraphicsItem> hole, holes) {
875     result.append(hole.data());
876   }
877 
878   Q_ASSERT(result.count() ==
879            (pads.count() + texts.count() + polygons.count() + circles.count() +
880             holes.count()));
881   Q_ASSERT(result.count() == count);
882   return result;
883 }
884 
findPolygonVerticesAtPosition(const Point & pos)885 bool PackageEditorState_Select::findPolygonVerticesAtPosition(
886     const Point& pos) noexcept {
887   if (mContext.currentFootprint) {
888     for (Polygon& p : mContext.currentFootprint->getPolygons()) {
889       PolygonGraphicsItem* i =
890           mContext.currentGraphicsItem->getPolygonGraphicsItem(p);
891       if (i && i->isSelected()) {
892         mSelectedPolygonVertices = i->getVertexIndicesAtPosition(pos);
893         if (!mSelectedPolygonVertices.isEmpty()) {
894           mSelectedPolygon = &p;
895           return true;
896         }
897       }
898     }
899   }
900 
901   mSelectedPolygon = nullptr;
902   mSelectedPolygonVertices.clear();
903   return false;
904 }
905 
906 /*******************************************************************************
907  *  End of File
908  ******************************************************************************/
909 
910 }  // namespace editor
911 }  // namespace library
912 }  // namespace librepcb
913