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