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 "addlibrarywidget.h"
24 
25 #include "librarydownload.h"
26 #include "repositorylibrarylistwidgetitem.h"
27 #include "ui_addlibrarywidget.h"
28 
29 #include <librepcb/common/application.h>
30 #include <librepcb/common/fileio/fileutils.h>
31 #include <librepcb/common/fileio/transactionalfilesystem.h>
32 #include <librepcb/common/network/repository.h>
33 #include <librepcb/library/library.h>
34 #include <librepcb/workspace/settings/workspacesettings.h>
35 #include <librepcb/workspace/workspace.h>
36 
37 #include <QtCore>
38 #include <QtWidgets>
39 
40 /*******************************************************************************
41  *  Namespace
42  ******************************************************************************/
43 namespace librepcb {
44 namespace library {
45 namespace manager {
46 
47 /*******************************************************************************
48  *  Constructors / Destructor
49  ******************************************************************************/
50 
AddLibraryWidget(workspace::Workspace & ws)51 AddLibraryWidget::AddLibraryWidget(workspace::Workspace& ws) noexcept
52   : QWidget(nullptr), mWorkspace(ws), mUi(new Ui::AddLibraryWidget) {
53   mUi->setupUi(this);
54   connect(mUi->btnDownloadZip, &QPushButton::clicked, this,
55           &AddLibraryWidget::downloadZippedLibraryButtonClicked);
56   connect(mUi->btnLocalCreate, &QPushButton::clicked, this,
57           &AddLibraryWidget::createLocalLibraryButtonClicked);
58   connect(mUi->edtLocalName, &QLineEdit::textChanged, this,
59           &AddLibraryWidget::localLibraryNameLineEditTextChanged);
60   connect(mUi->edtDownloadZipUrl, &QLineEdit::textChanged, this,
61           &AddLibraryWidget::downloadZipUrlLineEditTextChanged);
62   connect(mUi->btnRepoLibsDownload, &QPushButton::clicked, this,
63           &AddLibraryWidget::downloadLibrariesFromRepositoryButtonClicked);
64 
65   // tab "create local library": set placeholder texts
66   mUi->edtLocalName->setPlaceholderText("My Library");
67   mUi->edtLocalAuthor->setPlaceholderText(
68       mWorkspace.getSettings().userName.get());
69   mUi->edtLocalVersion->setPlaceholderText("0.1");
70   mUi->edtLocalUrl->setPlaceholderText(
71       tr("e.g. the URL to the Git repository (optional)"));
72   localLibraryNameLineEditTextChanged(mUi->edtLocalName->text());
73 
74   // tab "download ZIP": set placeholder texts and hide widgets
75   mUi->edtDownloadZipUrl->setPlaceholderText(
76       tr("e.g. "
77          "https://github.com/LibrePCB-Libraries/LibrePCB_Base.lplib/archive/"
78          "master.zip"));
79   mUi->prgDownloadZipProgress->setVisible(false);
80   mUi->btnDownloadZipAbort->setVisible(false);
81   mUi->lblDownloadZipStatusMsg->setText("");
82 
83   // select the default tab
84   mUi->tabWidget->setCurrentIndex(0);
85 }
86 
~AddLibraryWidget()87 AddLibraryWidget::~AddLibraryWidget() noexcept {
88   clearRepositoryLibraryList();
89 }
90 
91 /*******************************************************************************
92  *  General Methods
93  ******************************************************************************/
94 
updateRepositoryLibraryList()95 void AddLibraryWidget::updateRepositoryLibraryList() noexcept {
96   clearRepositoryLibraryList();
97   foreach (const QUrl& url, mWorkspace.getSettings().repositoryUrls.get()) {
98     std::shared_ptr<Repository> repo = std::make_shared<Repository>(url);
99     connect(repo.get(), &Repository::libraryListReceived, this,
100             &AddLibraryWidget::repositoryLibraryListReceived);
101     connect(repo.get(), &Repository::errorWhileFetchingLibraryList, this,
102             &AddLibraryWidget::errorWhileFetchingLibraryList);
103     repo->requestLibraryList();
104     mRepositories.append(repo);
105   }
106 }
107 
108 /*******************************************************************************
109  *  Private Methods
110  ******************************************************************************/
111 
localLibraryNameLineEditTextChanged(QString name)112 void AddLibraryWidget::localLibraryNameLineEditTextChanged(
113     QString name) noexcept {
114   if (name.isEmpty()) name = mUi->edtLocalName->placeholderText();
115   QString dirname = FilePath::cleanFileName(
116       name, FilePath::ReplaceSpaces | FilePath::KeepCase);
117   if (!dirname.endsWith(".lplib")) dirname.append(".lplib");
118   mUi->edtLocalDirectory->setPlaceholderText(dirname);
119 }
120 
downloadZipUrlLineEditTextChanged(QString urlStr)121 void AddLibraryWidget::downloadZipUrlLineEditTextChanged(
122     QString urlStr) noexcept {
123   QString left = urlStr.left(urlStr.indexOf(".lplib", Qt::CaseInsensitive));
124   QString libName = left.right(left.length() - left.lastIndexOf("/"));
125   if (libName == urlStr) {
126     libName = QUrl(urlStr).fileName();
127   }
128   QString dirname = FilePath::cleanFileName(
129       libName, FilePath::ReplaceSpaces | FilePath::KeepCase);
130   if (dirname.contains(".zip")) {
131     dirname.remove(".zip");
132   }
133   if (!dirname.isEmpty()) {
134     dirname.append(".lplib");
135   }
136   mUi->edtDownloadZipDirectory->setPlaceholderText(dirname);
137 }
138 
createLocalLibraryButtonClicked()139 void AddLibraryWidget::createLocalLibraryButtonClicked() noexcept {
140   // get attributes
141   QString name = getTextOrPlaceholderFromQLineEdit(mUi->edtLocalName, false);
142   QString desc =
143       getTextOrPlaceholderFromQLineEdit(mUi->edtLocalDescription, false);
144   QString author =
145       getTextOrPlaceholderFromQLineEdit(mUi->edtLocalAuthor, false);
146   QString versionStr =
147       getTextOrPlaceholderFromQLineEdit(mUi->edtLocalVersion, false);
148   tl::optional<Version> version = Version::tryFromString(versionStr);
149   QString urlStr = mUi->edtLocalUrl->text().trimmed();
150   QUrl url = QUrl::fromUserInput(urlStr);
151   bool useCc0License = mUi->cbxLocalCc0License->isChecked();
152   QString directoryStr =
153       getTextOrPlaceholderFromQLineEdit(mUi->edtLocalDirectory, true);
154   if ((!directoryStr.isEmpty()) && (!directoryStr.endsWith(".lplib"))) {
155     directoryStr.append(".lplib");
156   }
157   FilePath directory =
158       mWorkspace.getLibrariesPath().getPathTo("local/" % directoryStr);
159 
160   // check attributes validity
161   if (name.isEmpty()) {
162     QMessageBox::critical(this, tr("Invalid Input"),
163                           tr("Please enter a name."));
164     return;
165   }
166   if (author.isEmpty()) {
167     QMessageBox::critical(this, tr("Invalid Input"),
168                           tr("Please enter an author."));
169     return;
170   }
171   if (!version) {
172     QMessageBox::critical(this, tr("Invalid Input"),
173                           tr("The specified version number is not valid."));
174     return;
175   }
176   if (!url.isValid() && !urlStr.isEmpty()) {
177     QMessageBox::critical(this, tr("Invalid Input"),
178                           tr("The specified URL is not valid."));
179     return;
180   }
181   if (directoryStr.isEmpty()) {
182     QMessageBox::critical(this, tr("Invalid Input"),
183                           tr("Please enter a directory name."));
184     return;
185   }
186   if (directory.isExistingFile() || directory.isExistingDir()) {
187     QMessageBox::critical(this, tr("Invalid Input"),
188                           tr("The specified directory exists already."));
189     return;
190   }
191 
192   try {
193     // create transactional file system
194     std::shared_ptr<TransactionalFileSystem> fs =
195         TransactionalFileSystem::openRW(directory);
196     TransactionalDirectory dir(fs);
197 
198     // create the new library
199     QScopedPointer<Library> lib(new Library(Uuid::createRandom(), *version,
200                                             author, ElementName(name), desc,
201                                             QString("")));  // can throw
202     lib->setUrl(url);
203     try {
204       lib->setIcon(FileUtils::readFile(
205           qApp->getResourcesDir().getPathTo("library/default_image.png")));
206     } catch (const Exception& e) {
207       qCritical() << "Could not open the library image:" << e.getMsg();
208     }
209     lib->moveTo(dir);  // can throw
210 
211     // copy license file
212     if (useCc0License) {
213       try {
214         FilePath source =
215             qApp->getResourcesDir().getPathTo("licenses/cc0-1.0.txt");
216         fs->write("LICENSE.txt", FileUtils::readFile(source));  // can throw
217       } catch (Exception& e) {
218         qCritical() << "Could not copy the license file:" << e.getMsg();
219       }
220     }
221 
222     // copy readme file
223     try {
224       FilePath source =
225           qApp->getResourcesDir().getPathTo("library/readme_template");
226       QByteArray content = FileUtils::readFile(source);  // can throw
227       content.replace("{LIBRARY_NAME}", name.toUtf8());
228       if (useCc0License) {
229         content.replace("{LICENSE_TEXT}",
230                         "Creative Commons (CC0-1.0). For the "
231                         "license text, see [LICENSE.txt](LICENSE.txt).");
232       } else {
233         content.replace("{LICENSE_TEXT}", "No license set.");
234       }
235       fs->write("README.md", content);  // can throw
236     } catch (Exception& e) {
237       qCritical() << "Could not copy the readme file:" << e.getMsg();
238     }
239 
240     // copy .gitignore
241     try {
242       FilePath source =
243           qApp->getResourcesDir().getPathTo("library/gitignore_template");
244       fs->write(".gitignore", FileUtils::readFile(source));  // can throw
245     } catch (Exception& e) {
246       qCritical() << "Could not copy the .gitignore file:" << e.getMsg();
247     }
248 
249     // copy .gitattributes
250     try {
251       FilePath source =
252           qApp->getResourcesDir().getPathTo("library/gitattributes_template");
253       fs->write(".gitattributes", FileUtils::readFile(source));  // can throw
254     } catch (Exception& e) {
255       qCritical() << "Could not copy the .gitattributes file:" << e.getMsg();
256     }
257 
258     // save file system
259     fs->save();  // can throw
260 
261     // library successfully added! reset input fields and emit signal
262     mUi->edtLocalName->clear();
263     mUi->edtLocalDescription->clear();
264     mUi->edtLocalAuthor->clear();
265     mUi->edtLocalVersion->clear();
266     mUi->edtLocalUrl->clear();
267     mUi->cbxLocalCc0License->setChecked(false);
268     mUi->edtLocalDirectory->clear();
269     emit libraryAdded(directory);
270   } catch (Exception& e) {
271     QMessageBox::critical(this, tr("Error"), e.getMsg());
272   }
273 }
274 
downloadZippedLibraryButtonClicked()275 void AddLibraryWidget::downloadZippedLibraryButtonClicked() noexcept {
276   if (mManualLibraryDownload) {
277     QMessageBox::critical(this, tr("Busy"),
278                           tr("A download is already running."));
279     return;
280   }
281 
282   // get attributes
283   QUrl url = QUrl::fromUserInput(mUi->edtDownloadZipUrl->text().trimmed());
284   QString dirStr =
285       getTextOrPlaceholderFromQLineEdit(mUi->edtDownloadZipDirectory, true);
286   if ((!dirStr.isEmpty()) && (!dirStr.endsWith(".lplib"))) {
287     dirStr.append(".lplib");
288   }
289   FilePath extractToDir =
290       mWorkspace.getLibrariesPath().getPathTo("local/" % dirStr);
291 
292   // check attributes validity
293   if (!url.isValid()) {
294     QMessageBox::critical(this, tr("Invalid Input"),
295                           tr("Please enter a valid URL."));
296     return;
297   }
298   if ((dirStr.isEmpty()) || (!extractToDir.isValid())) {
299     QMessageBox::critical(this, tr("Invalid Input"),
300                           tr("Please enter a valid directory."));
301     return;
302   }
303   if (extractToDir.isExistingFile() || extractToDir.isExistingDir()) {
304     QMessageBox::critical(this, tr("Directory exists already"),
305                           tr("The directory \"%1\" exists already.")
306                               .arg(extractToDir.toNative()));
307     return;
308   }
309 
310   // update widgets
311   mUi->btnDownloadZip->setEnabled(false);
312   mUi->btnDownloadZipAbort->setVisible(true);
313   mUi->prgDownloadZipProgress->setVisible(true);
314   mUi->prgDownloadZipProgress->setValue(0);
315   mUi->lblDownloadZipStatusMsg->setText("");
316   mUi->lblDownloadZipStatusMsg->setStyleSheet("");
317 
318   // download library
319   mManualLibraryDownload.reset(new LibraryDownload(url, extractToDir));
320   connect(mManualLibraryDownload.data(), &LibraryDownload::progressState,
321           mUi->lblDownloadZipStatusMsg, &QLabel::setText);
322   connect(mManualLibraryDownload.data(), &LibraryDownload::progressPercent,
323           mUi->prgDownloadZipProgress, &QProgressBar::setValue);
324   connect(mManualLibraryDownload.data(), &LibraryDownload::finished, this,
325           &AddLibraryWidget::downloadZipFinished);
326   connect(mUi->btnDownloadZipAbort, &QPushButton::clicked,
327           mManualLibraryDownload.data(), &LibraryDownload::abort);
328   mManualLibraryDownload->start();
329 }
330 
downloadZipFinished(bool success,const QString & errMsg)331 void AddLibraryWidget::downloadZipFinished(bool success,
332                                            const QString& errMsg) noexcept {
333   Q_ASSERT(mManualLibraryDownload);
334 
335   if (success) {
336     mUi->lblDownloadZipStatusMsg->setText("");
337     emit libraryAdded(mManualLibraryDownload->getDestinationDir());
338   } else {
339     mUi->lblDownloadZipStatusMsg->setText(errMsg);
340   }
341 
342   // update widgets
343   mUi->btnDownloadZip->setEnabled(true);
344   mUi->btnDownloadZipAbort->setVisible(false);
345   mUi->prgDownloadZipProgress->setVisible(false);
346   mUi->lblDownloadZipStatusMsg->setStyleSheet("QLabel {color: red;}");
347 
348   // delete download helper
349   mManualLibraryDownload.reset();
350 }
351 
repositoryLibraryListReceived(const QJsonArray & libs)352 void AddLibraryWidget::repositoryLibraryListReceived(
353     const QJsonArray& libs) noexcept {
354   foreach (const QJsonValue& libVal, libs) {
355     RepositoryLibraryListWidgetItem* widget =
356         new RepositoryLibraryListWidgetItem(mWorkspace, libVal.toObject());
357     widget->setChecked(mUi->cbxRepoLibsSelectAll->isChecked());
358     connect(mUi->cbxRepoLibsSelectAll, &QCheckBox::clicked, widget,
359             &RepositoryLibraryListWidgetItem::setChecked);
360     connect(widget, &RepositoryLibraryListWidgetItem::checkedChanged, this,
361             &AddLibraryWidget::repoLibraryDownloadCheckedChanged);
362     QListWidgetItem* item = new QListWidgetItem(mUi->lstRepoLibs);
363     item->setSizeHint(widget->sizeHint());
364     mUi->lstRepoLibs->setItemWidget(item, widget);
365   }
366 }
367 
errorWhileFetchingLibraryList(const QString & errorMsg)368 void AddLibraryWidget::errorWhileFetchingLibraryList(
369     const QString& errorMsg) noexcept {
370   QListWidgetItem* item = new QListWidgetItem(errorMsg, mUi->lstRepoLibs);
371   item->setBackground(Qt::red);
372   item->setForeground(Qt::white);
373 }
374 
clearRepositoryLibraryList()375 void AddLibraryWidget::clearRepositoryLibraryList() noexcept {
376   mRepositories.clear();  // disconnects all signal/slot connections
377   for (int i = mUi->lstRepoLibs->count() - 1; i >= 0; i--) {
378     QListWidgetItem* item = mUi->lstRepoLibs->item(i);
379     Q_ASSERT(item);
380     delete mUi->lstRepoLibs->itemWidget(item);
381     delete item;
382   }
383   Q_ASSERT(mUi->lstRepoLibs->count() == 0);
384 }
385 
repoLibraryDownloadCheckedChanged(bool checked)386 void AddLibraryWidget::repoLibraryDownloadCheckedChanged(
387     bool checked) noexcept {
388   if (checked) {
389     // one more library is checked, check all dependencies too
390     QSet<Uuid> libs;
391     for (int i = 0; i < mUi->lstRepoLibs->count(); i++) {
392       QListWidgetItem* item = mUi->lstRepoLibs->item(i);
393       Q_ASSERT(item);
394       auto* widget = dynamic_cast<RepositoryLibraryListWidgetItem*>(
395           mUi->lstRepoLibs->itemWidget(item));
396       if (widget && widget->isChecked()) {
397         libs.unite(widget->getDependencies());
398       }
399     }
400     for (int i = 0; i < mUi->lstRepoLibs->count(); i++) {
401       QListWidgetItem* item = mUi->lstRepoLibs->item(i);
402       Q_ASSERT(item);
403       auto* widget = dynamic_cast<RepositoryLibraryListWidgetItem*>(
404           mUi->lstRepoLibs->itemWidget(item));
405       if (widget && widget->getUuid() && (libs.contains(*widget->getUuid()))) {
406         widget->setChecked(true);
407       }
408     }
409   } else {
410     // one library was unchecked, uncheck all libraries with missing
411     // dependencies
412     QSet<Uuid> libs;
413     for (int i = 0; i < mUi->lstRepoLibs->count(); i++) {
414       QListWidgetItem* item = mUi->lstRepoLibs->item(i);
415       Q_ASSERT(item);
416       auto* widget = dynamic_cast<RepositoryLibraryListWidgetItem*>(
417           mUi->lstRepoLibs->itemWidget(item));
418       if (widget && widget->isChecked() && widget->getUuid()) {
419         libs.insert(*widget->getUuid());
420       }
421     }
422     for (int i = 0; i < mUi->lstRepoLibs->count(); i++) {
423       QListWidgetItem* item = mUi->lstRepoLibs->item(i);
424       Q_ASSERT(item);
425       auto* widget = dynamic_cast<RepositoryLibraryListWidgetItem*>(
426           mUi->lstRepoLibs->itemWidget(item));
427       if (widget && (!libs.contains(widget->getDependencies()))) {
428         widget->setChecked(false);
429       }
430     }
431   }
432 }
433 
downloadLibrariesFromRepositoryButtonClicked()434 void AddLibraryWidget::downloadLibrariesFromRepositoryButtonClicked() noexcept {
435   for (int i = 0; i < mUi->lstRepoLibs->count(); i++) {
436     QListWidgetItem* item = mUi->lstRepoLibs->item(i);
437     Q_ASSERT(item);
438     auto* widget = dynamic_cast<RepositoryLibraryListWidgetItem*>(
439         mUi->lstRepoLibs->itemWidget(item));
440     if (widget) {
441       widget->startDownloadIfSelected();
442     } else {
443       qWarning() << "Invalid item widget detected.";
444     }
445   }
446 }
447 
448 /*******************************************************************************
449  *  Private Static Methods
450  ******************************************************************************/
451 
getTextOrPlaceholderFromQLineEdit(QLineEdit * edit,bool isFilename)452 QString AddLibraryWidget::getTextOrPlaceholderFromQLineEdit(
453     QLineEdit* edit, bool isFilename) noexcept {
454   if (edit) {
455     QString text = edit->text().trimmed();
456     QString placeholder = edit->placeholderText().trimmed();
457     QString retval = (text.length() > 0) ? text : placeholder;
458     if (isFilename) {
459       return FilePath::cleanFileName(
460           retval, FilePath::ReplaceSpaces | FilePath::KeepCase);
461     } else {
462       return retval;
463     }
464   } else {
465     return QString("");
466   }
467 }
468 
469 /*******************************************************************************
470  *  End of File
471  ******************************************************************************/
472 
473 }  // namespace manager
474 }  // namespace library
475 }  // namespace librepcb
476