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