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