Loading files
/*
* LibrePCB - Professional EDA for everyone!
* Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
* https://librepcb.org/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*******************************************************************************
* Includes
******************************************************************************/
#include "controlpanel.h"
#include "../../dialogs/directorylockhandlerdialog.h"
#include "../../dialogs/filedialog.h"
#include "../../editorcommandset.h"
#include "../../library/libraryeditor.h"
#include "../../project/newprojectwizard/newprojectwizard.h"
#include "../../project/projecteditor.h"
#include "../../utils/menubuilder.h"
#include "../../utils/standardeditorcommandhandler.h"
#include "../../workspace/desktopservices.h"
#include "../../workspace/librarymanager/librarymanager.h"
#include "../desktopintegration.h"
#include "../desktopservices.h"
#include "../initializeworkspacewizard/initializeworkspacewizard.h"
#include "../projectlibraryupdater/projectlibraryupdater.h"
#include "../workspacesettingsdialog.h"
#include "favoriteprojectsmodel.h"
#include "markdownconverter.h"
#include "projecttreemodel.h"
#include "recentprojectsmodel.h"
#include "ui_controlpanel.h"
#include <librepcb/core/application.h>
#include <librepcb/core/fileio/fileutils.h>
#include <librepcb/core/fileio/transactionalfilesystem.h>
#include <librepcb/core/project/project.h>
#include <librepcb/core/project/projectloader.h>
#include <librepcb/core/utils/scopeguard.h>
#include <librepcb/core/workspace/workspace.h>
#include <librepcb/core/workspace/workspacelibrarydb.h>
#include <librepcb_build_env.h>
#include <QtCore>
#include <QtWidgets>
#if BUILD_QTQUICK_TEST
#include <QQmlContext>
#include <QQmlEngine>
#include <QQuickView>
#endif
/*******************************************************************************
* Namespace
******************************************************************************/
namespace librepcb {
namespace editor {
/*******************************************************************************
* Constructors / Destructor
******************************************************************************/
ControlPanel::ControlPanel(Workspace& workspace, bool fileFormatIsOutdated)
: QMainWindow(nullptr),
mWorkspace(workspace),
mUi(new Ui::ControlPanel),
mStandardCommandHandler(
new StandardEditorCommandHandler(mWorkspace.getSettings(), this)),
mLibraryManager(new LibraryManager(mWorkspace, this)) {
mUi->setupUi(this);
setWindowTitle(
tr("Control Panel - LibrePCB %1").arg(Application::getVersion()));
// initialize status bar
mUi->statusBar->setFields(StatusBar::ProgressBar);
mUi->statusBar->setPermanentMessage(
tr("Workspace: %1").arg(mWorkspace.getPath().toNative()));
mUi->statusBar->setProgressBarTextFormat(tr("Scanning libraries (%p%)"));
connect(&mWorkspace.getLibraryDb(), &WorkspaceLibraryDb::scanProgressUpdate,
mUi->statusBar, &StatusBar::setProgressBarPercent,
Qt::QueuedConnection);
mUi->statusBar->setProgressBarPercent(
mWorkspace.getLibraryDb().getScanProgressPercent());
// Setup actions and menus.
createActions();
createMenus();
// Show warning if the workspace has already been opened with a higher
// file format version.
mUi->msgWarnForNewerAppVersions->init(
mWorkspace,
QString("WORKSPACE_V%1_OPENED_WITH_NEWER_VERSION")
.arg(Application::getFileFormatVersion().toStr()),
tr("This workspace was already used with a newer version of LibrePCB. "
"All changes in libraries and workspace settings will not be "
"available in newer versions of LibrePCB."),
fileFormatIsOutdated);
// Suggest to install the desktop integration, if available.
mUi->msgInstallDesktopIntegration->init(
mWorkspace, "DESKTOP_INTEGRATION_NOT_INSTALLED",
tr("This application executable does not seem to be integrated into your "
"desktop environment. If desired, <a href=\"%1\">install it now</a> "
"to allow opening LibrePCB projects through the file manager.")
.arg("install"),
false);
updateDesktopIntegrationMessage();
connect(mUi->msgInstallDesktopIntegration, &MessageWidget::linkActivated,
this, [this]() {
DesktopIntegration::execDialog(DesktopIntegration::Mode::Install,
this);
updateDesktopIntegrationMessage();
});
// Setup warning about missing libraries, and update visibility each time the
// workspace library was scanned.
mUi->msgWarnForNoLibraries->init(
mWorkspace,
QString("WORKSPACE_V%1_HAS_NO_LIBRARIES")
.arg(Application::getFileFormatVersion().toStr()),
tr("This workspace does not contain any libraries, which are essential "
"to create and modify projects. You should <a href=\"%1\">open the "
"library manager</a> to add some libraries.")
.arg("library-manager"),
false);
connect(mUi->msgWarnForNoLibraries, &MessageWidget::linkActivated, this,
&ControlPanel::openLibraryManager);
connect(
&mWorkspace.getLibraryDb(), &WorkspaceLibraryDb::scanLibraryListUpdated,
this, [this]() {
bool showWarning = false;
try {
showWarning = mWorkspace.getLibraryDb().getAll<Library>().isEmpty();
} catch (const Exception& e) {
qCritical() << "Failed to get workspace library list:" << e.getMsg();
}
mUi->msgWarnForNoLibraries->setActive(showWarning);
});
// connect some actions which are created with the Qt Designer
connect(mUi->openProjectButton, &QPushButton::clicked,
mActionOpenProject.data(), &QAction::trigger);
connect(mUi->newProjectButton, &QPushButton::clicked,
mActionNewProject.data(), &QAction::trigger);
connect(mUi->openLibraryManagerButton, &QPushButton::clicked,
mActionLibraryManager.data(), &QAction::trigger);
connect(mLibraryManager.data(), &LibraryManager::openLibraryEditorTriggered,
this, &ControlPanel::openLibraryEditor);
connect(mUi->textBrowser, &QTextBrowser::anchorClicked, this,
[this](QUrl url) {
const QStringList searchPaths = mUi->textBrowser->searchPaths();
if (url.isRelative() && (!searchPaths.isEmpty())) {
url.setPath(searchPaths.first() % "/" % url.path());
}
DesktopServices ds(mWorkspace.getSettings(), this);
ds.openWebUrl(url);
});
connect(mUi->btnDonate, &QPushButton::clicked, this, [this]() {
DesktopServices ds(mWorkspace.getSettings(), this);
ds.openWebUrl(QUrl("https://librepcb.org/donate/"));
});
// load project models
mRecentProjectsModel.reset(new RecentProjectsModel(mWorkspace));
mFavoriteProjectsModel.reset(new FavoriteProjectsModel(mWorkspace));
mProjectTreeModel.reset(new ProjectTreeModel(mWorkspace));
// build projects file tree
mUi->projectTreeView->setModel(mProjectTreeModel.data());
mUi->projectTreeView->setRootIndex(
mProjectTreeModel->index(mWorkspace.getProjectsPath().toStr()));
for (int i = 1; i < mUi->projectTreeView->header()->count(); ++i) {
mUi->projectTreeView->hideColumn(i);
}
// load recent and favorite project models
mUi->recentProjectsListView->setModel(mRecentProjectsModel.data());
mUi->favoriteProjectsListView->setModel(mFavoriteProjectsModel.data());
loadSettings();
// slightly delay opening projects to make sure the control panel window goes
// to background (schematic editor should be the top most window)
QTimer::singleShot(10, this, &ControlPanel::openProjectsPassedByCommandLine);
// To allow opening files by the MacOS Finder, install event filter.
qApp->installEventFilter(this);
// start scanning the workspace library (asynchronously)
mWorkspace.getLibraryDb().startLibraryRescan();
}
ControlPanel::~ControlPanel() noexcept {
mProjectLibraryUpdater.reset();
closeAllProjects(false);
closeAllLibraryEditors(false);
mLibraryManager.reset();
mUi.reset();
}
void ControlPanel::closeEvent(QCloseEvent* event) {
// close all projects, unsaved projects will ask for saving
if (!closeAllProjects(true)) {
event->ignore();
return; // do NOT close the application, there are still open projects!
}
// close all library editors, unsaved libraries will ask for saving
if (!closeAllLibraryEditors(true)) {
event->ignore();
return; // do NOT close the application, there are still open library
// editors!
}
saveSettings();
QMainWindow::closeEvent(event);
// if the control panel is closed, we will quit the whole application
QApplication::exit();
}
bool ControlPanel::eventFilter(QObject* watched, QEvent* event) {
if (event->type() == QEvent::FileOpen) {
QFileOpenEvent* openEvent = static_cast<QFileOpenEvent*>(event);
qDebug() << "Received request to open file:" << openEvent->file();
openProjectPassedByOs(openEvent->file());
return true;
}
return QMainWindow::eventFilter(watched, event);
}
void ControlPanel::showControlPanel() noexcept {
show();
raise();
activateWindow();
}
void ControlPanel::openProjectLibraryUpdater(const FilePath& project) noexcept {
mProjectLibraryUpdater.reset(
new ProjectLibraryUpdater(mWorkspace, project, *this));
mProjectLibraryUpdater->show();
}
/*******************************************************************************
* General private methods
******************************************************************************/
void ControlPanel::createActions() noexcept {
const EditorCommandSet& cmd = EditorCommandSet::instance();
mActionLibraryManager.reset(cmd.libraryManager.createAction(
this, this, &ControlPanel::openLibraryManager,
EditorCommand::ActionFlag::ApplicationShortcut));
mActionWorkspaceSettings.reset(cmd.workspaceSettings.createAction(
this, this,
[this]() {
WorkspaceSettingsDialog dialog(mWorkspace, this);
connect(&dialog,
&WorkspaceSettingsDialog::desktopIntegrationStatusChanged, this,
&ControlPanel::updateDesktopIntegrationMessage);
dialog.exec();
},
EditorCommand::ActionFlag::ApplicationShortcut));
mActionRescanLibraries.reset(cmd.workspaceLibrariesRescan.createAction(
this, &mWorkspace.getLibraryDb(),
&WorkspaceLibraryDb::startLibraryRescan));
mActionSwitchWorkspace.reset(cmd.workspaceSwitch.createAction(
this, this, &ControlPanel::switchWorkspace));
mActionNewProject.reset(
cmd.projectNew.createAction(this, this, [this]() { newProject(); }));
mActionOpenProject.reset(
cmd.projectOpen.createAction(this, this, [this]() { openProject(); }));
mActionCloseAllProjects.reset(cmd.projectCloseAll.createAction(
this, this, [this]() { closeAllProjects(true); },
EditorCommand::ActionFlag::ApplicationShortcut));
mActionAddExampleProjects.reset(cmd.addExampleProjects.createAction(
this, this, &ControlPanel::addExampleProjects));
mActionImportEagleProject.reset(cmd.importEagleProject.createAction(
this, this, [this]() { newProject(true); }));
mActionAboutLibrePcb.reset(cmd.aboutLibrePcb.createAction(
this, mStandardCommandHandler.data(),
&StandardEditorCommandHandler::aboutLibrePcb));
mActionAboutQt.reset(
cmd.aboutQt.createAction(this, qApp, &QApplication::aboutQt));
mActionOnlineDocumentation.reset(cmd.documentationOnline.createAction(
this, mStandardCommandHandler.data(),
&StandardEditorCommandHandler::onlineDocumentation));
mActionKeyboardShortcutsReference.reset(
cmd.keyboardShortcutsReference.createAction(
this, mStandardCommandHandler.data(),
&StandardEditorCommandHandler::shortcutsReference));
mActionWebsite.reset(
cmd.website.createAction(this, mStandardCommandHandler.data(),
&StandardEditorCommandHandler::website));
mActionQtQuickTest.reset(new QAction("QtQuick Test Window", this));
connect(mActionQtQuickTest.data(), &QAction::triggered, this, [this]() {
#if BUILD_QTQUICK_TEST
Q_UNUSED(this);
QQuickView* view = new QQuickView;
view->setTitle("QtQuick Test");
view->setResizeMode(QQuickView::SizeRootObjectToView);
view->resize(350, 150);
view->engine()->rootContext()->setContextProperty("view", view);
view->setSource(Application::getResourcesDir()
.getPathTo("qml/testwindow.qml")
.toQUrl());
view->show();
#else
QMessageBox::warning(this, "QtQuick Test",
"LibrePCB was built without QtQuick support!");
#endif
});
mActionQuit.reset(cmd.applicationQuit.createAction(
this, qApp, &QApplication::closeAllWindows,
EditorCommand::ActionFlag::QueuedConnection));
// Widget actions.
mUi->projectTreeView->addAction(cmd.remove.createAction(
mUi->projectTreeView, this,
[this]() {
const QModelIndexList indexes =
mUi->projectTreeView->selectionModel()->selectedRows();
if (indexes.count() == 1) {
removeProjectsTreeItem(
FilePath(mProjectTreeModel->filePath(indexes.first())));
}
},
// Queued for funq testing.
EditorCommand::ActionFlag::QueuedConnection |
EditorCommand::ActionFlag::WidgetShortcut));
}
void ControlPanel::createMenus() noexcept {
MenuBuilder mb(mUi->menuBar);
// File.
mb.newMenu(&MenuBuilder::createFileMenu);
mb.addAction(mActionNewProject);
mb.addAction(mActionOpenProject);
mb.addAction(mActionCloseAllProjects);
mb.addSeparator();
{
MenuBuilder smb(mb.addSubMenu(&MenuBuilder::createImportMenu));
smb.addAction(mActionAddExampleProjects);
smb.addAction(mActionImportEagleProject);
}
mb.addSeparator();
mb.addAction(mActionSwitchWorkspace);
mb.addSeparator();
mb.addAction(mActionQuit);
// Extras.
mb.newMenu(&MenuBuilder::createExtrasMenu);
mb.addAction(mActionRescanLibraries);
mb.addAction(mActionLibraryManager);
mb.addSeparator();
mb.addAction(mActionWorkspaceSettings);
// Help.
mb.newMenu(&MenuBuilder::createHelpMenu);
mb.addAction(mActionOnlineDocumentation);
mb.addAction(mActionKeyboardShortcutsReference);
mb.addAction(mActionWebsite);
mb.addSeparator();
mb.addAction(mActionQtQuickTest);
mb.addSeparator();
mb.addAction(mActionAboutLibrePcb);
mb.addAction(mActionAboutQt);
}
void ControlPanel::saveSettings() {
QSettings clientSettings;
clientSettings.beginGroup("controlpanel");
// main window
clientSettings.setValue("window_geometry", saveGeometry());
clientSettings.setValue("window_state", saveState());
clientSettings.setValue("splitter_h_state", mUi->splitter_h->saveState());
clientSettings.setValue("splitter_v_state", mUi->splitter_v->saveState());
// projects treeview (expanded items)
if (ProjectTreeModel* model =
dynamic_cast<ProjectTreeModel*>(mUi->projectTreeView->model())) {
QStringList list;
foreach (QModelIndex index, model->getPersistentIndexList()) {
if (mUi->projectTreeView->isExpanded(index)) {
list.append(
FilePath(model->filePath(index)).toRelative(mWorkspace.getPath()));
}
}
clientSettings.setValue("expanded_projecttreeview_items",
QVariant::fromValue(list));
}
clientSettings.endGroup();
}
void ControlPanel::loadSettings() {
QSettings clientSettings;
clientSettings.beginGroup("controlpanel");
// main window
restoreGeometry(clientSettings.value("window_geometry").toByteArray());
restoreState(clientSettings.value("window_state").toByteArray());
mUi->splitter_h->restoreState(
clientSettings.value("splitter_h_state").toByteArray());
mUi->splitter_v->restoreState(
clientSettings.value("splitter_v_state").toByteArray());
// projects treeview (expanded items)
if (ProjectTreeModel* model =
dynamic_cast<ProjectTreeModel*>(mUi->projectTreeView->model())) {
QStringList list =
clientSettings.value("expanded_projecttreeview_items").toStringList();
foreach (QString item, list) {
FilePath filepath = FilePath::fromRelative(mWorkspace.getPath(), item);
QModelIndex index = model->index(filepath.toStr());
mUi->projectTreeView->setExpanded(index, true);
}
}
clientSettings.endGroup();
}
void ControlPanel::updateDesktopIntegrationMessage() noexcept {
mUi->msgInstallDesktopIntegration->setActive(
DesktopIntegration::isSupported() &&
(DesktopIntegration::getStatus() !=
DesktopIntegration::Status::InstalledThis));
}
void ControlPanel::openLibraryManager() noexcept {
mLibraryManager->show();
mLibraryManager->raise();
mLibraryManager->activateWindow();
mLibraryManager->updateOnlineLibraryList();
}
void ControlPanel::addExampleProjects() noexcept {
const QString msg =
tr("This downloads some example projects from the internet and copies "
"them into the workspace to help you evaluating LibrePCB with real "
"projects.") %
"\n\n" %
tr("Once you don't need them anymore, just delete the examples directory "
"to get rid of them.");
const int ret = QMessageBox::information(
this, EditorCommandSet::instance().addExampleProjects.getText(), msg,
QMessageBox::Ok | QMessageBox::Cancel);
if (ret == QMessageBox::Ok) {
InitializeWorkspaceWizardContext ctx(this);
ctx.setWorkspacePath(mWorkspace.getPath());
ctx.installExampleProjects();
}
}
void ControlPanel::switchWorkspace() noexcept {
InitializeWorkspaceWizard wizard(true, this);
try {
wizard.setWorkspacePath(mWorkspace.getPath());
} catch (const Exception& e) {
qWarning() << "Failed to prepare workspace switching:" << e.getMsg();
}
if ((wizard.exec() == QDialog::Accepted) &&
(wizard.getWorkspacePath().isValid())) {
Workspace::setMostRecentlyUsedWorkspacePath(wizard.getWorkspacePath());
QMessageBox::information(this, tr("Workspace changed"),
tr("The chosen workspace will be used after "
"restarting the application."));
}
}
void ControlPanel::showProjectReadmeInBrowser(
const FilePath& projectFilePath) noexcept {
if (projectFilePath.isValid()) {
FilePath readmeFilePath = projectFilePath.getPathTo("README.md");
mUi->textBrowser->setSearchPaths(QStringList(projectFilePath.toStr()));
mUi->textBrowser->setHtml(
MarkdownConverter::convertMarkdownToHtml(readmeFilePath));
} else {
mUi->textBrowser->clear();
}
}
void ControlPanel::removeProjectsTreeItem(const FilePath& fp) noexcept {
if ((!fp.isValid()) || (!fp.isLocatedInDir(mWorkspace.getProjectsPath()))) {
return;
}
const QMessageBox::StandardButton btn = QMessageBox::question(
this, tr("Remove"),
tr("Are you really sure to remove following file or directory?\n\n"
"%1\n\nWarning: This cannot be undone!")
.arg(fp.toNative()));
if (btn == QMessageBox::Yes) {
try {
if (fp.isExistingDir()) {
FileUtils::removeDirRecursively(fp);
} else {
FileUtils::removeFile(fp);
}
} catch (const Exception& e) {
QMessageBox::critical(this, tr("Error"), e.getMsg());
}
// Something was removed -> update lists of recent and favorite projects.
mRecentProjectsModel->updateVisibleProjects();
mFavoriteProjectsModel->updateVisibleProjects();
}
}
/*******************************************************************************
* Project Management
******************************************************************************/
ProjectEditor* ControlPanel::newProject(bool eagleImport,
FilePath parentDir) noexcept {
const NewProjectWizard::Mode mode = eagleImport
? NewProjectWizard::Mode::EagleImport
: NewProjectWizard::Mode::NewProject;
NewProjectWizard wizard(mWorkspace, mode, this);
if (parentDir.isValid()) {
wizard.setLocationOverride(parentDir);
}
if (wizard.exec() == QWizard::Accepted) {
try {
std::unique_ptr<Project> project = wizard.createProject(); // can throw
const FilePath fp = project->getFilepath();
project.reset(); // Release lock.
return openProject(fp);
} catch (const Exception& e) {
QMessageBox::critical(this, tr("Could not create project"), e.getMsg());
}
}
return nullptr;
}
ProjectEditor* ControlPanel::openProject(FilePath filepath) noexcept {
if (!filepath.isValid()) {
QSettings settings; // client settings
QString lastOpenedFile = settings
.value("controlpanel/last_open_project",
mWorkspace.getPath().toStr())
.toString();
filepath = FilePath(FileDialog::getOpenFileName(
this, tr("Open Project"), lastOpenedFile,
tr("LibrePCB project files (%1)").arg("*.lpp *.lppz")));
if (!filepath.isValid()) return nullptr;
settings.setValue("controlpanel/last_open_project", filepath.toNative());
}
try {
ProjectEditor* editor = getOpenProject(filepath);
if (!editor) {
// Opening the project can take some time, use wait cursor to provide
// immediate UI feedback.
setCursor(Qt::WaitCursor);
auto cursorScopeGuard = scopeGuard([this]() { unsetCursor(); });
std::shared_ptr<TransactionalFileSystem> fs;
QString projectFileName = filepath.getFilename();
if (filepath.getSuffix() == "lppz") {
fs = TransactionalFileSystem::openRO(
FilePath::getRandomTempPath(),
&TransactionalFileSystem::RestoreMode::no);
fs->removeDirRecursively(); // 1) Get a clean initial state.
fs->loadFromZip(filepath); // 2) Load files from ZIP.
foreach (const QString& fn, fs->getFiles()) {
if (fn.endsWith(".lpp")) {
projectFileName = fn;
}
}
} else {
fs = TransactionalFileSystem::openRW(
filepath.getParentDir(), &askForRestoringBackup,
DirectoryLockHandlerDialog::createDirectoryLockCallback());
}
ProjectLoader loader;
std::unique_ptr<Project> project =
loader.open(std::unique_ptr<TransactionalDirectory>(
new TransactionalDirectory(fs)),
projectFileName); // can throw
editor = new ProjectEditor(mWorkspace, *project.release(),
loader.getUpgradeMessages());
connect(editor, &ProjectEditor::projectEditorClosed, this,
&ControlPanel::projectEditorClosed);
connect(editor, &ProjectEditor::showControlPanelClicked, this,
&ControlPanel::showControlPanel);
connect(editor, &ProjectEditor::openProjectLibraryUpdaterClicked, this,
&ControlPanel::openProjectLibraryUpdater);
mOpenProjectEditors.insert(filepath.toUnique().toStr(), editor);
// Delay updating the last opened project to avoid an issue when
// double-clicking: https://github.com/LibrePCB/LibrePCB/issues/293
QTimer::singleShot(500, this, [this, filepath]() {
mRecentProjectsModel->setLastRecentProject(filepath);
});
}
editor->showAllRequiredEditors();
return editor;
} catch (UserCanceled& e) {
// do nothing
return nullptr;
} catch (Exception& e) {
QMessageBox::critical(this, tr("Could not open project"), e.getMsg());
return nullptr;
}
}
bool ControlPanel::closeProject(ProjectEditor& editor,
bool askForSave) noexcept {
Q_ASSERT(mOpenProjectEditors.contains(
editor.getProject().getFilepath().toUnique().toStr()));
bool success = editor.closeAndDestroy(
askForSave,
this); // this will implicitly call the slot "projectEditorClosed()"!
if (success) {
delete &editor; // delete immediately to avoid locked projects when closing
// the app
}
return success;
}
bool ControlPanel::closeProject(const FilePath& filepath,
bool askForSave) noexcept {
ProjectEditor* editor = getOpenProject(filepath);
if (editor)
return closeProject(*editor, askForSave);
else
return false;
}
bool ControlPanel::closeAllProjects(bool askForSave) noexcept {
bool success = true;
foreach (ProjectEditor* editor, mOpenProjectEditors) {
if (!closeProject(*editor, askForSave)) success = false;
}
return success;
}
ProjectEditor* ControlPanel::getOpenProject(
const FilePath& filepath) const noexcept {
if (mOpenProjectEditors.contains(filepath.toUnique().toStr()))
return mOpenProjectEditors.value(filepath.toUnique().toStr());
else
return nullptr;
}
bool ControlPanel::askForRestoringBackup(const FilePath& dir) {
Q_UNUSED(dir);
QMessageBox::StandardButton btn = QMessageBox::question(
0, tr("Restore autosave backup?"),
tr("It seems that the application crashed the last time you opened this "
"project. Do you want to restore the last autosave backup?"),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
QMessageBox::Cancel);
switch (btn) {
case QMessageBox::Yes:
return true;
case QMessageBox::No:
return false;
default:
throw UserCanceled(__FILE__, __LINE__);
}
}
/*******************************************************************************
* Library Management
******************************************************************************/
void ControlPanel::openLibraryEditor(const FilePath& libDir) noexcept {
LibraryEditor* editor = mOpenLibraryEditors.value(libDir);
if (!editor) {
try {
bool remote = libDir.isLocatedInDir(mWorkspace.getRemoteLibrariesPath());
editor = new LibraryEditor(mWorkspace, libDir, remote);
connect(editor, &LibraryEditor::destroyed, this,
&ControlPanel::libraryEditorDestroyed);
mOpenLibraryEditors.insert(libDir, editor);
} catch (const UserCanceled& e) {
// User requested to abort -> do nothing.
} catch (const Exception& e) {
QMessageBox::critical(this, tr("Error"), e.getMsg());
}
}
if (editor) {
editor->show();
editor->raise();
editor->activateWindow();
}
}
void ControlPanel::libraryEditorDestroyed() noexcept {
// Note: Actually we should dynamic_cast the QObject* to LibraryEditor*, but
// as this slot is called in the destructor of QObject (base class of
// LibraryEditor), the dynamic_cast does no longer work at this point, so a
// static_cast is used instead ;)
LibraryEditor* editor = static_cast<LibraryEditor*>(QObject::sender());
Q_ASSERT(editor);
FilePath library = mOpenLibraryEditors.key(editor);
Q_ASSERT(library.isValid());
mOpenLibraryEditors.remove(library);
}
bool ControlPanel::closeAllLibraryEditors(bool askForSave) noexcept {
bool success = true;
foreach (LibraryEditor* editor, mOpenLibraryEditors) {
if (editor->closeAndDestroy(askForSave)) {
delete editor; // this calls the slot "libraryEditorDestroyed()"
} else {
success = false;
}
}
return success;
}
/*******************************************************************************
* Private Slots
******************************************************************************/
void ControlPanel::openProjectsPassedByCommandLine() noexcept {
// Parse command line arguments and open all project files.
// Note: Do not print a warning if the first argument is not a valid project,
// since it might or might not be the application file path.
const QStringList args = qApp->arguments();
for (int i = 0; i < args.count(); ++i) {
openProjectPassedByOs(args.at(i), i == 0); // Silent on first item.
}
}
void ControlPanel::openProjectPassedByOs(const QString& file,
bool silent) noexcept {
FilePath filepath(file);
if ((filepath.isExistingFile()) &&
((filepath.getSuffix() == "lpp") || (filepath.getSuffix() == "lppz"))) {
openProject(filepath);
} else if (!silent) {
qWarning() << "Ignore invalid request to open project:" << file;
}
}
void ControlPanel::projectEditorClosed() noexcept {
ProjectEditor* editor = dynamic_cast<ProjectEditor*>(QObject::sender());
Q_ASSERT(editor);
if (!editor) return;
const QString fp = mOpenProjectEditors.key(editor);
Q_ASSERT(mOpenProjectEditors.contains(fp));
mOpenProjectEditors.remove(fp);
Project* project = &editor->getProject();
delete project;
}
/*******************************************************************************
* Actions
******************************************************************************/
void ControlPanel::on_projectTreeView_clicked(const QModelIndex& index) {
FilePath fp(mProjectTreeModel->filePath(index));
if ((fp.getSuffix() == "lpp") || (fp.getFilename() == "README.md")) {
showProjectReadmeInBrowser(fp.getParentDir());
} else {
showProjectReadmeInBrowser(fp);
}
}
void ControlPanel::on_projectTreeView_doubleClicked(const QModelIndex& index) {
FilePath fp(mProjectTreeModel->filePath(index));
if (fp.isExistingDir()) {
mUi->projectTreeView->setExpanded(index,
!mUi->projectTreeView->isExpanded(index));
} else if ((fp.getSuffix() == "lpp") || (fp.getSuffix() == "lppz")) {
openProject(fp);
} else {
DesktopServices ds(mWorkspace.getSettings(), this);
ds.openLocalPath(fp);
}
}
void ControlPanel::on_projectTreeView_customContextMenuRequested(
const QPoint& pos) {
// get clicked tree item filepath
QModelIndex index = mUi->projectTreeView->indexAt(pos);
FilePath fp = index.isValid() ? FilePath(mProjectTreeModel->filePath(index))
: mWorkspace.getProjectsPath();
bool isProjectFile = Project::isProjectFile(fp);
bool isProjectDir = Project::isProjectDirectory(fp);
bool isInProjectDir = Project::isFilePathInsideProjectDirectory(fp);
// build context menu with actions
QMenu menu;
MenuBuilder mb(&menu);
const EditorCommandSet& cmd = EditorCommandSet::instance();
if (isProjectFile) {
if (!getOpenProject(fp)) {
mb.addAction(cmd.itemOpen.createAction(
&menu, this, [this, fp]() { openProject(fp); },
EditorCommand::ActionFlag::NoShortcuts),
MenuBuilder::Flag::DefaultAction);
} else {
mb.addAction(cmd.projectClose.createAction(
&menu, this, [this, fp]() { closeProject(fp, true); },
EditorCommand::ActionFlag::NoShortcuts));
}
mb.addSeparator();
if (mFavoriteProjectsModel->isFavoriteProject(fp)) {
mb.addAction(cmd.favoriteRemove.createAction(
&menu, this,
[this, fp]() { mFavoriteProjectsModel->removeFavoriteProject(fp); },
EditorCommand::ActionFlag::NoShortcuts));
} else {
mb.addAction(cmd.favoriteAdd.createAction(
&menu, this,
[this, fp]() { mFavoriteProjectsModel->addFavoriteProject(fp); },
EditorCommand::ActionFlag::NoShortcuts));
}
mb.addAction(cmd.projectLibraryUpdate.createAction(
&menu, this, [this, fp]() { openProjectLibraryUpdater(fp); },
EditorCommand::ActionFlag::NoShortcuts));
} else {
mb.addAction(cmd.itemOpen.createAction(
&menu, this,
[this, fp]() {
DesktopServices ds(mWorkspace.getSettings(), this);
ds.openLocalPath(fp);
},
EditorCommand::ActionFlag::NoShortcuts),
MenuBuilder::Flag::DefaultAction);
}
mb.addSeparator();
if (fp.isExistingDir() && (!isProjectDir) && (!isInProjectDir)) {
mb.addAction(cmd.projectNew.createAction(
&menu, this, [this, fp]() { newProject(false, fp); },
EditorCommand::ActionFlag::NoShortcuts));
mb.addAction(cmd.folderNew.createAction(
&menu, this,
[this, fp]() {
QDir(fp.toStr())
.mkdir(
QInputDialog::getText(this, tr("New Folder"), tr("Name:")));
},
EditorCommand::ActionFlag::NoShortcuts));
}
if (fp != mWorkspace.getProjectsPath()) {
mb.addSeparator();
mb.addAction(cmd.remove.createAction(
&menu, this, [this, fp]() { removeProjectsTreeItem(fp); },
EditorCommand::ActionFlag::NoShortcuts));
}
// show context menu and execute the clicked action
menu.exec(QCursor::pos());
}
void ControlPanel::on_recentProjectsListView_entered(const QModelIndex& index) {
FilePath filepath(index.data(Qt::UserRole).toString());
showProjectReadmeInBrowser(filepath.getParentDir());
}
void ControlPanel::on_favoriteProjectsListView_entered(
const QModelIndex& index) {
FilePath filepath(index.data(Qt::UserRole).toString());
showProjectReadmeInBrowser(filepath.getParentDir());
}
void ControlPanel::on_recentProjectsListView_clicked(const QModelIndex& index) {
FilePath filepath(index.data(Qt::UserRole).toString());
openProject(filepath);
}
void ControlPanel::on_favoriteProjectsListView_clicked(
const QModelIndex& index) {
FilePath filepath(index.data(Qt::UserRole).toString());
openProject(filepath);
}
void ControlPanel::on_recentProjectsListView_customContextMenuRequested(
const QPoint& pos) {
QModelIndex index = mUi->recentProjectsListView->indexAt(pos);
if (!index.isValid()) return;
bool isFavorite = mFavoriteProjectsModel->isFavoriteProject(
FilePath(index.data(Qt::UserRole).toString()));
FilePath fp = FilePath(index.data(Qt::UserRole).toString());
if (!fp.isValid()) return;
QMenu menu;
MenuBuilder mb(&menu);
const EditorCommandSet& cmd = EditorCommandSet::instance();
mb.addAction(cmd.itemOpen.createAction(
&menu, this, [this, fp]() { openProject(fp); },
EditorCommand::ActionFlag::NoShortcuts),
MenuBuilder::Flag::DefaultAction);
mb.addSeparator();
if (isFavorite) {
mb.addAction(cmd.favoriteRemove.createAction(
&menu, this,
[this, fp]() { mFavoriteProjectsModel->removeFavoriteProject(fp); },
EditorCommand::ActionFlag::NoShortcuts));
} else {
mb.addAction(cmd.favoriteAdd.createAction(
&menu, this,
[this, fp]() { mFavoriteProjectsModel->addFavoriteProject(fp); },
EditorCommand::ActionFlag::NoShortcuts));
}
mb.addAction(cmd.projectLibraryUpdate.createAction(
&menu, this, [this, fp]() { openProjectLibraryUpdater(fp); }));
menu.exec(QCursor::pos());
}
void ControlPanel::on_favoriteProjectsListView_customContextMenuRequested(
const QPoint& pos) {
QModelIndex index = mUi->favoriteProjectsListView->indexAt(pos);
if (!index.isValid()) return;
FilePath fp = FilePath(index.data(Qt::UserRole).toString());
if (!fp.isValid()) return;
QMenu menu;
MenuBuilder mb(&menu);
const EditorCommandSet& cmd = EditorCommandSet::instance();
mb.addAction(cmd.itemOpen.createAction(
&menu, this, [this, fp]() { openProject(fp); },
EditorCommand::ActionFlag::NoShortcuts),
MenuBuilder::Flag::DefaultAction);
mb.addSeparator();
mb.addAction(cmd.favoriteRemove.createAction(
&menu, this,
[this, fp]() { mFavoriteProjectsModel->removeFavoriteProject(fp); },
EditorCommand::ActionFlag::NoShortcuts));
mb.addAction(cmd.projectLibraryUpdate.createAction(
&menu, this, [this, fp]() { openProjectLibraryUpdater(fp); }));
menu.exec(QCursor::pos());
}
/*******************************************************************************
* End of File
******************************************************************************/
} // namespace editor
} // namespace librepcb
c74bd0883This file is pinned to the selected release. Download the original to open it in your design tool.