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 "libraryoverviewwidget.h"
24 
25 #include "librarylisteditorwidget.h"
26 #include "ui_libraryoverviewwidget.h"
27 
28 #include <librepcb/common/dialogs/filedialog.h>
29 #include <librepcb/common/fileio/fileutils.h>
30 #include <librepcb/library/cmd/cmdlibraryedit.h>
31 #include <librepcb/library/elements.h>
32 #include <librepcb/library/msg/msgmissingauthor.h>
33 #include <librepcb/library/msg/msgnamenottitlecase.h>
34 #include <librepcb/workspace/library/workspacelibrarydb.h>
35 #include <librepcb/workspace/settings/workspacesettings.h>
36 #include <librepcb/workspace/workspace.h>
37 
38 #include <QtCore>
39 #include <QtWidgets>
40 
41 /*******************************************************************************
42  *  Namespace
43  ******************************************************************************/
44 namespace librepcb {
45 namespace library {
46 namespace editor {
47 
48 /*******************************************************************************
49  *  Constructors / Destructor
50  ******************************************************************************/
51 
LibraryOverviewWidget(const Context & context,const FilePath & fp,QWidget * parent)52 LibraryOverviewWidget::LibraryOverviewWidget(const Context& context,
53                                              const FilePath& fp,
54                                              QWidget* parent)
55   : EditorWidgetBase(context, fp, parent),
56     mUi(new Ui::LibraryOverviewWidget),
57     mCurrentFilter() {
58   mUi->setupUi(this);
59   mUi->lstMessages->setHandler(this);
60   mUi->edtName->setReadOnly(mContext.readOnly);
61   mUi->edtDescription->setReadOnly(mContext.readOnly);
62   mUi->edtKeywords->setReadOnly(mContext.readOnly);
63   mUi->edtAuthor->setReadOnly(mContext.readOnly);
64   mUi->edtVersion->setReadOnly(mContext.readOnly);
65   mUi->cbxDeprecated->setCheckable(!mContext.readOnly);
66   mUi->edtUrl->setReadOnly(mContext.readOnly);
67   connect(mUi->btnIcon, &QPushButton::clicked, this,
68           &LibraryOverviewWidget::btnIconClicked);
69   connect(mUi->lstCmpCat, &QListWidget::doubleClicked, this,
70           &LibraryOverviewWidget::lstDoubleClicked);
71   connect(mUi->lstPkgCat, &QListWidget::doubleClicked, this,
72           &LibraryOverviewWidget::lstDoubleClicked);
73   connect(mUi->lstSym, &QListWidget::doubleClicked, this,
74           &LibraryOverviewWidget::lstDoubleClicked);
75   connect(mUi->lstPkg, &QListWidget::doubleClicked, this,
76           &LibraryOverviewWidget::lstDoubleClicked);
77   connect(mUi->lstCmp, &QListWidget::doubleClicked, this,
78           &LibraryOverviewWidget::lstDoubleClicked);
79   connect(mUi->lstDev, &QListWidget::doubleClicked, this,
80           &LibraryOverviewWidget::lstDoubleClicked);
81 
82   // Insert dependencies editor widget.
83   mDependenciesEditorWidget.reset(
84       new LibraryListEditorWidget(mContext.workspace, this));
85   mDependenciesEditorWidget->setReadOnly(mContext.readOnly);
86   int row;
87   QFormLayout::ItemRole role;
88   mUi->formLayout->getWidgetPosition(mUi->lblDependencies, &row, &role);
89   mUi->formLayout->setWidget(row, QFormLayout::FieldRole,
90                              mDependenciesEditorWidget.data());
91 
92   // Load library.
93   mLibrary.reset(new Library(std::unique_ptr<TransactionalDirectory>(
94       new TransactionalDirectory(mFileSystem))));
95   updateMetadata();
96 
97   // Reload metadata on undo stack state changes.
98   connect(mUndoStack.data(), &UndoStack::stateModified, this,
99           &LibraryOverviewWidget::updateMetadata);
100 
101   // Handle changes of metadata.
102   connect(mUi->edtName, &QLineEdit::editingFinished, this,
103           &LibraryOverviewWidget::commitMetadata);
104   connect(mUi->edtDescription, &PlainTextEdit::editingFinished, this,
105           &LibraryOverviewWidget::commitMetadata);
106   connect(mUi->edtKeywords, &QLineEdit::editingFinished, this,
107           &LibraryOverviewWidget::commitMetadata);
108   connect(mUi->edtAuthor, &QLineEdit::editingFinished, this,
109           &LibraryOverviewWidget::commitMetadata);
110   connect(mUi->edtVersion, &QLineEdit::editingFinished, this,
111           &LibraryOverviewWidget::commitMetadata);
112   connect(mUi->cbxDeprecated, &QCheckBox::clicked, this,
113           &LibraryOverviewWidget::commitMetadata);
114   connect(mUi->edtUrl, &QLineEdit::editingFinished, this,
115           &LibraryOverviewWidget::commitMetadata);
116   connect(mDependenciesEditorWidget.data(), &LibraryListEditorWidget::edited,
117           this, &LibraryOverviewWidget::commitMetadata);
118 
119   // Set up context menu triggers
120   connect(mUi->lstCmpCat, &QListWidget::customContextMenuRequested, this,
121           &LibraryOverviewWidget::openContextMenuAtPos);
122   connect(mUi->lstSym, &QListWidget::customContextMenuRequested, this,
123           &LibraryOverviewWidget::openContextMenuAtPos);
124   connect(mUi->lstCmp, &QListWidget::customContextMenuRequested, this,
125           &LibraryOverviewWidget::openContextMenuAtPos);
126   connect(mUi->lstPkgCat, &QListWidget::customContextMenuRequested, this,
127           &LibraryOverviewWidget::openContextMenuAtPos);
128   connect(mUi->lstPkg, &QListWidget::customContextMenuRequested, this,
129           &LibraryOverviewWidget::openContextMenuAtPos);
130   connect(mUi->lstDev, &QListWidget::customContextMenuRequested, this,
131           &LibraryOverviewWidget::openContextMenuAtPos);
132 
133   // Load all library elements.
134   updateElementLists();
135 
136   // Update the library element lists each time the library scan succeeded,
137   // i.e. new information about the libraries is available. Attention: Use
138   // the "scanSucceeded" signal, not "scanFinished" since "scanFinished" is
139   // also called when a scan is aborted, i.e. *no* new information is available!
140   // This can cause wrong list items after removing or adding elements, since
141   // these operations are immediately applied on the list widgets (for immediate
142   // feedback) but will then be reverted if a scan was aborted.
143   connect(&mContext.workspace.getLibraryDb(),
144           &workspace::WorkspaceLibraryDb::scanSucceeded, this,
145           &LibraryOverviewWidget::updateElementLists);
146 }
147 
~LibraryOverviewWidget()148 LibraryOverviewWidget::~LibraryOverviewWidget() noexcept {
149 }
150 
151 /*******************************************************************************
152  *  Setters
153  ******************************************************************************/
154 
setFilter(const QString & filter)155 void LibraryOverviewWidget::setFilter(const QString& filter) noexcept {
156   mCurrentFilter = filter.toLower().trimmed();
157   updateElementListFilter(*mUi->lstCmpCat);
158   updateElementListFilter(*mUi->lstPkgCat);
159   updateElementListFilter(*mUi->lstSym);
160   updateElementListFilter(*mUi->lstPkg);
161   updateElementListFilter(*mUi->lstCmp);
162   updateElementListFilter(*mUi->lstDev);
163 }
164 
165 /*******************************************************************************
166  *  Public Slots
167  ******************************************************************************/
168 
save()169 bool LibraryOverviewWidget::save() noexcept {
170   // Commit metadata.
171   QString errorMsg = commitMetadata();
172   if (!errorMsg.isEmpty()) {
173     QMessageBox::critical(this, tr("Invalid metadata"), errorMsg);
174     return false;
175   }
176 
177   // Save element.
178   try {
179     mLibrary->save();  // can throw
180     mFileSystem->save();  // can throw
181     return EditorWidgetBase::save();
182   } catch (const Exception& e) {
183     QMessageBox::critical(this, tr("Save failed"), e.getMsg());
184     return false;
185   }
186 }
187 
remove()188 bool LibraryOverviewWidget::remove() noexcept {
189   if (QListWidget* list = dynamic_cast<QListWidget*>(focusWidget())) {
190     QHash<QListWidgetItem*, FilePath> selectedItemPaths =
191         getElementListItemFilePaths(list->selectedItems());
192     if (!selectedItemPaths.empty()) {
193       removeItems(selectedItemPaths);
194       return true;
195     }
196   }
197 
198   return false;
199 }
200 
201 /*******************************************************************************
202  *  Private Methods
203  ******************************************************************************/
204 
updateMetadata()205 void LibraryOverviewWidget::updateMetadata() noexcept {
206   setWindowTitle(*mLibrary->getNames().getDefaultValue());
207   setWindowIcon(mLibrary->getIconAsPixmap());
208   mUi->btnIcon->setIcon(mLibrary->getIconAsPixmap());
209   if (mLibrary->getIconAsPixmap().isNull()) {
210     mUi->btnIcon->setText(mUi->btnIcon->toolTip());
211   } else {
212     mUi->btnIcon->setText(QString());
213   }
214   mUi->edtName->setText(*mLibrary->getNames().getDefaultValue());
215   mUi->edtDescription->setPlainText(
216       mLibrary->getDescriptions().getDefaultValue());
217   mUi->edtKeywords->setText(mLibrary->getKeywords().getDefaultValue());
218   mUi->edtAuthor->setText(mLibrary->getAuthor());
219   mUi->edtVersion->setText(mLibrary->getVersion().toStr());
220   mUi->cbxDeprecated->setChecked(mLibrary->isDeprecated());
221   mUi->edtUrl->setText(mLibrary->getUrl().toString());
222   mDependenciesEditorWidget->setUuids(mLibrary->getDependencies());
223   mIcon = mLibrary->getIcon();
224 }
225 
commitMetadata()226 QString LibraryOverviewWidget::commitMetadata() noexcept {
227   try {
228     QScopedPointer<CmdLibraryEdit> cmd(new CmdLibraryEdit(*mLibrary));
229     try {
230       // throws on invalid name
231       cmd->setName("", ElementName(mUi->edtName->text().trimmed()));
232     } catch (const Exception& e) {
233     }
234     cmd->setDescription("", mUi->edtDescription->toPlainText().trimmed());
235     cmd->setKeywords("", mUi->edtKeywords->text().trimmed());
236     try {
237       // throws on invalid version
238       cmd->setVersion(Version::fromString(mUi->edtVersion->text().trimmed()));
239     } catch (const Exception& e) {
240     }
241     cmd->setAuthor(mUi->edtAuthor->text().trimmed());
242     cmd->setDeprecated(mUi->cbxDeprecated->isChecked());
243     cmd->setUrl(QUrl::fromUserInput(mUi->edtUrl->text().trimmed()));
244     cmd->setDependencies(mDependenciesEditorWidget->getUuids());
245     cmd->setIcon(mIcon);
246 
247     // Commit all changes.
248     mUndoStack->execCmd(cmd.take());  // can throw
249 
250     // Reload metadata into widgets to discard invalid input.
251     updateMetadata();
252   } catch (const Exception& e) {
253     return e.getMsg();
254   }
255   return QString();
256 }
257 
runChecks(LibraryElementCheckMessageList & msgs) const258 bool LibraryOverviewWidget::runChecks(
259     LibraryElementCheckMessageList& msgs) const {
260   msgs = mLibrary->runChecks();  // can throw
261   mUi->lstMessages->setMessages(msgs);
262   return true;
263 }
264 
265 template <>
fixMsg(const MsgNameNotTitleCase & msg)266 void LibraryOverviewWidget::fixMsg(const MsgNameNotTitleCase& msg) {
267   mUi->edtName->setText(*msg.getFixedName());
268   commitMetadata();
269 }
270 
271 template <>
fixMsg(const MsgMissingAuthor & msg)272 void LibraryOverviewWidget::fixMsg(const MsgMissingAuthor& msg) {
273   Q_UNUSED(msg);
274   mUi->edtAuthor->setText(getWorkspaceSettingsUserName());
275   commitMetadata();
276 }
277 
278 template <typename MessageType>
fixMsgHelper(std::shared_ptr<const LibraryElementCheckMessage> msg,bool applyFix)279 bool LibraryOverviewWidget::fixMsgHelper(
280     std::shared_ptr<const LibraryElementCheckMessage> msg, bool applyFix) {
281   if (msg) {
282     if (auto m = msg->as<MessageType>()) {
283       if (applyFix) fixMsg(*m);  // can throw
284       return true;
285     }
286   }
287   return false;
288 }
289 
processCheckMessage(std::shared_ptr<const LibraryElementCheckMessage> msg,bool applyFix)290 bool LibraryOverviewWidget::processCheckMessage(
291     std::shared_ptr<const LibraryElementCheckMessage> msg, bool applyFix) {
292   if (fixMsgHelper<MsgNameNotTitleCase>(msg, applyFix)) return true;
293   if (fixMsgHelper<MsgMissingAuthor>(msg, applyFix)) return true;
294   return false;
295 }
296 
updateElementLists()297 void LibraryOverviewWidget::updateElementLists() noexcept {
298   updateElementList<ComponentCategory>(*mUi->lstCmpCat,
299                                        QIcon(":/img/places/folder.png"));
300   updateElementList<PackageCategory>(*mUi->lstPkgCat,
301                                      QIcon(":/img/places/folder_green.png"));
302   updateElementList<Symbol>(*mUi->lstSym, QIcon(":/img/library/symbol.png"));
303   updateElementList<Package>(*mUi->lstPkg, QIcon(":/img/library/package.png"));
304   updateElementList<Component>(*mUi->lstCmp,
305                                QIcon(":/img/library/component.png"));
306   updateElementList<Device>(*mUi->lstDev, QIcon(":/img/library/device.png"));
307 }
308 
309 template <typename ElementType>
updateElementList(QListWidget & listWidget,const QIcon & icon)310 void LibraryOverviewWidget::updateElementList(QListWidget& listWidget,
311                                               const QIcon& icon) noexcept {
312   QHash<FilePath, QString> elementNames;
313 
314   try {
315     // get all library element names
316     QList<FilePath> elements =
317         mContext.workspace.getLibraryDb().getLibraryElements<ElementType>(
318             mLibrary->getDirectory().getAbsPath());  // can throw
319     foreach (const FilePath& filepath, elements) {
320       QString name;
321       mContext.workspace.getLibraryDb().getElementTranslations<ElementType>(
322           filepath, getLibLocaleOrder(), &name);  // can throw
323       elementNames.insert(filepath, name);
324     }
325   } catch (const Exception& e) {
326     listWidget.clear();
327     QListWidgetItem* item = new QListWidgetItem(&listWidget);
328     item->setText(e.getMsg());
329     item->setToolTip(e.getMsg());
330     item->setIcon(QIcon(":/img/status/dialog_error.png"));
331     item->setBackground(Qt::red);
332     item->setForeground(Qt::white);
333     return;
334   }
335 
336   // update/remove existing list widget items
337   for (int i = listWidget.count() - 1; i >= 0; --i) {
338     QListWidgetItem* item = listWidget.item(i);
339     Q_ASSERT(item);
340     FilePath filePath(item->data(Qt::UserRole).toString());
341     if (elementNames.contains(filePath)) {
342       item->setText(elementNames.take(filePath));
343     } else {
344       delete item;
345     }
346   }
347 
348   // add new list widget items
349   foreach (const FilePath& fp, elementNames.keys()) {
350     QString name = elementNames.value(fp);
351     QListWidgetItem* item = new QListWidgetItem(&listWidget);
352     item->setText(name);
353     item->setToolTip(name);
354     item->setData(Qt::UserRole, fp.toStr());
355     item->setIcon(icon);
356   }
357 
358   // apply filter
359   updateElementListFilter(listWidget);
360 }
361 
362 QHash<QListWidgetItem*, FilePath>
getElementListItemFilePaths(const QList<QListWidgetItem * > & items) const363     LibraryOverviewWidget::getElementListItemFilePaths(
364         const QList<QListWidgetItem*>& items) const noexcept {
365   QHash<QListWidgetItem*, FilePath> itemPaths;
366   foreach (QListWidgetItem* item, items) {
367     FilePath fp = FilePath(item->data(Qt::UserRole).toString());
368     if (fp.isValid()) {
369       itemPaths.insert(item, fp);
370     } else {
371       qWarning() << "File path for item is not valid";
372     }
373   }
374   return itemPaths;
375 }
376 
updateElementListFilter(QListWidget & listWidget)377 void LibraryOverviewWidget::updateElementListFilter(
378     QListWidget& listWidget) noexcept {
379   for (int i = 0; i < listWidget.count(); ++i) {
380     QListWidgetItem* item = listWidget.item(i);
381     Q_ASSERT(item);
382     item->setHidden((!mCurrentFilter.isEmpty()) &&
383                     (!item->text().toLower().contains(mCurrentFilter)));
384   }
385 }
386 
openContextMenuAtPos(const QPoint & pos)387 void LibraryOverviewWidget::openContextMenuAtPos(const QPoint& pos) noexcept {
388   Q_UNUSED(pos);
389 
390   // Get list widget item file paths
391   QListWidget* list = dynamic_cast<QListWidget*>(sender());
392   Q_ASSERT(list);
393   QHash<QListWidgetItem*, FilePath> selectedItemPaths =
394       getElementListItemFilePaths(list->selectedItems());
395   QHash<QAction*, FilePath> aCopyToLibChildren;
396   QHash<QAction*, FilePath> aMoveToLibChildren;
397 
398   // Build the context menu
399   QMenu menu;
400   QAction* aEdit = menu.addAction(QIcon(":/img/actions/edit.png"),
401                                   mContext.readOnly ? tr("Open") : tr("Edit"));
402   aEdit->setVisible(!selectedItemPaths.isEmpty());
403   QAction* aDuplicate =
404       menu.addAction(QIcon(":/img/actions/clone.png"), tr("Duplicate"));
405   aDuplicate->setVisible(selectedItemPaths.count() == 1);
406   aDuplicate->setEnabled(!mContext.readOnly);
407   QAction* aRemove =
408       menu.addAction(QIcon(":/img/actions/delete.png"), tr("Remove"));
409   aRemove->setVisible(!selectedItemPaths.isEmpty());
410   aRemove->setEnabled(!mContext.readOnly);
411   if (!selectedItemPaths.isEmpty()) {
412     QMenu* menuCopyToLib = menu.addMenu(QIcon(":/img/actions/copy.png"),
413                                         tr("Copy to other library"));
414     QMenu* menuMoveToLib = menu.addMenu(QIcon(":/img/actions/move_to.png"),
415                                         tr("Move to other library"));
416     foreach (const LibraryMenuItem& item, getLocalLibraries()) {
417       if (item.filepath != mLibrary->getDirectory().getAbsPath()) {
418         QAction* actionCopy = menuCopyToLib->addAction(item.pixmap, item.name);
419         aCopyToLibChildren.insert(actionCopy, item.filepath);
420         QAction* actionMove = menuMoveToLib->addAction(item.pixmap, item.name);
421         aMoveToLibChildren.insert(actionMove, item.filepath);
422       }
423     }
424     // Disable menu item if it doesn't contain children.
425     menuCopyToLib->setEnabled(!aCopyToLibChildren.isEmpty());
426     menuMoveToLib->setEnabled((!aMoveToLibChildren.isEmpty()) &&
427                               (!mContext.readOnly));
428   }
429   QAction* aNew = menu.addAction(QIcon(":/img/actions/new.png"), tr("New"));
430   aNew->setVisible(selectedItemPaths.count() <= 1);
431   aNew->setEnabled(!mContext.readOnly);
432 
433   // Set default action
434   if (selectedItemPaths.isEmpty() && aNew->isVisible() && aNew->isEnabled()) {
435     menu.setDefaultAction(aNew);
436   } else {
437     menu.setDefaultAction(aEdit);
438   }
439 
440   // Show context menu, handle action
441   QAction* action = menu.exec(QCursor::pos());
442   if (action == aEdit) {
443     Q_ASSERT(selectedItemPaths.count() > 0);
444     foreach (const FilePath& fp, selectedItemPaths) { editItem(list, fp); }
445   } else if (action == aDuplicate) {
446     Q_ASSERT(selectedItemPaths.count() == 1);
447     duplicateItem(list, selectedItemPaths.values().first());
448   } else if (action == aRemove) {
449     Q_ASSERT(selectedItemPaths.count() > 0);
450     removeItems(selectedItemPaths);
451   } else if (action == aNew) {
452     newItem(list);
453   } else if (aCopyToLibChildren.contains(action)) {
454     Q_ASSERT(selectedItemPaths.count() > 0);
455     copyElementsToOtherLibrary(selectedItemPaths, aCopyToLibChildren[action],
456                                action->text(), false);
457   } else if (aMoveToLibChildren.contains(action)) {
458     Q_ASSERT(selectedItemPaths.count() > 0);
459     copyElementsToOtherLibrary(selectedItemPaths, aMoveToLibChildren[action],
460                                action->text(), true);
461   }
462 }
463 
newItem(QListWidget * list)464 void LibraryOverviewWidget::newItem(QListWidget* list) noexcept {
465   if (list == mUi->lstCmpCat) {
466     emit newComponentCategoryTriggered();
467   } else if (list == mUi->lstPkgCat) {
468     emit newPackageCategoryTriggered();
469   } else if (list == mUi->lstSym) {
470     emit newSymbolTriggered();
471   } else if (list == mUi->lstPkg) {
472     emit newPackageTriggered();
473   } else if (list == mUi->lstCmp) {
474     emit newComponentTriggered();
475   } else if (list == mUi->lstDev) {
476     emit newDeviceTriggered();
477   } else if (list) {
478     qCritical() << "Unknown list widget!";
479   }
480 }
481 
duplicateItem(QListWidget * list,const FilePath & fp)482 void LibraryOverviewWidget::duplicateItem(QListWidget* list,
483                                           const FilePath& fp) noexcept {
484   if (list == mUi->lstCmpCat) {
485     emit duplicateComponentCategoryTriggered(fp);
486   } else if (list == mUi->lstPkgCat) {
487     emit duplicatePackageCategoryTriggered(fp);
488   } else if (list == mUi->lstSym) {
489     emit duplicateSymbolTriggered(fp);
490   } else if (list == mUi->lstPkg) {
491     emit duplicatePackageTriggered(fp);
492   } else if (list == mUi->lstCmp) {
493     emit duplicateComponentTriggered(fp);
494   } else if (list == mUi->lstDev) {
495     emit duplicateDeviceTriggered(fp);
496   } else if (list) {
497     qCritical() << "Unknown list widget!";
498   }
499 }
500 
editItem(QListWidget * list,const FilePath & fp)501 void LibraryOverviewWidget::editItem(QListWidget* list,
502                                      const FilePath& fp) noexcept {
503   if (list == mUi->lstCmpCat) {
504     emit editComponentCategoryTriggered(fp);
505   } else if (list == mUi->lstPkgCat) {
506     emit editPackageCategoryTriggered(fp);
507   } else if (list == mUi->lstSym) {
508     emit editSymbolTriggered(fp);
509   } else if (list == mUi->lstPkg) {
510     emit editPackageTriggered(fp);
511   } else if (list == mUi->lstCmp) {
512     emit editComponentTriggered(fp);
513   } else if (list == mUi->lstDev) {
514     emit editDeviceTriggered(fp);
515   } else if (list) {
516     qCritical() << "Unknown list widget!";
517   }
518 }
519 
removeItems(const QHash<QListWidgetItem *,FilePath> & selectedItemPaths)520 void LibraryOverviewWidget::removeItems(
521     const QHash<QListWidgetItem*, FilePath>& selectedItemPaths) noexcept {
522   // Build message (list only the first few elements to avoid a huge message
523   // box)
524   QString msg = tr("WARNING: Library elements must normally NOT be removed "
525                    "because this will break other elements which depend on "
526                    "this one! They should be just marked as deprecated "
527                    "instead.\n\nAre you still sure to delete the following "
528                    "library elements?") %
529       "\n\n";
530   QList<QListWidgetItem*> listedItems = selectedItemPaths.keys().mid(0, 10);
531   foreach (QListWidgetItem* item, listedItems) {
532     msg.append(" - " % item->text() % "\n");
533   }
534   if (selectedItemPaths.count() > listedItems.count()) {
535     msg.append(" - ...\n");
536   }
537   msg.append("\n" % tr("This cannot be undone!"));
538 
539   // Show message box
540   int ret = QMessageBox::warning(
541       this, tr("Remove %1 elements").arg(selectedItemPaths.count()), msg,
542       QMessageBox::Yes, QMessageBox::Cancel);
543   if (ret == QMessageBox::Yes) {
544     foreach (QListWidgetItem* item, selectedItemPaths.keys()) {
545       FilePath itemPath = selectedItemPaths.value(item);
546       try {
547         // Emit signal so that the library editor can close any tabs that have
548         // opened this item
549         emit removeElementTriggered(itemPath);
550         FileUtils::removeDirRecursively(itemPath);
551         delete item;  // Remove from list
552       } catch (const Exception& e) {
553         QMessageBox::critical(this, tr("Error"), e.getMsg());
554       }
555     }
556     mContext.workspace.getLibraryDb().startLibraryRescan();
557   }
558 }
559 
copyElementsToOtherLibrary(const QHash<QListWidgetItem *,FilePath> & selectedItemPaths,const FilePath & libFp,const QString & libName,bool removeFromSource)560 void LibraryOverviewWidget::copyElementsToOtherLibrary(
561     const QHash<QListWidgetItem*, FilePath>& selectedItemPaths,
562     const FilePath& libFp, const QString& libName,
563     bool removeFromSource) noexcept {
564   // Build message (list only the first few elements to avoid a huge message
565   // box)
566   QString msg = removeFromSource
567       ? tr("Are you sure to move the following elements into the library '%1'?")
568       : tr("Are you sure to copy the following elements into the library "
569            "'%1'?");
570   msg = msg.arg(libName) % "\n\n";
571   QList<QListWidgetItem*> listedItems = selectedItemPaths.keys().mid(0, 10);
572   foreach (QListWidgetItem* item, listedItems) {
573     msg.append(" - " % item->text() % "\n");
574   }
575   if (selectedItemPaths.count() > listedItems.count()) {
576     msg.append(" - ...\n");
577   }
578   msg.append("\n" % tr("Note: This cannot be easily undone!"));
579 
580   // Show message box
581   QString title =
582       removeFromSource ? tr("Move %1 elements") : tr("Copy %1 elements");
583   int ret = QMessageBox::warning(this, title.arg(selectedItemPaths.count()),
584                                  msg, QMessageBox::Yes, QMessageBox::Cancel);
585   if (ret == QMessageBox::Yes) {
586     foreach (QListWidgetItem* item, selectedItemPaths.keys()) {
587       FilePath itemPath = selectedItemPaths.value(item);
588       QString relativePath =
589           itemPath.toRelative(itemPath.getParentDir().getParentDir());
590       FilePath destination = libFp.getPathTo(relativePath);
591       try {
592         if (removeFromSource) {
593           qInfo() << "Move library element from" << itemPath.toNative() << "to"
594                   << destination.toNative();
595           // Emit signal so that the library editor can close any tabs that have
596           // opened this item
597           emit removeElementTriggered(itemPath);
598           FileUtils::move(itemPath, destination);
599           delete item;  // Remove from list
600         } else {
601           qInfo() << "Copy library element from" << itemPath.toNative() << "to"
602                   << destination.toNative();
603           FileUtils::copyDirRecursively(itemPath, destination);
604         }
605       } catch (const Exception& e) {
606         QMessageBox::critical(this, tr("Error"), e.getMsg());
607       }
608     }
609     mContext.workspace.getLibraryDb().startLibraryRescan();
610   }
611 }
612 
613 QList<LibraryOverviewWidget::LibraryMenuItem>
getLocalLibraries() const614     LibraryOverviewWidget::getLocalLibraries() const noexcept {
615   QList<LibraryMenuItem> libs;
616   try {
617     QMultiMap<Version, FilePath> libraries =
618         mContext.workspace.getLibraryDb().getLibraries();  // can throw
619     foreach (const FilePath& libDir, libraries) {
620       // Don't list remote libraries since they are read-only!
621       if (libDir.isLocatedInDir(mContext.workspace.getLocalLibrariesPath())) {
622         QString name;
623         mContext.workspace.getLibraryDb().getElementTranslations<Library>(
624             libDir, getLibLocaleOrder(), &name);  // can throw
625         QPixmap icon;
626         mContext.workspace.getLibraryDb().getLibraryMetadata(
627             libDir,
628             &icon);  // can throw
629         libs.append(LibraryMenuItem{name, icon, libDir});
630       }
631     }
632   } catch (const Exception& e) {
633     qCritical() << "Could not list local libraries:" << e.getMsg();
634   }
635   // sort by name
636   std::sort(libs.begin(), libs.end(),
637             [](const LibraryMenuItem& lhs, const LibraryMenuItem& rhs) {
638               return lhs.name < rhs.name;
639             });
640   return libs;
641 }
642 
643 /*******************************************************************************
644  *  Event Handlers
645  ******************************************************************************/
646 
btnIconClicked()647 void LibraryOverviewWidget::btnIconClicked() noexcept {
648   if (mContext.readOnly) return;
649 
650   QString fp = FileDialog::getOpenFileName(
651       this, tr("Choose library icon"),
652       mLibrary->getDirectory().getAbsPath().toNative(),
653       tr("Portable Network Graphics (*.png)"));
654   if (!fp.isEmpty()) {
655     try {
656       mIcon = FileUtils::readFile(FilePath(fp));  // can throw
657       commitMetadata();
658     } catch (const Exception& e) {
659       QMessageBox::critical(this, tr("Could not open file"), e.getMsg());
660     }
661   }
662 }
663 
lstDoubleClicked(const QModelIndex & index)664 void LibraryOverviewWidget::lstDoubleClicked(
665     const QModelIndex& index) noexcept {
666   // Get list widget
667   QListWidget* list = dynamic_cast<QListWidget*>(sender());
668   Q_ASSERT(list);
669   QListWidgetItem* item = list->item(index.row());
670   FilePath fp =
671       item ? FilePath(item->data(Qt::UserRole).toString()) : FilePath();
672   if (fp.isValid()) {
673     editItem(list, fp);
674   }
675 }
676 
677 /*******************************************************************************
678  *  End of File
679  ******************************************************************************/
680 
681 }  // namespace editor
682 }  // namespace library
683 }  // namespace librepcb
684