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