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