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 "symboleditorwidget.h"
24 
25 #include "fsm/symboleditorfsm.h"
26 #include "ui_symboleditorwidget.h"
27 
28 #include <librepcb/common/dialogs/gridsettingsdialog.h>
29 #include <librepcb/common/geometry/cmd/cmdtextedit.h>
30 #include <librepcb/common/graphics/circlegraphicsitem.h>
31 #include <librepcb/common/graphics/graphicslayer.h>
32 #include <librepcb/common/graphics/graphicsscene.h>
33 #include <librepcb/common/gridproperties.h>
34 #include <librepcb/common/utils/exclusiveactiongroup.h>
35 #include <librepcb/common/widgets/statusbar.h>
36 #include <librepcb/library/cmd/cmdlibraryelementedit.h>
37 #include <librepcb/library/cmp/cmpsigpindisplaytype.h>
38 #include <librepcb/library/msg/msgmissingauthor.h>
39 #include <librepcb/library/msg/msgmissingcategories.h>
40 #include <librepcb/library/msg/msgnamenottitlecase.h>
41 #include <librepcb/library/sym/cmd/cmdsymbolpinedit.h>
42 #include <librepcb/library/sym/msg/msgmissingsymbolname.h>
43 #include <librepcb/library/sym/msg/msgmissingsymbolvalue.h>
44 #include <librepcb/library/sym/msg/msgsymbolpinnotongrid.h>
45 #include <librepcb/library/sym/msg/msgwrongsymboltextlayer.h>
46 #include <librepcb/library/sym/symbol.h>
47 #include <librepcb/library/sym/symbolgraphicsitem.h>
48 #include <librepcb/workspace/settings/workspacesettings.h>
49 #include <librepcb/workspace/workspace.h>
50 
51 #include <QtCore>
52 #include <QtWidgets>
53 
54 /*******************************************************************************
55  *  Namespace
56  ******************************************************************************/
57 namespace librepcb {
58 namespace library {
59 namespace editor {
60 
61 /*******************************************************************************
62  *  Constructors / Destructor
63  ******************************************************************************/
64 
SymbolEditorWidget(const Context & context,const FilePath & fp,QWidget * parent)65 SymbolEditorWidget::SymbolEditorWidget(const Context& context,
66                                        const FilePath& fp, QWidget* parent)
67   : EditorWidgetBase(context, fp, parent),
68     mUi(new Ui::SymbolEditorWidget),
69     mGraphicsScene(new GraphicsScene()) {
70   mUi->setupUi(this);
71   mUi->lstMessages->setHandler(this);
72   mUi->lstMessages->setProvideFixes(!mContext.readOnly);
73   mUi->edtName->setReadOnly(mContext.readOnly);
74   mUi->edtDescription->setReadOnly(mContext.readOnly);
75   mUi->edtKeywords->setReadOnly(mContext.readOnly);
76   mUi->edtAuthor->setReadOnly(mContext.readOnly);
77   mUi->edtVersion->setReadOnly(mContext.readOnly);
78   mUi->cbxDeprecated->setCheckable(!mContext.readOnly);
79   setupErrorNotificationWidget(*mUi->errorNotificationWidget);
80   mUi->graphicsView->setUseOpenGl(
81       mContext.workspace.getSettings().useOpenGl.get());
82   mUi->graphicsView->setScene(mGraphicsScene.data());
83   connect(mUi->graphicsView, &GraphicsView::cursorScenePositionChanged, this,
84           &SymbolEditorWidget::cursorPositionChanged);
85   setWindowIcon(QIcon(":/img/library/symbol.png"));
86 
87   // Apply grid properties unit from workspace settings
88   {
89     GridProperties p = mUi->graphicsView->getGridProperties();
90     p.setUnit(mContext.workspace.getSettings().defaultLengthUnit.get());
91     mUi->graphicsView->setGridProperties(p);
92   }
93 
94   // Insert category list editor widget.
95   mCategoriesEditorWidget.reset(
96       new ComponentCategoryListEditorWidget(mContext.workspace, this));
97   mCategoriesEditorWidget->setReadOnly(mContext.readOnly);
98   mCategoriesEditorWidget->setRequiresMinimumOneEntry(true);
99   int row;
100   QFormLayout::ItemRole role;
101   mUi->formLayout->getWidgetPosition(mUi->lblCategories, &row, &role);
102   mUi->formLayout->setWidget(row, QFormLayout::FieldRole,
103                              mCategoriesEditorWidget.data());
104 
105   // Load element.
106   mSymbol.reset(new Symbol(std::unique_ptr<TransactionalDirectory>(
107       new TransactionalDirectory(mFileSystem))));  // can throw
108   updateMetadata();
109 
110   // Show "interface broken" warning when related properties are modified.
111   mOriginalSymbolPinUuids = mSymbol->getPins().getUuidSet();
112   setupInterfaceBrokenWarningWidget(*mUi->interfaceBrokenWarningWidget);
113 
114   // Reload metadata on undo stack state changes.
115   connect(mUndoStack.data(), &UndoStack::stateModified, this,
116           &SymbolEditorWidget::updateMetadata);
117 
118   // Handle changes of metadata.
119   connect(mUi->edtName, &QLineEdit::editingFinished, this,
120           &SymbolEditorWidget::commitMetadata);
121   connect(mUi->edtDescription, &PlainTextEdit::editingFinished, this,
122           &SymbolEditorWidget::commitMetadata);
123   connect(mUi->edtKeywords, &QLineEdit::editingFinished, this,
124           &SymbolEditorWidget::commitMetadata);
125   connect(mUi->edtAuthor, &QLineEdit::editingFinished, this,
126           &SymbolEditorWidget::commitMetadata);
127   connect(mUi->edtVersion, &QLineEdit::editingFinished, this,
128           &SymbolEditorWidget::commitMetadata);
129   connect(mUi->cbxDeprecated, &QCheckBox::clicked, this,
130           &SymbolEditorWidget::commitMetadata);
131   connect(mCategoriesEditorWidget.data(),
132           &ComponentCategoryListEditorWidget::edited, this,
133           &SymbolEditorWidget::commitMetadata);
134 
135   // Load graphics items recursively.
136   mGraphicsItem.reset(new SymbolGraphicsItem(*mSymbol, mContext.layerProvider));
137   mGraphicsScene->addItem(*mGraphicsItem);
138   mUi->graphicsView->zoomAll();
139 
140   // Load finite state machine (FSM).
141   SymbolEditorFsm::Context fsmContext{mContext.workspace,
142                                       *this,
143                                       *mUndoStack,
144                                       mContext.readOnly,
145                                       mContext.layerProvider,
146                                       *mGraphicsScene,
147                                       *mUi->graphicsView,
148                                       *mSymbol,
149                                       *mGraphicsItem,
150                                       *mCommandToolBarProxy};
151   mFsm.reset(new SymbolEditorFsm(fsmContext));
152 
153   // Last but not least, connect the graphics scene events with the FSM.
154   mUi->graphicsView->setEventHandlerObject(this);
155 }
156 
~SymbolEditorWidget()157 SymbolEditorWidget::~SymbolEditorWidget() noexcept {
158 }
159 
160 /*******************************************************************************
161  *  Setters
162  ******************************************************************************/
163 
setToolsActionGroup(ExclusiveActionGroup * group)164 void SymbolEditorWidget::setToolsActionGroup(
165     ExclusiveActionGroup* group) noexcept {
166   if (mToolsActionGroup) {
167     disconnect(mFsm.data(), &SymbolEditorFsm::toolChanged, mToolsActionGroup,
168                &ExclusiveActionGroup::setCurrentAction);
169   }
170 
171   EditorWidgetBase::setToolsActionGroup(group);
172 
173   if (mToolsActionGroup) {
174     bool enabled = !mContext.readOnly;
175     mToolsActionGroup->setActionEnabled(Tool::SELECT, true);
176     mToolsActionGroup->setActionEnabled(Tool::ADD_PINS, enabled);
177     mToolsActionGroup->setActionEnabled(Tool::ADD_NAMES, enabled);
178     mToolsActionGroup->setActionEnabled(Tool::ADD_VALUES, enabled);
179     mToolsActionGroup->setActionEnabled(Tool::DRAW_LINE, enabled);
180     mToolsActionGroup->setActionEnabled(Tool::DRAW_RECT, enabled);
181     mToolsActionGroup->setActionEnabled(Tool::DRAW_POLYGON, enabled);
182     mToolsActionGroup->setActionEnabled(Tool::DRAW_CIRCLE, enabled);
183     mToolsActionGroup->setActionEnabled(Tool::DRAW_TEXT, enabled);
184     mToolsActionGroup->setCurrentAction(mFsm->getCurrentTool());
185     connect(mFsm.data(), &SymbolEditorFsm::toolChanged, mToolsActionGroup,
186             &ExclusiveActionGroup::setCurrentAction);
187   }
188 }
189 
setStatusBar(StatusBar * statusbar)190 void SymbolEditorWidget::setStatusBar(StatusBar* statusbar) noexcept {
191   EditorWidgetBase::setStatusBar(statusbar);
192 
193   if (mStatusBar) {
194     mStatusBar->setLengthUnit(mUi->graphicsView->getGridProperties().getUnit());
195   }
196 }
197 
198 /*******************************************************************************
199  *  Public Slots
200  ******************************************************************************/
201 
save()202 bool SymbolEditorWidget::save() noexcept {
203   // Commit metadata.
204   QString errorMsg = commitMetadata();
205   if (!errorMsg.isEmpty()) {
206     QMessageBox::critical(this, tr("Invalid metadata"), errorMsg);
207     return false;
208   }
209 
210   // Save element.
211   try {
212     mSymbol->save();  // can throw
213     mFileSystem->save();  // can throw
214     mOriginalSymbolPinUuids = mSymbol->getPins().getUuidSet();
215     return EditorWidgetBase::save();
216   } catch (const Exception& e) {
217     QMessageBox::critical(this, tr("Save failed"), e.getMsg());
218     return false;
219   }
220 }
221 
selectAll()222 bool SymbolEditorWidget::selectAll() noexcept {
223   return mFsm->processSelectAll();
224 }
225 
cut()226 bool SymbolEditorWidget::cut() noexcept {
227   return mFsm->processCut();
228 }
229 
copy()230 bool SymbolEditorWidget::copy() noexcept {
231   return mFsm->processCopy();
232 }
233 
paste()234 bool SymbolEditorWidget::paste() noexcept {
235   return mFsm->processPaste();
236 }
237 
rotateCw()238 bool SymbolEditorWidget::rotateCw() noexcept {
239   return mFsm->processRotateCw();
240 }
241 
rotateCcw()242 bool SymbolEditorWidget::rotateCcw() noexcept {
243   return mFsm->processRotateCcw();
244 }
245 
mirror()246 bool SymbolEditorWidget::mirror() noexcept {
247   return mFsm->processMirror();
248 }
249 
remove()250 bool SymbolEditorWidget::remove() noexcept {
251   return mFsm->processRemove();
252 }
253 
zoomIn()254 bool SymbolEditorWidget::zoomIn() noexcept {
255   mUi->graphicsView->zoomIn();
256   return true;
257 }
258 
zoomOut()259 bool SymbolEditorWidget::zoomOut() noexcept {
260   mUi->graphicsView->zoomOut();
261   return true;
262 }
263 
zoomAll()264 bool SymbolEditorWidget::zoomAll() noexcept {
265   mUi->graphicsView->zoomAll();
266   return true;
267 }
268 
abortCommand()269 bool SymbolEditorWidget::abortCommand() noexcept {
270   return mFsm->processAbortCommand();
271 }
272 
importDxf()273 bool SymbolEditorWidget::importDxf() noexcept {
274   return mFsm->processStartDxfImport();
275 }
276 
editGridProperties()277 bool SymbolEditorWidget::editGridProperties() noexcept {
278   GridSettingsDialog dialog(mUi->graphicsView->getGridProperties(), this);
279   connect(&dialog, &GridSettingsDialog::gridPropertiesChanged,
280           [this](const GridProperties& grid) {
281             mUi->graphicsView->setGridProperties(grid);
282             if (mStatusBar) {
283               mStatusBar->setLengthUnit(grid.getUnit());
284             }
285           });
286   dialog.exec();
287   return true;
288 }
289 
290 /*******************************************************************************
291  *  Private Methods
292  ******************************************************************************/
293 
updateMetadata()294 void SymbolEditorWidget::updateMetadata() noexcept {
295   setWindowTitle(*mSymbol->getNames().getDefaultValue());
296   mUi->edtName->setText(*mSymbol->getNames().getDefaultValue());
297   mUi->edtDescription->setPlainText(
298       mSymbol->getDescriptions().getDefaultValue());
299   mUi->edtKeywords->setText(mSymbol->getKeywords().getDefaultValue());
300   mUi->edtAuthor->setText(mSymbol->getAuthor());
301   mUi->edtVersion->setText(mSymbol->getVersion().toStr());
302   mUi->cbxDeprecated->setChecked(mSymbol->isDeprecated());
303   mCategoriesEditorWidget->setUuids(mSymbol->getCategories());
304 }
305 
commitMetadata()306 QString SymbolEditorWidget::commitMetadata() noexcept {
307   try {
308     QScopedPointer<CmdLibraryElementEdit> cmd(
309         new CmdLibraryElementEdit(*mSymbol, tr("Edit symbol metadata")));
310     try {
311       // throws on invalid name
312       cmd->setName("", ElementName(mUi->edtName->text().trimmed()));
313     } catch (const Exception& e) {
314     }
315     cmd->setDescription("", mUi->edtDescription->toPlainText().trimmed());
316     cmd->setKeywords("", mUi->edtKeywords->text().trimmed());
317     try {
318       // throws on invalid version
319       cmd->setVersion(Version::fromString(mUi->edtVersion->text().trimmed()));
320     } catch (const Exception& e) {
321     }
322     cmd->setAuthor(mUi->edtAuthor->text().trimmed());
323     cmd->setDeprecated(mUi->cbxDeprecated->isChecked());
324     cmd->setCategories(mCategoriesEditorWidget->getUuids());
325 
326     // Commit all changes.
327     mUndoStack->execCmd(cmd.take());  // can throw
328 
329     // Reload metadata into widgets to discard invalid input.
330     updateMetadata();
331   } catch (const Exception& e) {
332     return e.getMsg();
333   }
334   return QString();
335 }
336 
graphicsViewEventHandler(QEvent * event)337 bool SymbolEditorWidget::graphicsViewEventHandler(QEvent* event) noexcept {
338   Q_ASSERT(event);
339   switch (event->type()) {
340     case QEvent::GraphicsSceneMouseMove: {
341       auto* e = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
342       Q_ASSERT(e);
343       return mFsm->processGraphicsSceneMouseMoved(*e);
344     }
345     case QEvent::GraphicsSceneMousePress: {
346       auto* e = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
347       Q_ASSERT(e);
348       switch (e->button()) {
349         case Qt::LeftButton:
350           return mFsm->processGraphicsSceneLeftMouseButtonPressed(*e);
351         default:
352           return false;
353       }
354     }
355     case QEvent::GraphicsSceneMouseRelease: {
356       auto* e = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
357       Q_ASSERT(e);
358       switch (e->button()) {
359         case Qt::LeftButton:
360           return mFsm->processGraphicsSceneLeftMouseButtonReleased(*e);
361         case Qt::RightButton:
362           return mFsm->processGraphicsSceneRightMouseButtonReleased(*e);
363         default:
364           return false;
365       }
366     }
367     case QEvent::GraphicsSceneMouseDoubleClick: {
368       auto* e = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
369       Q_ASSERT(e);
370       switch (e->button()) {
371         case Qt::LeftButton:
372           return mFsm->processGraphicsSceneLeftMouseButtonDoubleClicked(*e);
373         default:
374           return false;
375       }
376     }
377     default: { return false; }
378   }
379 }
380 
toolChangeRequested(Tool newTool)381 bool SymbolEditorWidget::toolChangeRequested(Tool newTool) noexcept {
382   switch (newTool) {
383     case Tool::SELECT:
384       return mFsm->processStartSelecting();
385     case Tool::ADD_PINS:
386       return mFsm->processStartAddingSymbolPins();
387     case Tool::ADD_NAMES:
388       return mFsm->processStartAddingNames();
389     case Tool::ADD_VALUES:
390       return mFsm->processStartAddingValues();
391     case Tool::DRAW_LINE:
392       return mFsm->processStartDrawLines();
393     case Tool::DRAW_RECT:
394       return mFsm->processStartDrawRects();
395     case Tool::DRAW_POLYGON:
396       return mFsm->processStartDrawPolygons();
397     case Tool::DRAW_CIRCLE:
398       return mFsm->processStartDrawCircles();
399     case Tool::DRAW_TEXT:
400       return mFsm->processStartDrawTexts();
401     default:
402       return false;
403   }
404 }
405 
isInterfaceBroken() const406 bool SymbolEditorWidget::isInterfaceBroken() const noexcept {
407   return mSymbol->getPins().getUuidSet() != mOriginalSymbolPinUuids;
408 }
409 
runChecks(LibraryElementCheckMessageList & msgs) const410 bool SymbolEditorWidget::runChecks(LibraryElementCheckMessageList& msgs) const {
411   if ((mFsm->getCurrentTool() != NONE) && (mFsm->getCurrentTool() != SELECT)) {
412     // Do not run checks if a tool is active because it could lead to annoying,
413     // flickering messages. For example when placing pins, they always overlap
414     // right after placing them, so we have to wait until the user has moved the
415     // cursor to place the pin at a different position.
416     return false;
417   }
418   msgs = mSymbol->runChecks();  // can throw
419   mUi->lstMessages->setMessages(msgs);
420   return true;
421 }
422 
423 template <>
fixMsg(const MsgNameNotTitleCase & msg)424 void SymbolEditorWidget::fixMsg(const MsgNameNotTitleCase& msg) {
425   mUi->edtName->setText(*msg.getFixedName());
426   commitMetadata();
427 }
428 
429 template <>
fixMsg(const MsgMissingAuthor & msg)430 void SymbolEditorWidget::fixMsg(const MsgMissingAuthor& msg) {
431   Q_UNUSED(msg);
432   mUi->edtAuthor->setText(getWorkspaceSettingsUserName());
433   commitMetadata();
434 }
435 
436 template <>
fixMsg(const MsgMissingCategories & msg)437 void SymbolEditorWidget::fixMsg(const MsgMissingCategories& msg) {
438   Q_UNUSED(msg);
439   mCategoriesEditorWidget->openAddCategoryDialog();
440 }
441 
442 template <>
fixMsg(const MsgMissingSymbolName & msg)443 void SymbolEditorWidget::fixMsg(const MsgMissingSymbolName& msg) {
444   Q_UNUSED(msg);
445   mFsm->processStartAddingNames();
446 }
447 
448 template <>
fixMsg(const MsgMissingSymbolValue & msg)449 void SymbolEditorWidget::fixMsg(const MsgMissingSymbolValue& msg) {
450   Q_UNUSED(msg);
451   mFsm->processStartAddingValues();
452 }
453 
454 template <>
fixMsg(const MsgWrongSymbolTextLayer & msg)455 void SymbolEditorWidget::fixMsg(const MsgWrongSymbolTextLayer& msg) {
456   std::shared_ptr<Text> text = mSymbol->getTexts().get(msg.getText().get());
457   QScopedPointer<CmdTextEdit> cmd(new CmdTextEdit(*text));
458   cmd->setLayerName(GraphicsLayerName(msg.getExpectedLayerName()), false);
459   mUndoStack->execCmd(cmd.take());
460 }
461 
462 template <>
fixMsg(const MsgSymbolPinNotOnGrid & msg)463 void SymbolEditorWidget::fixMsg(const MsgSymbolPinNotOnGrid& msg) {
464   std::shared_ptr<SymbolPin> pin = mSymbol->getPins().get(msg.getPin().get());
465   Point newPos = pin->getPosition().mappedToGrid(msg.getGridInterval());
466   QScopedPointer<CmdSymbolPinEdit> cmd(new CmdSymbolPinEdit(*pin));
467   cmd->setPosition(newPos, false);
468   mUndoStack->execCmd(cmd.take());
469 }
470 
471 template <typename MessageType>
fixMsgHelper(std::shared_ptr<const LibraryElementCheckMessage> msg,bool applyFix)472 bool SymbolEditorWidget::fixMsgHelper(
473     std::shared_ptr<const LibraryElementCheckMessage> msg, bool applyFix) {
474   if (msg) {
475     if (auto m = msg->as<MessageType>()) {
476       if (applyFix) fixMsg(*m);  // can throw
477       return true;
478     }
479   }
480   return false;
481 }
482 
processCheckMessage(std::shared_ptr<const LibraryElementCheckMessage> msg,bool applyFix)483 bool SymbolEditorWidget::processCheckMessage(
484     std::shared_ptr<const LibraryElementCheckMessage> msg, bool applyFix) {
485   if (fixMsgHelper<MsgNameNotTitleCase>(msg, applyFix)) return true;
486   if (fixMsgHelper<MsgMissingAuthor>(msg, applyFix)) return true;
487   if (fixMsgHelper<MsgMissingCategories>(msg, applyFix)) return true;
488   if (fixMsgHelper<MsgMissingSymbolName>(msg, applyFix)) return true;
489   if (fixMsgHelper<MsgMissingSymbolValue>(msg, applyFix)) return true;
490   if (fixMsgHelper<MsgWrongSymbolTextLayer>(msg, applyFix)) return true;
491   if (fixMsgHelper<MsgSymbolPinNotOnGrid>(msg, applyFix)) return true;
492   return false;
493 }
494 
495 /*******************************************************************************
496  *  End of File
497  ******************************************************************************/
498 
499 }  // namespace editor
500 }  // namespace library
501 }  // namespace librepcb
502