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 "boardeditorstate_drawpolygon.h"
24 
25 #include "../boardeditor.h"
26 
27 #include <librepcb/common/geometry/cmd/cmdpolygonedit.h>
28 #include <librepcb/common/geometry/polygon.h>
29 #include <librepcb/common/graphics/graphicsview.h>
30 #include <librepcb/common/undostack.h>
31 #include <librepcb/common/widgets/graphicslayercombobox.h>
32 #include <librepcb/common/widgets/unsignedlengthedit.h>
33 #include <librepcb/project/boards/board.h>
34 #include <librepcb/project/boards/boardlayerstack.h>
35 #include <librepcb/project/boards/cmd/cmdboardpolygonadd.h>
36 #include <librepcb/project/boards/items/bi_polygon.h>
37 
38 #include <QtCore>
39 
40 /*******************************************************************************
41  *  Namespace
42  ******************************************************************************/
43 namespace librepcb {
44 namespace project {
45 namespace editor {
46 
47 /*******************************************************************************
48  *  Constructors / Destructor
49  ******************************************************************************/
50 
BoardEditorState_DrawPolygon(const Context & context)51 BoardEditorState_DrawPolygon::BoardEditorState_DrawPolygon(
52     const Context& context) noexcept
53   : BoardEditorState(context),
54     mIsUndoCmdActive(false),
55     mLastPolygonProperties(
56         Uuid::createRandom(),  // UUID is not relevant here
57         GraphicsLayerName(GraphicsLayer::sBoardOutlines),  // Layer
58         UnsignedLength(0),  // Line width
59         false,  // Is filled
60         false,  // Is grab area
61         Path()  // Path is not relevant here
62         ),
63     mLastSegmentPos(),
64     mCurrentPolygon(nullptr),
65     mCurrentPolygonEditCmd(nullptr) {
66 }
67 
~BoardEditorState_DrawPolygon()68 BoardEditorState_DrawPolygon::~BoardEditorState_DrawPolygon() noexcept {
69 }
70 
71 /*******************************************************************************
72  *  General Methods
73  ******************************************************************************/
74 
entry()75 bool BoardEditorState_DrawPolygon::entry() noexcept {
76   Q_ASSERT(mIsUndoCmdActive == false);
77 
78   Board* board = getActiveBoard();
79   if (!board) return false;
80 
81   // Clear board selection because selection does not make sense in this state
82   board->clearSelection();
83 
84   // Add the "Layer:" label to the toolbar
85   mLayerLabel.reset(new QLabel(tr("Layer:")));
86   mLayerLabel->setIndent(10);
87   mContext.editorUi.commandToolbar->addWidget(mLayerLabel.data());
88 
89   // Add the layers combobox to the toolbar
90   mLayerComboBox.reset(new GraphicsLayerComboBox());
91   mLayerComboBox->setLayers(getAllowedGeometryLayers(*board));
92   mLayerComboBox->setCurrentLayer(mLastPolygonProperties.getLayerName());
93   mContext.editorUi.commandToolbar->addWidget(mLayerComboBox.data());
94   connect(mLayerComboBox.data(), &GraphicsLayerComboBox::currentLayerChanged,
95           this, &BoardEditorState_DrawPolygon::layerComboBoxLayerChanged);
96 
97   // Add the "Width:" label to the toolbar
98   mWidthLabel.reset(new QLabel(tr("Width:")));
99   mWidthLabel->setIndent(10);
100   mContext.editorUi.commandToolbar->addWidget(mWidthLabel.data());
101 
102   // Add the widths combobox to the toolbar
103   mWidthEdit.reset(new UnsignedLengthEdit());
104   mWidthEdit->setValue(mLastPolygonProperties.getLineWidth());
105   mContext.editorUi.commandToolbar->addWidget(mWidthEdit.data());
106   connect(mWidthEdit.data(), &UnsignedLengthEdit::valueChanged, this,
107           &BoardEditorState_DrawPolygon::widthEditValueChanged);
108 
109   // Add the "Filled:" label to the toolbar
110   mFillLabel.reset(new QLabel(tr("Filled:")));
111   mFillLabel->setIndent(10);
112   mContext.editorUi.commandToolbar->addWidget(mFillLabel.data());
113 
114   // Add the filled checkbox to the toolbar
115   mFillCheckBox.reset(new QCheckBox());
116   mFillCheckBox->setChecked(mLastPolygonProperties.isFilled());
117   mContext.editorUi.commandToolbar->addWidget(mFillCheckBox.data());
118   connect(mFillCheckBox.data(), &QCheckBox::toggled, this,
119           &BoardEditorState_DrawPolygon::filledCheckBoxCheckedChanged);
120 
121   // Change the cursor
122   mContext.editorGraphicsView.setCursor(Qt::CrossCursor);
123 
124   return true;
125 }
126 
exit()127 bool BoardEditorState_DrawPolygon::exit() noexcept {
128   // Abort the currently active command
129   if (!abortCommand(true)) return false;
130 
131   // Remove actions / widgets from the "command" toolbar
132   mFillCheckBox.reset();
133   mFillLabel.reset();
134   mWidthEdit.reset();
135   mWidthLabel.reset();
136   mLayerComboBox.reset();
137   mLayerLabel.reset();
138   qDeleteAll(mActionSeparators);
139   mActionSeparators.clear();
140 
141   // Reset the cursor
142   mContext.editorGraphicsView.setCursor(Qt::ArrowCursor);
143 
144   return true;
145 }
146 
147 /*******************************************************************************
148  *  Event Handlers
149  ******************************************************************************/
150 
processAbortCommand()151 bool BoardEditorState_DrawPolygon::processAbortCommand() noexcept {
152   if (mIsUndoCmdActive) {
153     // Just finish the current polygon, not exiting the whole tool.
154     return abortCommand(true);
155   } else {
156     // Allow leaving the tool.
157     return false;
158   }
159 }
160 
processGraphicsSceneMouseMoved(QGraphicsSceneMouseEvent & e)161 bool BoardEditorState_DrawPolygon::processGraphicsSceneMouseMoved(
162     QGraphicsSceneMouseEvent& e) noexcept {
163   Point pos = Point::fromPx(e.scenePos()).mappedToGrid(getGridInterval());
164   return updateLastVertexPosition(pos);
165 }
166 
processGraphicsSceneLeftMouseButtonPressed(QGraphicsSceneMouseEvent & e)167 bool BoardEditorState_DrawPolygon::processGraphicsSceneLeftMouseButtonPressed(
168     QGraphicsSceneMouseEvent& e) noexcept {
169   Board* board = getActiveBoard();
170   if (!board) return false;
171 
172   Point pos = Point::fromPx(e.scenePos()).mappedToGrid(getGridInterval());
173   if (mIsUndoCmdActive) {
174     addSegment(pos);
175   } else {
176     startAddPolygon(*board, pos);
177   }
178   return true;
179 }
180 
181 bool BoardEditorState_DrawPolygon::
processGraphicsSceneLeftMouseButtonDoubleClicked(QGraphicsSceneMouseEvent & e)182     processGraphicsSceneLeftMouseButtonDoubleClicked(
183         QGraphicsSceneMouseEvent& e) noexcept {
184   return processGraphicsSceneLeftMouseButtonPressed(e);
185 }
186 
processGraphicsSceneRightMouseButtonReleased(QGraphicsSceneMouseEvent & e)187 bool BoardEditorState_DrawPolygon::processGraphicsSceneRightMouseButtonReleased(
188     QGraphicsSceneMouseEvent& e) noexcept {
189   Q_UNUSED(e);
190   return processAbortCommand();
191 }
192 
processSwitchToBoard(int index)193 bool BoardEditorState_DrawPolygon::processSwitchToBoard(int index) noexcept {
194   // Allow switching to an existing board if no command is active.
195   return (!mIsUndoCmdActive) && (index >= 0);
196 }
197 
198 /*******************************************************************************
199  *  Private Methods
200  ******************************************************************************/
201 
startAddPolygon(Board & board,const Point & pos)202 bool BoardEditorState_DrawPolygon::startAddPolygon(Board& board,
203                                                    const Point& pos) noexcept {
204   Q_ASSERT(mIsUndoCmdActive == false);
205 
206   try {
207     // Start a new undo command
208     mContext.undoStack.beginCmdGroup(tr("Draw board polygon"));
209     mIsUndoCmdActive = true;
210 
211     // Add polygon with two vertices
212     mLastPolygonProperties.setPath(Path({Vertex(pos), Vertex(pos)}));
213     mCurrentPolygon = new BI_Polygon(
214         board, Polygon(Uuid::createRandom(), mLastPolygonProperties));
215     mContext.undoStack.appendToCmdGroup(
216         new CmdBoardPolygonAdd(*mCurrentPolygon));
217 
218     // Start undo command
219     mCurrentPolygonEditCmd.reset(
220         new CmdPolygonEdit(mCurrentPolygon->getPolygon()));
221     mLastSegmentPos = pos;
222     makeSelectedLayerVisible();
223     return true;
224   } catch (const Exception& e) {
225     QMessageBox::critical(parentWidget(), tr("Error"), e.getMsg());
226     abortCommand(false);
227     return false;
228   }
229 }
230 
addSegment(const Point & pos)231 bool BoardEditorState_DrawPolygon::addSegment(const Point& pos) noexcept {
232   Q_ASSERT(mIsUndoCmdActive == true);
233 
234   // Abort if no segment drawn
235   if (pos == mLastSegmentPos) {
236     abortCommand(true);
237     return false;
238   }
239 
240   try {
241     // Finish undo command to allow reverting segment by segment
242     if (mCurrentPolygonEditCmd) {
243       mContext.undoStack.appendToCmdGroup(mCurrentPolygonEditCmd.take());
244     }
245     mContext.undoStack.commitCmdGroup();
246     mIsUndoCmdActive = false;
247 
248     // If the polygon is now closed, abort now
249     if (mCurrentPolygon->getPolygon().getPath().isClosed()) {
250       abortCommand(true);
251       return true;
252     }
253 
254     // Start a new undo command
255     mContext.undoStack.beginCmdGroup(tr("Draw board polygon"));
256     mIsUndoCmdActive = true;
257     mCurrentPolygonEditCmd.reset(
258         new CmdPolygonEdit(mCurrentPolygon->getPolygon()));
259 
260     // Add new vertex
261     Path newPath = mCurrentPolygon->getPolygon().getPath();
262     newPath.addVertex(pos, Angle::deg0());
263     mCurrentPolygonEditCmd->setPath(newPath, true);
264     mLastSegmentPos = pos;
265     return true;
266   } catch (const Exception& e) {
267     QMessageBox::critical(parentWidget(), tr("Error"), e.getMsg());
268     abortCommand(false);
269     return false;
270   }
271 }
272 
updateLastVertexPosition(const Point & pos)273 bool BoardEditorState_DrawPolygon::updateLastVertexPosition(
274     const Point& pos) noexcept {
275   if (mCurrentPolygonEditCmd) {
276     Path newPath = mCurrentPolygon->getPolygon().getPath();
277     newPath.getVertices().last().setPos(pos);
278     mCurrentPolygonEditCmd->setPath(newPath, true);
279     return true;
280   } else {
281     return false;
282   }
283 }
284 
abortCommand(bool showErrMsgBox)285 bool BoardEditorState_DrawPolygon::abortCommand(bool showErrMsgBox) noexcept {
286   try {
287     // Delete the current edit command
288     mCurrentPolygonEditCmd.reset();
289 
290     // Abort the undo command
291     if (mIsUndoCmdActive) {
292       mContext.undoStack.abortCmdGroup();
293       mIsUndoCmdActive = false;
294     }
295 
296     // Reset attributes, go back to idle state
297     mCurrentPolygon = nullptr;
298     return true;
299   } catch (const Exception& e) {
300     if (showErrMsgBox) {
301       QMessageBox::critical(parentWidget(), tr("Error"), e.getMsg());
302     }
303     return false;
304   }
305 }
306 
layerComboBoxLayerChanged(const GraphicsLayerName & layerName)307 void BoardEditorState_DrawPolygon::layerComboBoxLayerChanged(
308     const GraphicsLayerName& layerName) noexcept {
309   mLastPolygonProperties.setLayerName(layerName);
310   if (mCurrentPolygonEditCmd) {
311     mCurrentPolygonEditCmd->setLayerName(mLastPolygonProperties.getLayerName(),
312                                          true);
313     makeSelectedLayerVisible();
314   }
315 }
316 
widthEditValueChanged(const UnsignedLength & value)317 void BoardEditorState_DrawPolygon::widthEditValueChanged(
318     const UnsignedLength& value) noexcept {
319   mLastPolygonProperties.setLineWidth(value);
320   if (mCurrentPolygonEditCmd) {
321     mCurrentPolygonEditCmd->setLineWidth(mLastPolygonProperties.getLineWidth(),
322                                          true);
323   }
324 }
325 
filledCheckBoxCheckedChanged(bool checked)326 void BoardEditorState_DrawPolygon::filledCheckBoxCheckedChanged(
327     bool checked) noexcept {
328   mLastPolygonProperties.setIsFilled(checked);
329   if (mCurrentPolygonEditCmd) {
330     mCurrentPolygonEditCmd->setIsFilled(mLastPolygonProperties.isFilled(),
331                                         true);
332     mCurrentPolygonEditCmd->setIsGrabArea(mLastPolygonProperties.isFilled(),
333                                           true);
334   }
335 }
336 
makeSelectedLayerVisible()337 void BoardEditorState_DrawPolygon::makeSelectedLayerVisible() noexcept {
338   if (mCurrentPolygon) {
339     Board& board = mCurrentPolygon->getBoard();
340     GraphicsLayer* layer =
341         board.getLayerStack().getLayer(*mLastPolygonProperties.getLayerName());
342     if (layer && layer->isEnabled()) layer->setVisible(true);
343   }
344 }
345 
346 /*******************************************************************************
347  *  End of File
348  ******************************************************************************/
349 
350 }  // namespace editor
351 }  // namespace project
352 }  // namespace librepcb
353