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 "project.h"
24
25 #include "boards/board.h"
26 #include "circuit/circuit.h"
27 #include "erc/ercmsglist.h"
28 #include "library/projectlibrary.h"
29 #include "metadata/projectmetadata.h"
30 #include "schematics/schematic.h"
31 #include "schematics/schematiclayerprovider.h"
32 #include "settings/projectsettings.h"
33
34 #include <librepcb/common/application.h>
35 #include <librepcb/common/exceptions.h>
36 #include <librepcb/common/fileio/directorylock.h>
37 #include <librepcb/common/fileio/fileutils.h>
38 #include <librepcb/common/fileio/sexpression.h>
39 #include <librepcb/common/fileio/versionfile.h>
40 #include <librepcb/common/font/strokefontpool.h>
41
42 #include <QPrinter>
43 #include <QtCore>
44
45 /*******************************************************************************
46 * Namespace
47 ******************************************************************************/
48 namespace librepcb {
49 namespace project {
50
51 /*******************************************************************************
52 * Constructors / Destructor
53 ******************************************************************************/
54
Project(std::unique_ptr<TransactionalDirectory> directory,const QString & filename,bool create)55 Project::Project(std::unique_ptr<TransactionalDirectory> directory,
56 const QString& filename, bool create)
57 : QObject(nullptr),
58 AttributeProvider(),
59 mDirectory(std::move(directory)),
60 mFilename(filename) {
61 qDebug() << (create ? "create project:" : "open project:")
62 << getFilepath().toNative();
63
64 // Check if the file extension is correct
65 if (!mFilename.endsWith(".lpp")) {
66 throw RuntimeError(__FILE__, __LINE__,
67 tr("The suffix of the project file must be \"lpp\"!"));
68 }
69
70 Version fileFormat = qApp->getFileFormatVersion();
71 if (create) {
72 // Check if there isn't already a project in the selected directory
73 if (mDirectory->fileExists(".librepcb-project") ||
74 mDirectory->fileExists(mFilename)) {
75 throw RuntimeError(
76 __FILE__, __LINE__,
77 QString(
78 tr("The directory \"%1\" already contains a LibrePCB project."))
79 .arg(getPath().toNative()));
80 }
81 } else {
82 // check if the project does exist
83 if (!mDirectory->fileExists(".librepcb-project")) {
84 throw RuntimeError(
85 __FILE__, __LINE__,
86 QString(
87 tr("The directory \"%1\" does not contain a LibrePCB project."))
88 .arg(getPath().toNative()));
89 }
90 if (!mDirectory->fileExists(mFilename)) {
91 throw RuntimeError(
92 __FILE__, __LINE__,
93 tr("The file \"%1\" does not exist.").arg(getFilepath().toNative()));
94 }
95 // check the project's file format version
96 fileFormat =
97 VersionFile::fromByteArray(mDirectory->read(".librepcb-project"))
98 .getVersion();
99 if (fileFormat > qApp->getFileFormatVersion()) {
100 throw RuntimeError(
101 __FILE__, __LINE__,
102 QString(
103 tr("This project was created with a newer application version.\n"
104 "You need at least LibrePCB %1 to open it.\n\n%2"))
105 .arg(fileFormat.toPrettyStr(3))
106 .arg(getFilepath().toNative()));
107 }
108 }
109
110 // OK - the project is locked (or read-only) and can be opened!
111 // Until this line, there was no memory allocated on the heap. But in the rest
112 // of the constructor, a lot of object will be created on the heap. If an
113 // exception is thrown somewhere, we must ensure that all the allocated memory
114 // gets freed. This is done by a try/catch block. In the catch-block, all
115 // allocated memory will be freed. Then the exception is rethrown to leave the
116 // constructor.
117
118 try {
119 // copy and/or load stroke fonts
120 TransactionalDirectory fontobeneDir(*mDirectory, "resources/fontobene");
121 if (create) {
122 FilePath src = qApp->getResourcesFilePath("fontobene");
123 foreach (const FilePath& fp,
124 FileUtils::getFilesInDirectory(src, {"*.bene"})) {
125 if (fp.getSuffix() == "bene") {
126 fontobeneDir.write(fp.getFilename(), FileUtils::readFile(fp));
127 }
128 }
129 }
130 mStrokeFontPool.reset(new StrokeFontPool(fontobeneDir));
131
132 // Create or load metadata
133 if (create) {
134 QString name = cleanElementName(getFilepath().getCompleteBasename());
135 if (!ElementNameConstraint()(name)) {
136 name = "New Project"; // fallback if the filename is not a valid name
137 }
138 mProjectMetadata.reset(new ProjectMetadata(
139 Uuid::createRandom(), ElementName(name), tr("Unknown"), "v1",
140 QDateTime::currentDateTime(), QDateTime::currentDateTime()));
141 } else {
142 QString fp = "project/metadata.lp";
143 SExpression root =
144 SExpression::parse(mDirectory->read(fp), mDirectory->getAbsPath(fp));
145 mProjectMetadata.reset(new ProjectMetadata(root, fileFormat));
146 }
147
148 // Create all needed objects
149 connect(mProjectMetadata.data(), &ProjectMetadata::attributesChanged, this,
150 &Project::attributesChanged);
151 mProjectSettings.reset(new ProjectSettings(*this, fileFormat, create));
152 mProjectLibrary.reset(
153 new ProjectLibrary(std::unique_ptr<TransactionalDirectory>(
154 new TransactionalDirectory(*mDirectory, "library"))));
155 mErcMsgList.reset(new ErcMsgList(*this));
156 mCircuit.reset(new Circuit(*this, fileFormat, create));
157
158 // Load all schematic layers
159 mSchematicLayerProvider.reset(new SchematicLayerProvider(*this));
160
161 // Load all schematics
162 if (!create) {
163 QString fp = "schematics/schematics.lp";
164 SExpression schRoot =
165 SExpression::parse(mDirectory->read(fp), mDirectory->getAbsPath(fp));
166 foreach (const SExpression& node, schRoot.getChildren("schematic")) {
167 FilePath fp =
168 FilePath::fromRelative(getPath(), node.getChild("@0").getValue());
169 std::unique_ptr<TransactionalDirectory> dir(new TransactionalDirectory(
170 *mDirectory, fp.getParentDir().toRelative(getPath())));
171 Schematic* schematic = new Schematic(*this, std::move(dir), fileFormat);
172 addSchematic(*schematic);
173 }
174 qDebug() << mSchematics.count() << "schematics successfully loaded!";
175 }
176
177 // Load all boards
178 if (!create) {
179 QString fp = "boards/boards.lp";
180 SExpression brdRoot =
181 SExpression::parse(mDirectory->read(fp), mDirectory->getAbsPath(fp));
182 foreach (const SExpression& node, brdRoot.getChildren("board")) {
183 FilePath fp =
184 FilePath::fromRelative(getPath(), node.getChild("@0").getValue());
185 std::unique_ptr<TransactionalDirectory> dir(new TransactionalDirectory(
186 *mDirectory, fp.getParentDir().toRelative(getPath())));
187 Board* board = new Board(*this, std::move(dir), fileFormat);
188 addBoard(*board);
189 }
190 qDebug() << mBoards.count() << "boards successfully loaded!";
191 }
192
193 // at this point, the whole circuit with all schematics and boards is
194 // successfully loaded, so the ERC list now contains all the correct ERC
195 // messages. So we can now restore the ignore state of each ERC message from
196 // the file.
197 mErcMsgList->restoreIgnoreState(); // can throw
198
199 if (create) save(); // write all files to file system
200 } catch (...) {
201 // free the allocated memory in the reverse order of their allocation...
202 foreach (Board* board, mBoards) {
203 try {
204 removeBoard(*board, true);
205 } catch (...) {
206 }
207 }
208 foreach (Schematic* schematic, mSchematics) {
209 try {
210 removeSchematic(*schematic, true);
211 } catch (...) {
212 }
213 }
214 throw; // ...and rethrow the exception
215 }
216
217 // project successfully opened! :-)
218 qDebug() << "project successfully loaded!";
219 }
220
~Project()221 Project::~Project() noexcept {
222 // free the allocated memory in the reverse order of their allocation
223
224 // delete all boards and schematics (and catch all thrown exceptions)
225 foreach (Board* board, mBoards) {
226 try {
227 removeBoard(*board, true);
228 } catch (...) {
229 }
230 }
231 qDeleteAll(mRemovedBoards);
232 mRemovedBoards.clear();
233 foreach (Schematic* schematic, mSchematics) {
234 try {
235 removeSchematic(*schematic, true);
236 } catch (...) {
237 }
238 }
239 qDeleteAll(mRemovedSchematics);
240 mRemovedSchematics.clear();
241
242 qDebug() << "closed project:" << getFilepath().toNative();
243 }
244
245 /*******************************************************************************
246 * Schematic Methods
247 ******************************************************************************/
248
getSchematicIndex(const Schematic & schematic) const249 int Project::getSchematicIndex(const Schematic& schematic) const noexcept {
250 return mSchematics.indexOf(const_cast<Schematic*>(&schematic));
251 }
252
getSchematicByUuid(const Uuid & uuid) const253 Schematic* Project::getSchematicByUuid(const Uuid& uuid) const noexcept {
254 foreach (Schematic* schematic, mSchematics) {
255 if (schematic->getUuid() == uuid) return schematic;
256 }
257 return nullptr;
258 }
259
getSchematicByName(const QString & name) const260 Schematic* Project::getSchematicByName(const QString& name) const noexcept {
261 foreach (Schematic* schematic, mSchematics) {
262 if (schematic->getName() == name) return schematic;
263 }
264 return nullptr;
265 }
266
createSchematic(const ElementName & name)267 Schematic* Project::createSchematic(const ElementName& name) {
268 QString dirname = FilePath::cleanFileName(
269 *name, FilePath::ReplaceSpaces | FilePath::ToLowerCase);
270 if (dirname.isEmpty()) {
271 throw RuntimeError(__FILE__, __LINE__,
272 tr("Invalid schematic name: \"%1\"").arg(*name));
273 }
274 std::unique_ptr<TransactionalDirectory> dir(
275 new TransactionalDirectory(*mDirectory, "schematics/" % dirname));
276 if (dir->fileExists("schematic.lp")) {
277 throw RuntimeError(__FILE__, __LINE__,
278 tr("The schematic exists already: \"%1\"")
279 .arg(dir->getAbsPath().toNative()));
280 }
281 return Schematic::create(*this, std::move(dir), name);
282 }
283
addSchematic(Schematic & schematic,int newIndex)284 void Project::addSchematic(Schematic& schematic, int newIndex) {
285 if ((mSchematics.contains(&schematic)) || (&schematic.getProject() != this)) {
286 throw LogicError(__FILE__, __LINE__);
287 }
288 if (getSchematicByUuid(schematic.getUuid())) {
289 throw RuntimeError(
290 __FILE__, __LINE__,
291 QString("There is already a schematic with the UUID \"%1\"!")
292 .arg(schematic.getUuid().toStr()));
293 }
294 if (getSchematicByName(*schematic.getName())) {
295 throw RuntimeError(__FILE__, __LINE__,
296 tr("There is already a schematic with the name \"%1\"!")
297 .arg(*schematic.getName()));
298 }
299
300 if ((newIndex < 0) || (newIndex > mSchematics.count())) {
301 newIndex = mSchematics.count();
302 }
303
304 schematic.addToProject(); // can throw
305 mSchematics.insert(newIndex, &schematic);
306
307 if (mRemovedSchematics.contains(&schematic)) {
308 mRemovedSchematics.removeOne(&schematic);
309 }
310
311 emit schematicAdded(newIndex);
312 emit attributesChanged();
313 }
314
removeSchematic(Schematic & schematic,bool deleteSchematic)315 void Project::removeSchematic(Schematic& schematic, bool deleteSchematic) {
316 if ((!mSchematics.contains(&schematic)) ||
317 (mRemovedSchematics.contains(&schematic))) {
318 throw LogicError(__FILE__, __LINE__);
319 }
320 if ((!deleteSchematic) && (!schematic.isEmpty())) {
321 throw RuntimeError(__FILE__, __LINE__,
322 tr("There are still elements in the schematic \"%1\"!")
323 .arg(*schematic.getName()));
324 }
325
326 int index = getSchematicIndex(schematic);
327 Q_ASSERT(index >= 0);
328
329 schematic.removeFromProject(); // can throw
330 mSchematics.removeAt(index);
331
332 emit schematicRemoved(index);
333 emit attributesChanged();
334
335 if (deleteSchematic) {
336 delete &schematic;
337 } else {
338 mRemovedSchematics.append(&schematic);
339 }
340 }
341
exportSchematicsAsPdf(const FilePath & filepath)342 void Project::exportSchematicsAsPdf(const FilePath& filepath) {
343 // Create output directory first because QPrinter silently fails if it doesn't
344 // exist.
345 FileUtils::makePath(filepath.getParentDir()); // can throw
346
347 QPrinter printer(QPrinter::HighResolution);
348 printer.setPaperSize(QPrinter::A4);
349 printer.setOrientation(QPrinter::Landscape);
350 printer.setOutputFormat(QPrinter::PdfFormat);
351 printer.setCreator(QString("LibrePCB %1").arg(qApp->applicationVersion()));
352 printer.setOutputFileName(filepath.toStr());
353
354 QList<int> pages;
355 for (int i = 0; i < mSchematics.count(); i++) pages.append(i);
356
357 printSchematicPages(printer, pages);
358 }
359
printSchematicPages(QPrinter & printer,QList<int> & pages)360 void Project::printSchematicPages(QPrinter& printer, QList<int>& pages) {
361 if (pages.isEmpty())
362 throw RuntimeError(__FILE__, __LINE__, tr("No schematic pages selected."));
363
364 QPainter painter(&printer);
365
366 for (int i = 0; i < pages.count(); i++) {
367 Schematic* schematic = getSchematicByIndex(pages[i]);
368 if (!schematic) {
369 throw RuntimeError(
370 __FILE__, __LINE__,
371 tr("No schematic page with the index %1 found.").arg(pages[i]));
372 }
373 schematic->clearSelection();
374 schematic->renderToQPainter(painter);
375
376 if (i != pages.count() - 1) {
377 if (!printer.newPage()) {
378 throw RuntimeError(__FILE__, __LINE__,
379 tr("Unknown error while printing."));
380 }
381 }
382 }
383 }
384
385 /*******************************************************************************
386 * Board Methods
387 ******************************************************************************/
388
getBoardIndex(const Board & board) const389 int Project::getBoardIndex(const Board& board) const noexcept {
390 return mBoards.indexOf(const_cast<Board*>(&board));
391 }
392
getBoardByUuid(const Uuid & uuid) const393 Board* Project::getBoardByUuid(const Uuid& uuid) const noexcept {
394 foreach (Board* board, mBoards) {
395 if (board->getUuid() == uuid) return board;
396 }
397 return nullptr;
398 }
399
getBoardByName(const QString & name) const400 Board* Project::getBoardByName(const QString& name) const noexcept {
401 foreach (Board* board, mBoards) {
402 if (board->getName() == name) return board;
403 }
404 return nullptr;
405 }
406
createBoard(const ElementName & name)407 Board* Project::createBoard(const ElementName& name) {
408 QString dirname = FilePath::cleanFileName(
409 *name, FilePath::ReplaceSpaces | FilePath::ToLowerCase);
410 if (dirname.isEmpty()) {
411 throw RuntimeError(__FILE__, __LINE__,
412 tr("Invalid board name: \"%1\"").arg(*name));
413 }
414 std::unique_ptr<TransactionalDirectory> dir(
415 new TransactionalDirectory(*mDirectory, "boards/" % dirname));
416 if (dir->fileExists("board.lp")) {
417 throw RuntimeError(__FILE__, __LINE__,
418 tr("The board exists already: \"%1\"")
419 .arg(dir->getAbsPath().toNative()));
420 }
421 return Board::create(*this, std::move(dir), name);
422 }
423
createBoard(const Board & other,const ElementName & name)424 Board* Project::createBoard(const Board& other, const ElementName& name) {
425 QString dirname = FilePath::cleanFileName(
426 *name, FilePath::ReplaceSpaces | FilePath::ToLowerCase);
427 if (dirname.isEmpty()) {
428 throw RuntimeError(__FILE__, __LINE__,
429 tr("Invalid board name: \"%1\"").arg(*name));
430 }
431 std::unique_ptr<TransactionalDirectory> dir(
432 new TransactionalDirectory(*mDirectory, "boards/" % dirname));
433 if (dir->fileExists("board.lp")) {
434 throw RuntimeError(__FILE__, __LINE__,
435 tr("The board exists already: \"%1\"")
436 .arg(dir->getAbsPath().toNative()));
437 }
438 return new Board(other, std::move(dir), name);
439 }
440
addBoard(Board & board,int newIndex)441 void Project::addBoard(Board& board, int newIndex) {
442 if ((mBoards.contains(&board)) || (&board.getProject() != this)) {
443 throw LogicError(__FILE__, __LINE__);
444 }
445 if (getBoardByUuid(board.getUuid())) {
446 throw RuntimeError(__FILE__, __LINE__,
447 QString("There is already a board with the UUID \"%1\"!")
448 .arg(board.getUuid().toStr()));
449 }
450 if (getBoardByName(*board.getName())) {
451 throw RuntimeError(__FILE__, __LINE__,
452 tr("There is already a board with the name \"%1\"!")
453 .arg(*board.getName()));
454 }
455
456 if ((newIndex < 0) || (newIndex > mBoards.count())) {
457 newIndex = mBoards.count();
458 }
459
460 board.addToProject(); // can throw
461 mBoards.insert(newIndex, &board);
462
463 if (mRemovedBoards.contains(&board)) {
464 mRemovedBoards.removeOne(&board);
465 }
466
467 emit boardAdded(newIndex);
468 emit attributesChanged();
469 }
470
removeBoard(Board & board,bool deleteBoard)471 void Project::removeBoard(Board& board, bool deleteBoard) {
472 if ((!mBoards.contains(&board)) || (mRemovedBoards.contains(&board))) {
473 throw LogicError(__FILE__, __LINE__);
474 }
475
476 int index = getBoardIndex(board);
477 Q_ASSERT(index >= 0);
478
479 board.removeFromProject(); // can throw
480 mBoards.removeAt(index);
481
482 emit boardRemoved(index);
483 emit attributesChanged();
484
485 if (deleteBoard) {
486 delete &board;
487 } else {
488 mRemovedBoards.append(&board);
489 }
490 }
491
492 /*******************************************************************************
493 * General Methods
494 ******************************************************************************/
495
save()496 void Project::save() {
497 qDebug() << "Save project files to transactional file system...";
498
499 // Save version file
500 mDirectory->write(
501 ".librepcb-project",
502 VersionFile(qApp->getFileFormatVersion()).toByteArray()); // can throw
503
504 // Save *.lpp project file
505 mDirectory->write(mFilename, "LIBREPCB-PROJECT"); // can throw
506
507 // Save project/metadata.lp
508 mDirectory->write(
509 "project/metadata.lp",
510 mProjectMetadata->serializeToDomElement("librepcb_project_metadata")
511 .toByteArray()); // can throw
512
513 // Save settings
514 mProjectSettings->save(); // can throw
515
516 // Save library
517 mProjectLibrary->save(); // can throw
518
519 // Save circuit
520 mCircuit->save(); // can throw
521
522 // Save ERC messages list
523 mErcMsgList->save(); // can throw
524
525 // Save schematics/schematics.lp
526 SExpression schRoot = SExpression::createList("librepcb_schematics");
527 foreach (Schematic* schematic, mSchematics) {
528 schRoot.appendChild("schematic",
529 schematic->getFilePath().toRelative(getPath()), true);
530 }
531 mDirectory->write("schematics/schematics.lp",
532 schRoot.toByteArray()); // can throw
533
534 // Save boards/boards.lp
535 SExpression brdRoot = SExpression::createList("librepcb_boards");
536 foreach (Board* board, mBoards) {
537 brdRoot.appendChild("board", board->getFilePath().toRelative(getPath()),
538 true);
539 }
540 mDirectory->write("boards/boards.lp", brdRoot.toByteArray()); // can throw
541
542 // Save all removed schematics (*.lp files)
543 foreach (Schematic* schematic, mRemovedSchematics) {
544 schematic->save(); // can throw
545 }
546 // Save all added schematics (*.lp files)
547 foreach (Schematic* schematic, mSchematics) {
548 schematic->save(); // can throw
549 }
550
551 // Save all removed boards (*.lp files)
552 foreach (Board* board, mRemovedBoards) {
553 board->save(); // can throw
554 }
555 // Save all added boards (*.lp files)
556 foreach (Board* board, mBoards) {
557 board->save(); // can throw
558 }
559
560 // update the "last modified datetime" attribute of the project
561 mProjectMetadata->updateLastModified();
562 }
563
564 /*******************************************************************************
565 * Inherited from AttributeProvider
566 ******************************************************************************/
567
getUserDefinedAttributeValue(const QString & key) const568 QString Project::getUserDefinedAttributeValue(const QString& key) const
569 noexcept {
570 if (const auto& attr = mProjectMetadata->getAttributes().find(key)) {
571 return attr->getValueTr(true);
572 } else {
573 return QString();
574 }
575 }
576
getBuiltInAttributeValue(const QString & key) const577 QString Project::getBuiltInAttributeValue(const QString& key) const noexcept {
578 if (key == QLatin1String("PROJECT")) {
579 return *mProjectMetadata->getName();
580 } else if (key == QLatin1String("PROJECT_DIRPATH")) {
581 return getPath().toNative();
582 } else if (key == QLatin1String("PROJECT_BASENAME")) {
583 return getFilepath().getBasename();
584 } else if (key == QLatin1String("PROJECT_FILENAME")) {
585 return getFilepath().getFilename();
586 } else if (key == QLatin1String("PROJECT_FILEPATH")) {
587 return getFilepath().toNative();
588 } else if (key == QLatin1String("CREATED_DATE")) {
589 return mProjectMetadata->getCreated().date().toString(Qt::ISODate);
590 } else if (key == QLatin1String("CREATED_TIME")) {
591 return mProjectMetadata->getCreated().time().toString(Qt::ISODate);
592 } else if (key == QLatin1String("MODIFIED_DATE")) {
593 return mProjectMetadata->getLastModified().date().toString(Qt::ISODate);
594 } else if (key == QLatin1String("MODIFIED_TIME")) {
595 return mProjectMetadata->getLastModified().time().toString(Qt::ISODate);
596 } else if (key == QLatin1String("AUTHOR")) {
597 return mProjectMetadata->getAuthor();
598 } else if (key == QLatin1String("VERSION")) {
599 return mProjectMetadata->getVersion();
600 } else if (key == QLatin1String("PAGES")) {
601 return QString::number(mSchematics.count());
602 } else if (key == QLatin1String("PAGE_X_OF_Y")) {
603 return "Page {{PAGE}} of {{PAGES}}"; // do not translate this, must be the
604 // same for every user!
605 } else {
606 return QString();
607 }
608 }
609
610 /*******************************************************************************
611 * Static Methods
612 ******************************************************************************/
613
isFilePathInsideProjectDirectory(const FilePath & fp)614 bool Project::isFilePathInsideProjectDirectory(const FilePath& fp) noexcept {
615 FilePath parent = fp.getParentDir();
616 if (isProjectDirectory(parent)) {
617 return true;
618 } else if (parent.isValid() && !parent.isRoot()) {
619 return isFilePathInsideProjectDirectory(parent);
620 } else {
621 return false;
622 }
623 }
624
isProjectFile(const FilePath & file)625 bool Project::isProjectFile(const FilePath& file) noexcept {
626 return file.getSuffix() == "lpp" && file.isExistingFile() &&
627 isProjectDirectory(file.getParentDir());
628 }
629
isProjectDirectory(const FilePath & dir)630 bool Project::isProjectDirectory(const FilePath& dir) noexcept {
631 return dir.getPathTo(".librepcb-project").isExistingFile();
632 }
633
getProjectFileFormatVersion(const FilePath & dir)634 Version Project::getProjectFileFormatVersion(const FilePath& dir) {
635 QByteArray content = FileUtils::readFile(dir.getPathTo(".librepcb-project"));
636 VersionFile file = VersionFile::fromByteArray(content);
637 return file.getVersion();
638 }
639
640 /*******************************************************************************
641 * End of File
642 ******************************************************************************/
643
644 } // namespace project
645 } // namespace librepcb
646