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 "controlpanel.h"
24 
25 #include "../firstrunwizard/firstrunwizard.h"
26 #include "../markdown/markdownconverter.h"
27 #include "projectlibraryupdater/projectlibraryupdater.h"
28 #include "ui_controlpanel.h"
29 
30 #include <librepcb/common/application.h>
31 #include <librepcb/common/dialogs/aboutdialog.h>
32 #include <librepcb/common/dialogs/directorylockhandlerdialog.h>
33 #include <librepcb/common/dialogs/filedialog.h>
34 #include <librepcb/common/fileio/fileutils.h>
35 #include <librepcb/common/fileio/transactionalfilesystem.h>
36 #include <librepcb/library/library.h>
37 #include <librepcb/libraryeditor/libraryeditor.h>
38 #include <librepcb/librarymanager/librarymanager.h>
39 #include <librepcb/project/project.h>
40 #include <librepcb/projecteditor/newprojectwizard/newprojectwizard.h>
41 #include <librepcb/projecteditor/projecteditor.h>
42 #include <librepcb/workspace/favoriteprojectsmodel.h>
43 #include <librepcb/workspace/library/workspacelibrarydb.h>
44 #include <librepcb/workspace/projecttreemodel.h>
45 #include <librepcb/workspace/recentprojectsmodel.h>
46 #include <librepcb/workspace/settings/workspacesettings.h>
47 #include <librepcb/workspace/settings/workspacesettingsdialog.h>
48 #include <librepcb/workspace/workspace.h>
49 
50 #include <QtCore>
51 #include <QtWidgets>
52 
53 /*******************************************************************************
54  *  Namespace
55  ******************************************************************************/
56 namespace librepcb {
57 namespace application {
58 
59 using namespace project;
60 using namespace project::editor;
61 using namespace library::manager;
62 using namespace workspace;
63 
64 /*******************************************************************************
65  *  Constructors / Destructor
66  ******************************************************************************/
67 
ControlPanel(Workspace & workspace)68 ControlPanel::ControlPanel(Workspace& workspace)
69   : QMainWindow(nullptr),
70     mWorkspace(workspace),
71     mUi(new Ui::ControlPanel),
72     mLibraryManager(new LibraryManager(mWorkspace, this)) {
73   mUi->setupUi(this);
74 
75   setWindowTitle(
76       tr("Control Panel - LibrePCB %1").arg(qApp->applicationVersion()));
77 
78   // show workspace path in status bar
79   QString wsPath = mWorkspace.getPath().toNative();
80   QLabel* statusBarLabel = new QLabel(tr("Workspace: %1").arg(wsPath));
81   mUi->statusBar->addWidget(statusBarLabel, 1);
82 
83   // initialize status bar
84   mUi->statusBar->setFields(StatusBar::ProgressBar);
85   mUi->statusBar->setProgressBarTextFormat(tr("Scanning libraries (%p%)"));
86   connect(&mWorkspace.getLibraryDb(), &WorkspaceLibraryDb::scanProgressUpdate,
87           mUi->statusBar, &StatusBar::setProgressBarPercent,
88           Qt::QueuedConnection);
89 
90   // decive if we have to show the warning about a newer workspace file format
91   // version
92   Version actualVersion = qApp->getFileFormatVersion();
93   tl::optional<Version> highestVersion =
94       Workspace::getHighestFileFormatVersionOfWorkspace(workspace.getPath());
95   mUi->lblWarnForNewerAppVersions->setVisible(highestVersion > actualVersion);
96 
97   // hide warning about missing libraries, but update visibility each time the
98   // workspace library was scanned
99   mUi->lblWarnForNoLibraries->setVisible(false);
100   connect(mUi->lblWarnForNoLibraries, &QLabel::linkActivated, this,
101           &ControlPanel::on_actionOpen_Library_Manager_triggered);
102   connect(&mWorkspace.getLibraryDb(),
103           &WorkspaceLibraryDb::scanLibraryListUpdated, this,
104           &ControlPanel::updateNoLibrariesWarningVisibility);
105 
106   // connect some actions which are created with the Qt Designer
107   connect(mUi->actionQuit, &QAction::triggered, this, &ControlPanel::close);
108   connect(mUi->actionOpenWebsite, &QAction::triggered,
109           []() { QDesktopServices::openUrl(QUrl("https://librepcb.org")); });
110   connect(mUi->actionOnlineDocumentation, &QAction::triggered, []() {
111     QDesktopServices::openUrl(QUrl("https://docs.librepcb.org"));
112   });
113   connect(mUi->actionAbout_Qt, &QAction::triggered, qApp,
114           &QApplication::aboutQt);
115   connect(mUi->actionAbout, &QAction::triggered, qApp, &Application::about);
116   connect(mLibraryManager.data(), &LibraryManager::openLibraryEditorTriggered,
117           this, &ControlPanel::openLibraryEditor);
118 
119   // build projects file tree
120   mUi->projectTreeView->setModel(&mWorkspace.getProjectTreeModel());
121   mUi->projectTreeView->setRootIndex(mWorkspace.getProjectTreeModel().index(
122       mWorkspace.getProjectsPath().toStr()));
123   for (int i = 1; i < mUi->projectTreeView->header()->count(); ++i) {
124     mUi->projectTreeView->hideColumn(i);
125   }
126 
127   // load recent and favorite project models
128   mUi->recentProjectsListView->setModel(&mWorkspace.getRecentProjectsModel());
129   mUi->favoriteProjectsListView->setModel(
130       &mWorkspace.getFavoriteProjectsModel());
131 
132   loadSettings();
133 
134   // slightly delay opening projects to make sure the control panel window goes
135   // to background (schematic editor should be the top most window)
136   QTimer::singleShot(10, this, &ControlPanel::openProjectsPassedByCommandLine);
137 
138   // start scanning the workspace library (asynchronously)
139   mWorkspace.getLibraryDb().startLibraryRescan();
140 }
141 
~ControlPanel()142 ControlPanel::~ControlPanel() {
143   mProjectLibraryUpdater.reset();
144   closeAllProjects(false);
145   closeAllLibraryEditors(false);
146   mLibraryManager.reset();
147   mUi.reset();
148 }
149 
closeEvent(QCloseEvent * event)150 void ControlPanel::closeEvent(QCloseEvent* event) {
151   // close all projects, unsaved projects will ask for saving
152   if (!closeAllProjects(true)) {
153     event->ignore();
154     return;  // do NOT close the application, there are still open projects!
155   }
156 
157   // close all library editors, unsaved libraries will ask for saving
158   if (!closeAllLibraryEditors(true)) {
159     event->ignore();
160     return;  // do NOT close the application, there are still open library
161              // editors!
162   }
163 
164   saveSettings();
165 
166   QMainWindow::closeEvent(event);
167 
168   // if the control panel is closed, we will quit the whole application
169   QApplication::quit();
170 }
171 
showControlPanel()172 void ControlPanel::showControlPanel() noexcept {
173   show();
174   raise();
175   activateWindow();
176 }
177 
openProjectLibraryUpdater(const FilePath & project)178 void ControlPanel::openProjectLibraryUpdater(const FilePath& project) noexcept {
179   mProjectLibraryUpdater.reset(
180       new ProjectLibraryUpdater(mWorkspace, project, *this));
181   mProjectLibraryUpdater->show();
182 }
183 
184 /*******************************************************************************
185  *  General private methods
186  ******************************************************************************/
187 
saveSettings()188 void ControlPanel::saveSettings() {
189   QSettings clientSettings;
190   clientSettings.beginGroup("controlpanel");
191 
192   // main window
193   clientSettings.setValue("window_geometry", saveGeometry());
194   clientSettings.setValue("window_state", saveState());
195   clientSettings.setValue("splitter_h_state", mUi->splitter_h->saveState());
196   clientSettings.setValue("splitter_v_state", mUi->splitter_v->saveState());
197 
198   // projects treeview (expanded items)
199   if (ProjectTreeModel* model =
200           dynamic_cast<ProjectTreeModel*>(mUi->projectTreeView->model())) {
201     QStringList list;
202     foreach (QModelIndex index, model->getPersistentIndexList()) {
203       if (mUi->projectTreeView->isExpanded(index)) {
204         list.append(
205             FilePath(model->filePath(index)).toRelative(mWorkspace.getPath()));
206       }
207     }
208     clientSettings.setValue("expanded_projecttreeview_items",
209                             QVariant::fromValue(list));
210   }
211 
212   clientSettings.endGroup();
213 }
214 
loadSettings()215 void ControlPanel::loadSettings() {
216   QSettings clientSettings;
217   clientSettings.beginGroup("controlpanel");
218 
219   // main window
220   restoreGeometry(clientSettings.value("window_geometry").toByteArray());
221   restoreState(clientSettings.value("window_state").toByteArray());
222   mUi->splitter_h->restoreState(
223       clientSettings.value("splitter_h_state").toByteArray());
224   mUi->splitter_v->restoreState(
225       clientSettings.value("splitter_v_state").toByteArray());
226 
227   // projects treeview (expanded items)
228   if (ProjectTreeModel* model =
229           dynamic_cast<ProjectTreeModel*>(mUi->projectTreeView->model())) {
230     QStringList list =
231         clientSettings.value("expanded_projecttreeview_items").toStringList();
232     foreach (QString item, list) {
233       FilePath filepath = FilePath::fromRelative(mWorkspace.getPath(), item);
234       QModelIndex index = model->index(filepath.toStr());
235       mUi->projectTreeView->setExpanded(index, true);
236     }
237   }
238 
239   clientSettings.endGroup();
240 }
241 
updateNoLibrariesWarningVisibility()242 void ControlPanel::updateNoLibrariesWarningVisibility() noexcept {
243   bool showWarning = false;
244   try {
245     showWarning = mWorkspace.getLibraryDb().getLibraries().isEmpty();
246   } catch (const Exception& e) {
247     qCritical() << "Could not get library list:" << e.getMsg();
248   }
249   mUi->lblWarnForNoLibraries->setVisible(showWarning);
250 }
251 
showProjectReadmeInBrowser(const FilePath & projectFilePath)252 void ControlPanel::showProjectReadmeInBrowser(
253     const FilePath& projectFilePath) noexcept {
254   if (projectFilePath.isValid()) {
255     FilePath readmeFilePath = projectFilePath.getPathTo("README.md");
256     mUi->textBrowser->setSearchPaths(QStringList(projectFilePath.toStr()));
257     mUi->textBrowser->setHtml(
258         MarkdownConverter::convertMarkdownToHtml(readmeFilePath));
259   } else {
260     mUi->textBrowser->clear();
261   }
262 }
263 
264 /*******************************************************************************
265  *  Project Management
266  ******************************************************************************/
267 
newProject(const FilePath & parentDir)268 ProjectEditor* ControlPanel::newProject(const FilePath& parentDir) noexcept {
269   NewProjectWizard wizard(mWorkspace, this);
270   wizard.setLocation(parentDir);
271   if (wizard.exec() == QWizard::Accepted) {
272     try {
273       QScopedPointer<Project> project(wizard.createProject());  // can throw
274       return openProject(*project.take());
275     } catch (Exception& e) {
276       QMessageBox::critical(this, tr("Could not create project"), e.getMsg());
277     }
278   }
279   return nullptr;
280 }
281 
openProject(Project & project)282 ProjectEditor* ControlPanel::openProject(Project& project) noexcept {
283   try {
284     ProjectEditor* editor = getOpenProject(project.getFilepath());
285     if (!editor) {
286       editor = new ProjectEditor(mWorkspace, project);
287       connect(editor, &ProjectEditor::projectEditorClosed, this,
288               &ControlPanel::projectEditorClosed);
289       connect(editor, &ProjectEditor::showControlPanelClicked, this,
290               &ControlPanel::showControlPanel);
291       connect(editor, &ProjectEditor::openProjectLibraryUpdaterClicked, this,
292               &ControlPanel::openProjectLibraryUpdater);
293       mOpenProjectEditors.insert(project.getFilepath().toUnique().toStr(),
294                                  editor);
295       mWorkspace.setLastRecentlyUsedProject(project.getFilepath());
296     }
297     editor->showAllRequiredEditors();
298     return editor;
299   } catch (UserCanceled& e) {
300     // do nothing
301     return nullptr;
302   } catch (Exception& e) {
303     QMessageBox::critical(this, tr("Could not open project"), e.getMsg());
304     return nullptr;
305   }
306 }
307 
openProject(const FilePath & filepath)308 ProjectEditor* ControlPanel::openProject(const FilePath& filepath) noexcept {
309   try {
310     ProjectEditor* editor = getOpenProject(filepath);
311     if (!editor) {
312       std::shared_ptr<TransactionalFileSystem> fs =
313           TransactionalFileSystem::openRW(
314               filepath.getParentDir(), &askForRestoringBackup,
315               DirectoryLockHandlerDialog::createDirectoryLockCallback());
316       Project* project = new Project(std::unique_ptr<TransactionalDirectory>(
317                                          new TransactionalDirectory(fs)),
318                                      filepath.getFilename());
319       editor = new ProjectEditor(mWorkspace, *project);
320       connect(editor, &ProjectEditor::projectEditorClosed, this,
321               &ControlPanel::projectEditorClosed);
322       connect(editor, &ProjectEditor::showControlPanelClicked, this,
323               &ControlPanel::showControlPanel);
324       connect(editor, &ProjectEditor::openProjectLibraryUpdaterClicked, this,
325               &ControlPanel::openProjectLibraryUpdater);
326       mOpenProjectEditors.insert(filepath.toUnique().toStr(), editor);
327       mWorkspace.setLastRecentlyUsedProject(filepath);
328     }
329     editor->showAllRequiredEditors();
330     return editor;
331   } catch (UserCanceled& e) {
332     // do nothing
333     return nullptr;
334   } catch (Exception& e) {
335     QMessageBox::critical(this, tr("Could not open project"), e.getMsg());
336     return nullptr;
337   }
338 }
339 
closeProject(ProjectEditor & editor,bool askForSave)340 bool ControlPanel::closeProject(ProjectEditor& editor,
341                                 bool askForSave) noexcept {
342   Q_ASSERT(mOpenProjectEditors.contains(
343       editor.getProject().getFilepath().toUnique().toStr()));
344   bool success = editor.closeAndDestroy(
345       askForSave,
346       this);  // this will implicitly call the slot "projectEditorClosed()"!
347   if (success) {
348     delete &editor;  // delete immediately to avoid locked projects when closing
349                      // the app
350   }
351   return success;
352 }
353 
closeProject(const FilePath & filepath,bool askForSave)354 bool ControlPanel::closeProject(const FilePath& filepath,
355                                 bool askForSave) noexcept {
356   ProjectEditor* editor = getOpenProject(filepath);
357   if (editor)
358     return closeProject(*editor, askForSave);
359   else
360     return false;
361 }
362 
closeAllProjects(bool askForSave)363 bool ControlPanel::closeAllProjects(bool askForSave) noexcept {
364   bool success = true;
365   foreach (ProjectEditor* editor, mOpenProjectEditors) {
366     if (!closeProject(*editor, askForSave)) success = false;
367   }
368   return success;
369 }
370 
getOpenProject(const FilePath & filepath) const371 ProjectEditor* ControlPanel::getOpenProject(const FilePath& filepath) const
372     noexcept {
373   if (mOpenProjectEditors.contains(filepath.toUnique().toStr()))
374     return mOpenProjectEditors.value(filepath.toUnique().toStr());
375   else
376     return nullptr;
377 }
378 
askForRestoringBackup(const FilePath & dir)379 bool ControlPanel::askForRestoringBackup(const FilePath& dir) {
380   Q_UNUSED(dir);
381   QMessageBox::StandardButton btn = QMessageBox::question(
382       0, tr("Restore autosave backup?"),
383       tr("It seems that the application crashed the last time you opened this "
384          "project. Do you want to restore the last autosave backup?"),
385       QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
386       QMessageBox::Cancel);
387   switch (btn) {
388     case QMessageBox::Yes:
389       return true;
390     case QMessageBox::No:
391       return false;
392     default:
393       throw UserCanceled(__FILE__, __LINE__);
394   }
395 }
396 
397 /*******************************************************************************
398  *  Library Management
399  ******************************************************************************/
400 
openLibraryEditor(const FilePath & libDir)401 void ControlPanel::openLibraryEditor(const FilePath& libDir) noexcept {
402   using library::Library;
403   using library::editor::LibraryEditor;
404   LibraryEditor* editor = mOpenLibraryEditors.value(libDir);
405   if (!editor) {
406     try {
407       bool remote = libDir.isLocatedInDir(mWorkspace.getRemoteLibrariesPath());
408       editor = new LibraryEditor(mWorkspace, libDir, remote);
409       connect(editor, &LibraryEditor::destroyed, this,
410               &ControlPanel::libraryEditorDestroyed);
411       mOpenLibraryEditors.insert(libDir, editor);
412     } catch (const UserCanceled& e) {
413       // User requested to abort -> do nothing.
414     } catch (const Exception& e) {
415       QMessageBox::critical(this, tr("Error"), e.getMsg());
416     }
417   }
418   if (editor) {
419     editor->show();
420     editor->raise();
421     editor->activateWindow();
422   }
423 }
424 
libraryEditorDestroyed()425 void ControlPanel::libraryEditorDestroyed() noexcept {
426   using library::editor::LibraryEditor;
427   // Note: Actually we should dynamic_cast the QObject* to LibraryEditor*, but
428   // as this slot is called in the destructor of QObject (base class of
429   // LibraryEditor), the dynamic_cast does no longer work at this point, so a
430   // static_cast is used instead ;)
431   LibraryEditor* editor = static_cast<LibraryEditor*>(QObject::sender());
432   Q_ASSERT(editor);
433   FilePath library = mOpenLibraryEditors.key(editor);
434   Q_ASSERT(library.isValid());
435   mOpenLibraryEditors.remove(library);
436 }
437 
closeAllLibraryEditors(bool askForSave)438 bool ControlPanel::closeAllLibraryEditors(bool askForSave) noexcept {
439   using library::editor::LibraryEditor;
440   bool success = true;
441   foreach (LibraryEditor* editor, mOpenLibraryEditors) {
442     if (editor->closeAndDestroy(askForSave)) {
443       delete editor;  // this calls the slot "libraryEditorDestroyed()"
444     } else {
445       success = false;
446     }
447   }
448   return success;
449 }
450 
451 /*******************************************************************************
452  *  Private Slots
453  ******************************************************************************/
454 
openProjectsPassedByCommandLine()455 void ControlPanel::openProjectsPassedByCommandLine() noexcept {
456   // parse command line arguments and open all project files
457   foreach (const QString& arg, qApp->arguments()) {
458     FilePath filepath(arg);
459     if ((filepath.isExistingFile()) && (filepath.getSuffix() == "lpp")) {
460       openProject(filepath);
461     }
462   }
463 }
464 
projectEditorClosed()465 void ControlPanel::projectEditorClosed() noexcept {
466   ProjectEditor* editor = dynamic_cast<ProjectEditor*>(QObject::sender());
467   Q_ASSERT(editor);
468   if (!editor) return;
469 
470   Project* project = &editor->getProject();
471   Q_ASSERT(
472       mOpenProjectEditors.contains(project->getFilepath().toUnique().toStr()));
473   mOpenProjectEditors.remove(project->getFilepath().toUnique().toStr());
474   delete project;
475 }
476 
477 /*******************************************************************************
478  *  Actions
479  ******************************************************************************/
480 
on_actionNew_Project_triggered()481 void ControlPanel::on_actionNew_Project_triggered() {
482   newProject(mWorkspace.getProjectsPath());
483 }
484 
on_actionOpen_Project_triggered()485 void ControlPanel::on_actionOpen_Project_triggered() {
486   QSettings settings;  // client settings
487   QString lastOpenedFile =
488       settings
489           .value("controlpanel/last_open_project", mWorkspace.getPath().toStr())
490           .toString();
491 
492   FilePath filepath(FileDialog::getOpenFileName(
493       this, tr("Open Project"), lastOpenedFile,
494       tr("LibrePCB project files (%1)").arg("*.lpp")));
495 
496   if (!filepath.isValid()) return;
497 
498   settings.setValue("controlpanel/last_open_project", filepath.toNative());
499 
500   openProject(filepath);
501 }
502 
on_actionOpen_Library_Manager_triggered()503 void ControlPanel::on_actionOpen_Library_Manager_triggered() {
504   mLibraryManager->show();
505   mLibraryManager->raise();
506   mLibraryManager->activateWindow();
507   mLibraryManager->updateRepositoryLibraryList();
508 }
509 
on_actionClose_all_open_projects_triggered()510 void ControlPanel::on_actionClose_all_open_projects_triggered() {
511   closeAllProjects(true);
512 }
513 
on_actionSwitch_Workspace_triggered()514 void ControlPanel::on_actionSwitch_Workspace_triggered() {
515   FirstRunWizard wizard;
516   wizard.skipWelcomePage();  // Welcome page not needed here
517   if (wizard.exec() == QDialog::Accepted) {
518     FilePath wsPath = wizard.getWorkspaceFilePath();
519     if (wizard.getCreateNewWorkspace()) {
520       try {
521         // create new workspace
522         Workspace::createNewWorkspace(wsPath);  // can throw
523       } catch (const Exception& e) {
524         QMessageBox::critical(this, tr("Error"), e.getMsg());
525         return;
526       }
527     }
528     Workspace::setMostRecentlyUsedWorkspacePath(wsPath);
529     QMessageBox::information(this, tr("Workspace changed"),
530                              tr("The chosen workspace will be used after "
531                                 "restarting the application."));
532   }
533 }
534 
on_actionWorkspace_Settings_triggered()535 void ControlPanel::on_actionWorkspace_Settings_triggered() {
536   WorkspaceSettingsDialog dialog(mWorkspace.getSettings(), this);
537   dialog.exec();
538 }
539 
on_projectTreeView_clicked(const QModelIndex & index)540 void ControlPanel::on_projectTreeView_clicked(const QModelIndex& index) {
541   FilePath fp(mWorkspace.getProjectTreeModel().filePath(index));
542   if ((fp.getSuffix() == "lpp") || (fp.getFilename() == "README.md")) {
543     showProjectReadmeInBrowser(fp.getParentDir());
544   } else {
545     showProjectReadmeInBrowser(fp);
546   }
547 }
548 
on_projectTreeView_doubleClicked(const QModelIndex & index)549 void ControlPanel::on_projectTreeView_doubleClicked(const QModelIndex& index) {
550   FilePath fp(mWorkspace.getProjectTreeModel().filePath(index));
551   if (fp.isExistingDir()) {
552     mUi->projectTreeView->setExpanded(index,
553                                       !mUi->projectTreeView->isExpanded(index));
554   } else if (fp.getSuffix() == "lpp") {
555     openProject(fp);
556   } else {
557     QDesktopServices::openUrl(QUrl::fromLocalFile(fp.toStr()));
558   }
559 }
560 
on_projectTreeView_customContextMenuRequested(const QPoint & pos)561 void ControlPanel::on_projectTreeView_customContextMenuRequested(
562     const QPoint& pos) {
563   // get clicked tree item filepath
564   QModelIndex index = mUi->projectTreeView->indexAt(pos);
565   FilePath fp = index.isValid()
566       ? FilePath(mWorkspace.getProjectTreeModel().filePath(index))
567       : mWorkspace.getProjectsPath();
568   bool isProjectFile = Project::isProjectFile(fp);
569   bool isProjectDir = Project::isProjectDirectory(fp);
570   bool isInProjectDir = Project::isFilePathInsideProjectDirectory(fp);
571 
572   // build context menu with actions
573   QMenu menu;
574   enum Action {
575     OpenProject,
576     CloseProject,
577     AddFavorite,
578     RemoveFavorite,  // on projects
579     UpdateLibrary,  // on projects
580     NewProject,
581     NewFolder,  // on folders
582     Open,
583     Remove
584   };  // on folders+files
585   if (isProjectFile) {
586     if (!getOpenProject(fp)) {
587       menu.addAction(QIcon(":/img/actions/open.png"), tr("Open Project"))
588           ->setData(OpenProject);
589       menu.setDefaultAction(menu.actions().last());
590     } else {
591       menu.addAction(QIcon(":/img/actions/close.png"), tr("Close Project"))
592           ->setData(CloseProject);
593     }
594     menu.addSeparator();
595     if (mWorkspace.isFavoriteProject(fp)) {
596       menu.addAction(QIcon(":/img/actions/bookmark.png"),
597                      tr("Remove from favorites"))
598           ->setData(RemoveFavorite);
599     } else {
600       menu.addAction(QIcon(":/img/actions/bookmark_gray.png"),
601                      tr("Add to favorites"))
602           ->setData(AddFavorite);
603     }
604     menu.addSeparator();
605     menu.addAction(QIcon(":/img/actions/refresh.png"),
606                    tr("Update project library"))
607         ->setData(UpdateLibrary);
608   } else {
609     menu.addAction(QIcon(":/img/actions/open.png"), tr("Open"))->setData(Open);
610     if (fp.isExistingFile()) {
611       menu.setDefaultAction(menu.actions().last());
612     }
613   }
614   menu.addSeparator();
615   if (fp.isExistingDir() && (!isProjectDir) && (!isInProjectDir)) {
616     menu.addAction(QIcon(":/img/places/project_folder.png"), tr("New Project"))
617         ->setData(NewProject);
618     menu.addAction(QIcon(":/img/actions/new_folder.png"), tr("New Folder"))
619         ->setData(NewFolder);
620   }
621   if (fp != mWorkspace.getProjectsPath()) {
622     menu.addSeparator();
623     menu.addAction(QIcon(":/img/actions/delete.png"), tr("Remove"))
624         ->setData(Remove);
625   }
626 
627   // show context menu and execute the clicked action
628   QAction* action = menu.exec(QCursor::pos());
629   if (!action) return;
630   switch (action->data().toInt()) {
631     case OpenProject:
632       openProject(fp);
633       break;
634     case CloseProject:
635       closeProject(fp, true);
636       break;
637     case AddFavorite:
638       mWorkspace.addFavoriteProject(fp);
639       break;
640     case RemoveFavorite:
641       mWorkspace.removeFavoriteProject(fp);
642       break;
643     case UpdateLibrary:
644       openProjectLibraryUpdater(fp);
645       break;
646     case NewProject:
647       newProject(fp);
648       break;
649     case NewFolder:
650       QDir(fp.toStr())
651           .mkdir(QInputDialog::getText(this, tr("New Folder"), tr("Name:")));
652       break;
653     case Open:
654       QDesktopServices::openUrl(QUrl::fromLocalFile(fp.toStr()));
655       break;
656     case Remove: {
657       QMessageBox::StandardButton btn = QMessageBox::question(
658           this, tr("Remove"),
659           tr("Are you really sure to remove following file or "
660              "directory?\n\n"
661              "%1\n\nWarning: This cannot be undone!")
662               .arg(fp.toNative()));
663       if (btn == QMessageBox::Yes) {
664         try {
665           if (fp.isExistingDir()) {
666             FileUtils::removeDirRecursively(fp);
667           } else {
668             FileUtils::removeFile(fp);
669           }
670         } catch (const Exception& e) {
671           QMessageBox::critical(this, tr("Error"), e.getMsg());
672         }
673         // something was removed -> update lists of recent and favorite projects
674         mWorkspace.getRecentProjectsModel().updateVisibleProjects();
675         mWorkspace.getFavoriteProjectsModel().updateVisibleProjects();
676       }
677       break;
678     }
679     default:
680       qCritical() << "Unknown action triggered";
681       break;
682   }
683 }
684 
on_recentProjectsListView_entered(const QModelIndex & index)685 void ControlPanel::on_recentProjectsListView_entered(const QModelIndex& index) {
686   FilePath filepath(index.data(Qt::UserRole).toString());
687   showProjectReadmeInBrowser(filepath.getParentDir());
688 }
689 
on_favoriteProjectsListView_entered(const QModelIndex & index)690 void ControlPanel::on_favoriteProjectsListView_entered(
691     const QModelIndex& index) {
692   FilePath filepath(index.data(Qt::UserRole).toString());
693   showProjectReadmeInBrowser(filepath.getParentDir());
694 }
695 
on_recentProjectsListView_clicked(const QModelIndex & index)696 void ControlPanel::on_recentProjectsListView_clicked(const QModelIndex& index) {
697   FilePath filepath(index.data(Qt::UserRole).toString());
698   openProject(filepath);
699 }
700 
on_favoriteProjectsListView_clicked(const QModelIndex & index)701 void ControlPanel::on_favoriteProjectsListView_clicked(
702     const QModelIndex& index) {
703   FilePath filepath(index.data(Qt::UserRole).toString());
704   openProject(filepath);
705 }
706 
on_recentProjectsListView_customContextMenuRequested(const QPoint & pos)707 void ControlPanel::on_recentProjectsListView_customContextMenuRequested(
708     const QPoint& pos) {
709   QModelIndex index = mUi->recentProjectsListView->indexAt(pos);
710   if (!index.isValid()) return;
711 
712   bool isFavorite = mWorkspace.isFavoriteProject(
713       FilePath(index.data(Qt::UserRole).toString()));
714 
715   FilePath fp = FilePath(index.data(Qt::UserRole).toString());
716   if (!fp.isValid()) return;
717 
718   QMenu menu;
719   QAction* action;
720   if (isFavorite) {
721     action = menu.addAction(QIcon(":/img/actions/bookmark.png"),
722                             tr("Remove from favorites"));
723   } else {
724     action = menu.addAction(QIcon(":/img/actions/bookmark_gray.png"),
725                             tr("Add to favorites"));
726   }
727   QAction* libraryUpdaterAction = menu.addAction(
728       QIcon(":/img/actions/refresh.png"), tr("Update project library"));
729 
730   QAction* result = menu.exec(QCursor::pos());
731   if (result == action) {
732     if (isFavorite)
733       mWorkspace.removeFavoriteProject(fp);
734     else
735       mWorkspace.addFavoriteProject(fp);
736   } else if (result == libraryUpdaterAction) {
737     openProjectLibraryUpdater(fp);
738   }
739 }
740 
on_favoriteProjectsListView_customContextMenuRequested(const QPoint & pos)741 void ControlPanel::on_favoriteProjectsListView_customContextMenuRequested(
742     const QPoint& pos) {
743   QModelIndex index = mUi->favoriteProjectsListView->indexAt(pos);
744   if (!index.isValid()) return;
745 
746   FilePath fp = FilePath(index.data(Qt::UserRole).toString());
747   if (!fp.isValid()) return;
748 
749   QMenu menu;
750   QAction* removeAction = menu.addAction(QIcon(":/img/actions/cancel.png"),
751                                          tr("Remove from favorites"));
752   QAction* libraryUpdaterAction = menu.addAction(
753       QIcon(":/img/actions/refresh.png"), tr("Update project library"));
754 
755   QAction* result = menu.exec(QCursor::pos());
756   if (result == removeAction) {
757     mWorkspace.removeFavoriteProject(fp);
758   } else if (result == libraryUpdaterAction) {
759     openProjectLibraryUpdater(fp);
760   }
761 }
762 
on_actionRescanLibraries_triggered()763 void ControlPanel::on_actionRescanLibraries_triggered() {
764   mWorkspace.getLibraryDb().startLibraryRescan();
765 }
766 
767 /*******************************************************************************
768  *  End of File
769  ******************************************************************************/
770 
771 }  // namespace application
772 }  // namespace librepcb
773