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 "guiapplication.h"
#include "dialogs/directorylockhandlerdialog.h"
#include "dialogs/filedialog.h"
#include "graphics/graphicslayerlist.h"
#include "library/librariesmodel.h"
#include "library/libraryeditor.h"
#include "library/libraryelementcache.h"
#include "mainwindow.h"
#include "notification.h"
#include "notificationsmodel.h"
#include "project/newprojectwizard/newprojectwizard.h"
#include "project/projecteditor.h"
#include "utils/editortoolbox.h"
#include "utils/slinthelpers.h"
#include "utils/slintkeyeventtextbuilder.h"
#include "utils/uihelpers.h"
#include "workspace/desktopintegration.h"
#include "workspace/desktopservices.h"
#include "workspace/initializeworkspacewizard/initializeworkspacewizard.h"
#include "workspace/organizationsdbmodel.h"
#include "workspace/projectlibraryupdater/projectlibraryupdater.h"
#include "workspace/quickaccessmodel.h"
#include "workspace/workspacesettingsdialog.h"
#include <librepcb/core/application.h>
#include <librepcb/core/attribute/attributetype.h>
#include <librepcb/core/attribute/attributeunit.h>
#include <librepcb/core/fileio/transactionalfilesystem.h>
#include <librepcb/core/library/library.h>
#include <librepcb/core/norms.h>
#include <librepcb/core/project/project.h>
#include <librepcb/core/project/projectloader.h>
#include <librepcb/core/utils/mathparser.h>
#include <librepcb/core/utils/scopeguard.h>
#include <librepcb/core/workspace/workspace.h>
#include <librepcb/core/workspace/workspacelibrarydb.h>
#include <QtCore>
#include <algorithm>
/*******************************************************************************
* Namespace
******************************************************************************/
namespace librepcb {
namespace editor {
/*******************************************************************************
* Constructors / Destructor
******************************************************************************/
GuiApplication::GuiApplication(Workspace& ws, bool fileFormatIsOutdated,
const UiTheme* const& theme,
QObject* parent) noexcept
: QObject(parent),
mWorkspace(ws),
mTheme(theme),
mLibrariesContainStandardComponents(false),
mPreviewLayers(GraphicsLayerList::previewLayers(&ws.getSettings())),
mLibraryElementCache(new LibraryElementCache(ws.getLibraryDb())),
mNotifications(new NotificationsModel(ws)),
mQuickAccessModel(new QuickAccessModel(ws)),
mLocalLibraries(new LibrariesModel(ws, LibrariesModel::Mode::LocalLibs)),
mRemoteLibraries(new LibrariesModel(ws, LibrariesModel::Mode::RemoteLibs)),
mLibrariesFilter(new SlintKeyEventTextBuilder()),
mProjects(new UiObjectList<ProjectEditor, ui::ProjectData>()),
mLibraries(new UiObjectList<LibraryEditor, ui::LibraryData>()) {
QSettings cs;
// Check if this is the first run with this application version. This can
// be used to perform migration steps or to show notifications.
QStringList usedAppVersions;
{
const int size = cs.beginReadArray("used_app_versions");
for (int i = 0; i < size; ++i) {
cs.setArrayIndex(i);
usedAppVersions.append(cs.value("version").toString());
}
cs.endArray();
}
const bool appVersionAlreadyRunBefore =
usedAppVersions.contains(Application::getVersion());
if (!appVersionAlreadyRunBefore) {
usedAppVersions.append(Application::getVersion());
cs.beginWriteArray("used_app_versions", usedAppVersions.count());
for (int i = 0; i < usedAppVersions.count(); ++i) {
cs.setArrayIndex(i);
cs.setValue("version", usedAppVersions.at(i));
}
cs.endArray();
}
// Open windows.
for (const QString& idStr : cs.value("global/windows").toStringList()) {
if (int id = idStr.toInt()) {
createNewWindow(id);
}
}
if (mWindows.isEmpty()) {
createNewWindow();
}
// Setup quick access.
connect(mQuickAccessModel.get(), &QuickAccessModel::openFileTriggered, this,
[this](const FilePath& fp) { openFile(fp, qApp->activeWindow()); });
// Connect notification signals.
const qint64 startupTime = QDateTime::currentMSecsSinceEpoch();
connect(
mNotifications.get(), &NotificationsModel::autoPopUpRequested, this,
[this, startupTime]() {
if (auto w = getCurrentWindow()) {
// It looks ugly if the notifications pop up immediately when the
// whole window is just opened, so we delay it a bit. Not the best
// implementation, probably this could be improved somehow...
const qint64 delay =
std::max(startupTime + 500 - QDateTime::currentMSecsSinceEpoch(),
qint64(0));
QTimer::singleShot(delay, w.get(), &MainWindow::popUpNotifications);
}
});
// Show warning if the runtime resources were not found. Intended to catch
// deployment errors and to avoid bug reports if users didn't install the
// "share" directory.
bool runtimeResourcesValid = false;
const FilePath resourcesDir =
Application::getResourcesDir(&runtimeResourcesValid);
if (!runtimeResourcesValid) {
mNotifications->push(std::make_shared<Notification>(
ui::NotificationType::Critical, "Broken Installation Detected",
QString("The runtime resources from the 'share' folder were not found "
"at '%1', therefore the application will not work correctly. "
"Please make sure to install all files of LibrePCB as "
"explained in the installation instructions.")
.arg(resourcesDir.toNative()),
QString(), QString(), true));
}
// Show warning if the workspace has already been opened with a higher
// file format version.
if (fileFormatIsOutdated) {
mNotifications->push(std::make_shared<Notification>(
ui::NotificationType::Warning, tr("Older Application Version Used"),
tr("This workspace was already used with a newer version of LibrePCB. "
"This is fine, just note that any changes in libraries and "
"workspace "
"settings won't be available in newer versions of LibrePCB."),
QString(),
QString("WORKSPACE_V%1_OPENED_WITH_NEWER_VERSION")
.arg(Application::getFileFormatVersion().toStr()),
true));
}
// Setup warning about missing libraries, and update visibility each time the
// workspace library was scanned.
mNotificationNoLibrariesInstalled.reset(new Notification(
ui::NotificationType::Tip, tr("No Libraries Installed"),
tr("This workspace does not contain any libraries, which are essential "
"to create and modify projects. You should open the libraries panel "
"to add some libraries."),
tr("Open Library Manager"),
QString("WORKSPACE_V%1_HAS_NO_LIBRARIES")
.arg(Application::getFileFormatVersion().toStr()),
true));
connect(mNotificationNoLibrariesInstalled.get(), &Notification::buttonClicked,
this, [this]() {
if (auto win = getCurrentWindow()) {
win->showPanelPage(ui::PanelPage::Libraries);
}
});
connect(&mWorkspace.getLibraryDb(),
&WorkspaceLibraryDb::scanLibraryListUpdated, this,
&GuiApplication::updateNoLibrariesInstalledNotification);
updateNoLibrariesInstalledNotification();
// Suggest to install the desktop integration, if available.
mNotificationDesktopIntegration.reset(new Notification(
ui::NotificationType::Tip, tr("Application is Not Installed"),
tr("This application executable does not seem to be integrated into your "
"desktop environment. If desired, install it now to allow opening "
"LibrePCB projects through the file manager. Click the button for "
"details, or do it from the preferences dialog at any time."),
tr("Install Desktop Integration") % "...",
"DESKTOP_INTEGRATION_NOT_INSTALLED", true));
connect(mNotificationDesktopIntegration.get(), &Notification::buttonClicked,
this, [this]() {
DesktopIntegration::execDialog(DesktopIntegration::Mode::Install,
qApp->activeWindow());
updateDesktopIntegrationNotification();
});
updateDesktopIntegrationNotification();
// Offer to re-enable dark theme for existing users if their theme
// has suddenly switched to light due to their system theme. We detect
// existing users by checking if there are any libraries installed or not.
// QT_VERSION_CHECK: Remove this after LibrePCB 2.1.x.
if ((!appVersionAlreadyRunBefore) &&
(!cs.value("global/windows").toStringList().isEmpty()) &&
(Application::getVersion().startsWith("2.0.") ||
Application::getVersion().startsWith("2.1.")) &&
mWorkspace.getSettings().uiTheme.get().isEmpty() &&
(!EditorToolbox::isSystemThemeDark()) &&
((!mWorkspace.getLocalLibrariesPath().isEmptyDir()) ||
(!mWorkspace.getRemoteLibrariesPath().isEmptyDir()))) {
auto notification = std::make_shared<Notification>(
ui::NotificationType::Info, tr("Light System Theme Detected"),
tr("LibrePCB now provides a light theme in addition to the dark theme. "
"It has been activated automatically due to your operating system "
"settings. If you like, you can switch back to the dark theme."),
tr("Use Dark Theme"), QString(), true);
connect(notification.get(), &Notification::buttonClicked, this, [this]() {
mWorkspace.getSettings().uiTheme.set(UiTheme::dark().id);
});
connect(&mWorkspace.getSettings().uiTheme, &WorkspaceSettingsItem::edited,
notification.get(), &Notification::dismiss, Qt::QueuedConnection);
mNotifications->push(notification);
}
// Show a notification during workspace libraries rescan.
connect(
&mWorkspace.getLibraryDb(), &WorkspaceLibraryDb::scanStarted, this,
[this]() {
std::shared_ptr<Notification> n = std::make_shared<Notification>(
ui::NotificationType::Progress, tr("Scanning Libraries") % "...",
tr("The internal libraries database is being updated. This may "
"take a few minutes and in the mean time you might see outdated "
"information about libraries."),
QString(), QString(), false);
connect(&mWorkspace.getLibraryDb(),
&WorkspaceLibraryDb::scanProgressUpdate, n.get(),
&Notification::setProgress);
connect(&mWorkspace.getLibraryDb(), &WorkspaceLibraryDb::scanFinished,
n.get(), &Notification::dismiss);
mNotifications->push(n);
});
// If the library rescan failed, show a notification error.
connect(&mWorkspace.getLibraryDb(), &WorkspaceLibraryDb::scanFailed, this,
[this](const QString& err) {
std::shared_ptr<Notification> n = std::make_shared<Notification>(
ui::NotificationType::Critical, tr("Scanning Libraries Failed"),
err, tr("Clear Cache"), QString(), true);
connect(n.get(), &Notification::buttonClicked,
&mWorkspace.getLibraryDb(),
&WorkspaceLibraryDb::resetAndRescan);
connect(&mWorkspace.getLibraryDb(),
&WorkspaceLibraryDb::scanStarted, n.get(),
&Notification::dismiss);
mNotifications->push(n);
});
// Setup library models & filter.
connect(mRemoteLibraries.get(), &LibrariesModel::onlineVersionsAvailable,
mLocalLibraries.get(), &LibrariesModel::setOnlineVersions);
connect(mRemoteLibraries.get(), &LibrariesModel::aboutToUninstallLibrary,
this, &GuiApplication::closeLibrary);
// Check if standard components are installed.
connect(&mWorkspace.getLibraryDb(), &WorkspaceLibraryDb::scanFinished, this,
&GuiApplication::updateLibrariesContainStandardComponents);
updateLibrariesContainStandardComponents();
// Configure window saving countdown timer.
mSaveOpenedWindowsCountdown.setSingleShot(true);
connect(&mSaveOpenedWindowsCountdown, &QTimer::timeout, this, [this]() {
QStringList ids;
for (const auto& win : mWindows) {
ids.append(QString::number(win->getId()));
}
std::sort(ids.begin(), ids.end());
QSettings cs;
cs.setValue("global/windows", ids);
qDebug().noquote() << "Saved opened window IDs:" << ids.join(", ");
});
// 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,
&GuiApplication::openProjectsPassedByCommandLine);
// To allow opening files by the MacOS Finder, install event filter.
qApp->installEventFilter(this);
// Start library rescan.
mWorkspace.getLibraryDb().startLibraryRescan();
}
GuiApplication::~GuiApplication() noexcept {
mProjectLibraryUpdater.reset();
}
/*******************************************************************************
* Workspace
******************************************************************************/
void GuiApplication::openFile(const FilePath& fp, QWidget* parent) noexcept {
if ((fp.getSuffix() == "lpp") || (fp.getSuffix() == "lppz")) {
openProject(fp, parent);
} else if (fp.isValid()) {
DesktopServices ds(mWorkspace.getSettings());
ds.openLocalPath(fp);
}
}
void GuiApplication::switchWorkspace(QWidget* parent) noexcept {
InitializeWorkspaceWizard wizard(*mTheme, true, parent);
wizard.setWindowModality(Qt::WindowModal);
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(parent, tr("Workspace changed"),
tr("The chosen workspace will be used after "
"restarting the application."));
}
}
void GuiApplication::execWorkspaceSettingsDialog(QWidget* parent) noexcept {
WorkspaceSettingsDialog dlg(mWorkspace, parent);
connect(&dlg, &WorkspaceSettingsDialog::desktopIntegrationStatusChanged, this,
&GuiApplication::updateDesktopIntegrationNotification);
dlg.exec();
}
void GuiApplication::addExampleProjects(QWidget* parent) 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(parent, tr("Add Example Projects"), msg,
QMessageBox::Ok | QMessageBox::Cancel);
if (ret == QMessageBox::Ok) {
InitializeWorkspaceWizardContext ctx(*mTheme, parent);
ctx.setWorkspacePath(mWorkspace.getPath());
ctx.installExampleProjects();
}
}
/*******************************************************************************
* Libraries
******************************************************************************/
std::shared_ptr<LibraryEditor> GuiApplication::getLibrary(
const FilePath& libDir) noexcept {
for (int i = 0; i < mLibraries->count(); ++i) {
if (mLibraries->at(i)->getFilePath() == libDir) {
return mLibraries->at(i);
}
}
return nullptr;
}
std::shared_ptr<LibraryEditor> GuiApplication::openLibrary(
const FilePath& libDir) noexcept {
auto switchToLibrary = [this](int index) {
for (auto win : mWindows) {
win->setCurrentLibrary(index);
win->showPanelPage(ui::PanelPage::Documents);
}
};
if (auto lib = getLibrary(libDir)) {
if (auto index = mLibraries->indexOf(lib.get())) {
switchToLibrary(*index);
}
return lib;
}
auto askForRestoringBackup = [](const FilePath&) {
QMessageBox::StandardButton btn = QMessageBox::question(
qApp->activeWindow(), tr("Restore autosave backup?"),
tr("It seems that the application crashed the last time you opened "
"this library. 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__);
}
};
try {
// Open file system.
const bool readOnly =
libDir.isLocatedInDir(mWorkspace.getRemoteLibrariesPath());
auto fs = TransactionalFileSystem::open(
libDir, !readOnly, askForRestoringBackup,
DirectoryLockHandlerDialog::createDirectoryLockCallback()); // can
// throw
// Open library.
auto lib = Library::open(std::unique_ptr<TransactionalDirectory>(
new TransactionalDirectory(fs))); // can throw
// Keep handle.
const int index = mLibraries->count();
auto editor = std::make_shared<LibraryEditor>(*this, std::move(lib), index);
mLibraries->insert(index, editor);
switchToLibrary(index);
return editor;
} catch (const Exception& e) {
QMessageBox::critical(qApp->activeWindow(), tr("Failed to open library"),
e.getMsg());
}
return nullptr;
}
void GuiApplication::closeLibrary(const FilePath& libDir) noexcept {
if (auto index = mLibraries->indexOf(getLibrary(libDir).get())) {
mLibraries->remove(*index);
for (int i = *index; i < mLibraries->count(); ++i) {
mLibraries->at(i)->setUiIndex(i);
}
}
}
bool GuiApplication::requestClosingAllLibraries() noexcept {
for (std::size_t i = 0; i < mLibraries->row_count(); ++i) {
if (auto lib = mLibraries->value(i)) {
if (!lib->requestClose()) {
return false;
}
}
}
return true;
}
/*******************************************************************************
* Projects
******************************************************************************/
void GuiApplication::createProject(const FilePath& parentDir, bool eagleImport,
QWidget* parent) noexcept {
const NewProjectWizard::Mode mode = eagleImport
? NewProjectWizard::Mode::EagleImport
: NewProjectWizard::Mode::NewProject;
NewProjectWizard wizard(*mTheme, mWorkspace, mode, parent);
wizard.setWindowModality(Qt::WindowModal);
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.
openProject(fp, parent);
} catch (const Exception& e) {
QMessageBox::critical(parent, tr("Could not create project"), e.getMsg());
}
}
}
std::shared_ptr<ProjectEditor> GuiApplication::openProject(
FilePath fp, QWidget* parent) noexcept {
if (!fp.isValid()) {
QSettings cs; // client settings
QString lastOpenedFile =
cs.value("controlpanel/last_open_project", mWorkspace.getPath().toStr())
.toString();
fp = FilePath(FileDialog::getOpenFileName(
parent, tr("Open Project"), lastOpenedFile,
tr("LibrePCB project files (%1)").arg("*.lpp *.lppz")));
if (!fp.isValid()) return nullptr;
cs.setValue("controlpanel/last_open_project", fp.toNative());
}
auto switchToProject = [this](int index) {
for (auto win : mWindows) {
win->setCurrentProject(index);
win->showPanelPage(ui::PanelPage::Documents);
}
};
// If the same project is already open, just return it.
const FilePath uniqueFp = fp.toUnique();
for (int i = 0; i < mProjects->count(); ++i) {
auto prj = mProjects->at(i);
if (prj->getProject().getFilepath().toUnique() == uniqueFp) {
switchToProject(i);
return prj;
}
}
// Opening the project can take some time, use wait cursor to provide
// immediate UI feedback.
QApplication::setOverrideCursor(Qt::WaitCursor);
auto cursorScopeGuard =
scopeGuard([]() { QApplication::restoreOverrideCursor(); });
// Callback to ask for restoring backup.
auto askForRestoringBackup = [parent](const FilePath&) {
QMessageBox::StandardButton btn = QMessageBox::question(
parent, 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__);
}
};
try {
// Open file system.
std::shared_ptr<TransactionalFileSystem> fs;
QString projectFileName = fp.getFilename();
if (fp.getSuffix() == "lppz") {
fs = TransactionalFileSystem::openRO(
FilePath::getRandomTempPath(),
&TransactionalFileSystem::RestoreMode::no);
fs->removeDirRecursively(); // 1) Get a clean initial state.
fs->loadFromZip(fp); // 2) Load files from ZIP.
foreach (const QString& fn, fs->getFiles()) {
if (fn.endsWith(".lpp")) {
projectFileName = fn;
}
}
} else {
fs = TransactionalFileSystem::openRW(
fp.getParentDir(), askForRestoringBackup,
DirectoryLockHandlerDialog::createDirectoryLockCallback());
}
// Open project.
ProjectLoader loader;
std::unique_ptr<Project> project = loader.open(
std::unique_ptr<TransactionalDirectory>(new TransactionalDirectory(fs)),
projectFileName); // can throw
// Open editor & keep handle.
const int index = mProjects->count();
auto editor = std::make_shared<ProjectEditor>(
*this, std::move(project), index, loader.getMigrationLog());
connect(editor.get(), &ProjectEditor::statusBarMessageChanged, this,
&GuiApplication::statusBarMessageChanged);
connect(editor.get(), &ProjectEditor::ercMessageHighlightRequested, this,
&GuiApplication::highlightErcMessage);
connect(editor.get(), &ProjectEditor::projectLibraryUpdaterRequested, this,
&GuiApplication::openProjectLibraryUpdater);
mProjects->append(editor);
// Switch to documents tab.
switchToProject(index);
// Open the first schematic and board to get immediate feedback that the
// project has been opened. In future we should restore the tabs which were
// opened the last time, so this is just a temporary solution.
if (auto win = getCurrentWindow()) {
bool tabOpened = false;
if (!editor->getProject().getSchematics().isEmpty()) {
win->openSchematicTab(index, 0);
tabOpened = true;
}
if (!editor->getProject().getBoards().isEmpty()) {
win->openBoard2dTab(index, 0, !tabOpened);
}
}
// 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, fp]() { mQuickAccessModel->pushRecentProject(fp); });
return editor;
} catch (const Exception& e) {
QMessageBox::critical(parent, tr("Error"), e.getMsg());
return nullptr;
}
}
void GuiApplication::closeProject(int index) noexcept {
mProjects->remove(index);
for (int i = index; i < mProjects->count(); ++i) {
mProjects->at(i)->setUiIndex(i);
}
}
bool GuiApplication::requestClosingAllProjects() noexcept {
for (std::size_t i = 0; i < mProjects->row_count(); ++i) {
if (auto prj = mProjects->value(i)) {
if (!prj->requestClose()) {
return false;
}
}
}
return true;
}
/*******************************************************************************
* Window Management
******************************************************************************/
void GuiApplication::createNewWindow(int id, int projectIndex) noexcept {
// Reuse next free window ID.
if (id < 1) {
id = 1;
while (std::any_of(mWindows.begin(), mWindows.end(),
[id](const std::shared_ptr<MainWindow>& w) {
return w->getId() == id;
})) {
++id;
}
}
// Create Slint window.
auto win = ui::AppWindow::create();
// Helper to create filtered, sorted library models.
auto filteredLibs = [this](const std::shared_ptr<LibrariesModel>& model) {
auto filterModel =
std::make_shared<slint::FilterModel<ui::LibraryInfoData>>(
model, [this](const ui::LibraryInfoData& lib) {
auto s = mLibrariesFilter->getText().trimmed().toLower();
return s.isEmpty() || s2q(lib.name).toLower().contains(s);
});
connect(mLibrariesFilter.get(), &SlintKeyEventTextBuilder::textChanged,
this, [filterModel](const QString&) { filterModel->reset(); });
return filterModel;
};
auto sortedLibs =
[](const std::shared_ptr<slint::Model<ui::LibraryInfoData>>& model) {
return std::make_shared<slint::SortModel<ui::LibraryInfoData>>(
model,
[](const ui::LibraryInfoData& a, const ui::LibraryInfoData& b) {
if ((a.progress > 0) != (b.progress > 0)) {
return a.progress > 0;
} else if (a.outdated != b.outdated) {
return a.outdated;
} else if (a.installed_version.empty() !=
b.installed_version.empty()) {
return b.installed_version.empty();
} else if (a.recommended != b.recommended) {
return a.recommended;
} else {
return a.name < b.name;
}
});
};
// Set global data.
const ui::Data& d = win->global<ui::Data>();
d.set_preview_mode(false);
d.set_window_id(id);
d.set_window_title(
q2s(QString("LibrePCB %1").arg(Application::getVersion())));
d.set_about_librepcb_details(q2s(Application::buildFullVersionDetails()));
d.set_workspace_path(q2s(mWorkspace.getPath().toNative()));
d.set_notifications(mNotifications);
d.set_quick_access_items(mQuickAccessModel);
d.set_local_libraries(filteredLibs(mLocalLibraries));
d.set_remote_libraries(sortedLibs(filteredLibs(mRemoteLibraries)));
d.set_projects(mProjects);
d.fn_set_current_project(projectIndex);
d.set_libraries(mLibraries);
d.set_min_length(l2s(Length::min()));
d.set_norms(q2s(getAvailableNorms()));
// Populate attribute types & units.
auto attributeTypes =
std::make_shared<slint::VectorModel<slint::SharedString>>();
auto attributeUnits = std::make_shared<
slint::VectorModel<std::shared_ptr<slint::Model<slint::SharedString>>>>();
for (auto t : AttributeType::getAllTypes()) {
attributeTypes->push_back(q2s(t->getNameTr()));
auto units = std::make_shared<slint::VectorModel<slint::SharedString>>();
for (auto u : t->getAvailableUnits()) {
units->push_back(q2s(u->getSymbolTr()));
}
attributeUnits->push_back(units);
}
d.set_attribute_types(attributeTypes);
d.set_attribute_units(attributeUnits);
// Register global callbacks.
const ui::Backend& b = win->global<ui::Backend>();
b.on_is_shortcut(&isShortcut);
b.on_drop_tab([this](const slint::SharedString& srcData,
const slint::SharedString& dstData,
bool forceSwitchToTab) {
const QStringList src = s2q(srcData).split(",");
const QStringList dst = s2q(dstData).split(",");
moveTab(src[0].toInt(), src[1].toInt(), src[2].toInt(), //
dst[0].toInt(), dst[1].toInt(), dst[2].toInt(), forceSwitchToTab);
});
b.on_open_url([this](const slint::SharedString& url) {
DesktopServices ds(mWorkspace.getSettings());
return ds.openUrl(QUrl(s2q(url)));
});
b.on_libraries_key_pressed(
std::bind(&SlintKeyEventTextBuilder::processKeyPressed,
mLibrariesFilter.get(), std::placeholders::_1));
b.on_copy_to_clipboard([](const slint::SharedString& s) {
QApplication::clipboard()->setText(s2q(s));
return true;
});
b.on_get_organizations_with_design_rules([this]() {
auto model = mOrganizationsWithPcbDesignRules.lock();
if (!model) {
model = std::make_shared<OrganizationsDbModel>(mWorkspace.getLibraryDb(),
mWorkspace.getSettings());
mOrganizationsWithPcbDesignRules = model;
}
return model;
});
b.on_format_length([](const ui::Int64& value, ui::LengthUnit unit) {
const LengthUnit lpUnit = s2l(unit);
return q2s(Toolbox::floatToString(lpUnit.convertToUnit(s2l(value)),
lpUnit.getReasonableNumberOfDecimals(),
QLocale()));
});
b.on_parse_length_input(
[](slint::SharedString text, ui::LengthUnit unit, ui::Int64 minimum) {
ui::LengthEditParseResult res{false, ui::Int64{0, 0}, unit};
try {
QString value = s2q(text);
// Extract unit from string.
if (auto parsedUnit = LengthUnit::extractFromExpression(value)) {
res.evaluated_unit = l2s(*parsedUnit);
}
const LengthUnit lpUnit = s2l(res.evaluated_unit);
// Parse expression and convert to Length.
const MathParser::Result result = MathParser().parse(value);
if (result.valid) {
const Length lpValue =
lpUnit.convertFromUnit(result.value); // can throw
if (lpValue >= s2length(minimum)) {
res.evaluated_value = l2s(lpValue);
res.valid = true;
}
}
} catch (const Exception& e) {
}
return res;
});
b.on_format_angle([](const int& value) {
const Angle angle = s2angle(value);
return q2s(Toolbox::floatToString(angle.toDeg(), 3, QLocale()));
});
b.on_parse_angle_input([](slint::SharedString text) {
ui::AngleEditParseResult res{false, 0};
try {
QString value = s2q(text);
// Remove unit
value.replace("°", "");
// Parse expression and convert to Angle.
const MathParser::Result result = MathParser().parse(value);
if (result.valid) {
const Angle angle = Angle::fromDeg(result.value);
res.evaluated_value = l2s(angle);
res.valid = true;
}
} catch (const Exception& e) {
}
return res;
});
b.on_format_ratio([](const int& value) {
const Ratio ratio = s2ratio(value);
return q2s(Toolbox::floatToString(ratio.toPercent(), 3, QLocale()));
});
b.on_parse_ratio_input(
[](slint::SharedString text, int minimum, int maximum) {
ui::RatioEditParseResult res{false, 0};
try {
QString value = s2q(text);
// Remove unit and spaces
value.replace("%", "");
value.replace(" ", "");
// Parse expression and convert to Ratio.
const MathParser::Result result = MathParser().parse(value);
if (result.valid) {
const Ratio ratio = Ratio::fromPercent(result.value);
if ((ratio >= s2ratio(minimum)) && (ratio <= s2ratio(maximum))) {
res.evaluated_value = l2s(ratio);
res.valid = true;
}
}
} catch (const Exception& e) {
}
return res;
});
b.on_toggle_theme([this]() {
const auto& themes = UiTheme::all();
const int index = (themes.indexOf(mTheme) + 1) % themes.count();
mWorkspace.getSettings().uiTheme.set(themes.at(index)->id);
});
// Build wrapper.
auto mw = std::make_shared<MainWindow>(*this, win, id);
bind(mw.get(), d, &ui::Data::set_local_libraries_data, mLocalLibraries.get(),
&LibrariesModel::uiDataChanged, mLocalLibraries->getUiData());
bind(mw.get(), d, &ui::Data::set_remote_libraries_data,
mRemoteLibraries.get(), &LibrariesModel::uiDataChanged,
mRemoteLibraries->getUiData());
bind(mw.get(), d, &ui::Data::set_libraries_panel_filter,
mLibrariesFilter.get(), &SlintKeyEventTextBuilder::textChanged,
mLibrariesFilter->getText());
bind(mw.get(), d, &ui::Data::set_libraries_rescan_in_progress,
&mWorkspace.getLibraryDb(), &WorkspaceLibraryDb::scanInProgressChanged,
mWorkspace.getLibraryDb().isScanInProgress());
bind(mw.get(), d, &ui::Data::set_workspace_contains_standard_components, this,
&GuiApplication::librariesContainStandardComponentsChanged,
mLibrariesContainStandardComponents);
connect(this, &GuiApplication::statusBarMessageChanged, mw.get(),
&MainWindow::showStatusBarMessage);
connect(
mw.get(), &MainWindow::aboutToClose, this,
[this]() {
MainWindow* mw = static_cast<MainWindow*>(sender());
qDebug().nospace() << "Closed window with ID " << mw->getId() << ".";
mWindows.removeIf(
[mw](std::shared_ptr<MainWindow> p) { return p.get() == mw; });
mw = nullptr; // Not valid anymore!
// Schedule saving number of opened windows.
mSaveOpenedWindowsCountdown.start(10000);
},
Qt::QueuedConnection);
mWindows.append(mw);
qDebug().nospace() << "Opened new window with ID " << id << ".";
// Schedule saving number of opened windows.
mSaveOpenedWindowsCountdown.start(10000);
}
int GuiApplication::getWindowCount() const noexcept {
return mWindows.count();
}
void GuiApplication::stopWindowStateAutosaveTimer() noexcept {
mSaveOpenedWindowsCountdown.stop();
}
/*******************************************************************************
* General Methods
******************************************************************************/
void GuiApplication::exec() {
slint::run_event_loop();
}
void GuiApplication::quit(QPointer<QWidget> parent) noexcept {
// Need to be delayed since this call is made from the object to be
// deleted.
QMetaObject::invokeMethod(
this,
[this, parent]() {
if (requestClosingAllProjects() && requestClosingAllLibraries()) {
mWindows.clear();
slint::quit_event_loop();
}
},
Qt::QueuedConnection);
}
/*******************************************************************************
* Protected Methods
******************************************************************************/
bool GuiApplication::eventFilter(QObject* watched, QEvent* event) noexcept {
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 QObject::eventFilter(watched, event);
}
/*******************************************************************************
* Private Methods
******************************************************************************/
void GuiApplication::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 GuiApplication::openProjectPassedByOs(const QString& file,
bool silent) noexcept {
FilePath filepath(file);
if ((filepath.isExistingFile()) &&
((filepath.getSuffix() == "lpp") || (filepath.getSuffix() == "lppz"))) {
openProject(filepath, qApp->activeWindow());
} else if (!silent) {
qWarning() << "Ignore invalid request to open project:" << file;
}
}
void GuiApplication::openProjectLibraryUpdater(
const FilePath& project) noexcept {
bool wasOpen = false;
for (auto prj : *mProjects) {
if (prj->getProject().getFilepath() == project) {
wasOpen = true;
break;
}
}
mProjectLibraryUpdater.reset(new ProjectLibraryUpdater(
mWorkspace, project, [this](const FilePath& fp) {
for (int i = 0; i < mProjects->count(); ++i) {
auto prjEditor = mProjects->at(i);
if (prjEditor->getProject().getFilepath() == fp) {
if (prjEditor->requestClose()) {
closeProject(i);
return true;
}
}
}
return false;
}));
if (wasOpen) {
connect(
mProjectLibraryUpdater.get(), &ProjectLibraryUpdater::finished, this,
[this](const FilePath& fp) { openProject(fp, qApp->activeWindow()); });
}
mProjectLibraryUpdater->show();
}
void GuiApplication::highlightErcMessage(
std::shared_ptr<const RuleCheckMessage> msg, bool zoomTo,
int windowId) noexcept {
ProjectEditor* prjEditor = dynamic_cast<ProjectEditor*>(sender());
if (!prjEditor) {
qCritical() << "Signal from unknown ProjectEditor.";
return;
}
if (auto win = getWindowById(windowId)) {
win->highlightErcMessage(*prjEditor, msg, zoomTo);
} else {
qCritical() << "Unknown window ID:" << windowId;
}
}
std::shared_ptr<MainWindow> GuiApplication::getCurrentWindow() noexcept {
for (auto& win : mWindows) {
if (win->isCurrentWindow()) {
return win;
}
}
// TODO: This does not work in every case yet, so we implement some fallback
// as a workaround.
return mWindows.value(mWindows.count() - 1);
}
void GuiApplication::updateLibrariesContainStandardComponents() noexcept {
bool found = false;
try {
// Check only the resistor component, that should be enough...
const FilePath fp = mWorkspace.getLibraryDb().getLatest<Component>(
Uuid::fromString("ef80cd5e-2689-47ee-8888-31d04fc99174"));
found = fp.isValid();
} catch (const Exception& e) {
}
if (found != mLibrariesContainStandardComponents) {
mLibrariesContainStandardComponents = found;
emit librariesContainStandardComponentsChanged(found);
}
}
void GuiApplication::updateNoLibrariesInstalledNotification() noexcept {
if (!mNotificationNoLibrariesInstalled) return;
bool showWarning = false;
try {
showWarning = mWorkspace.getLibraryDb().getAll<Library>().isEmpty();
} catch (const Exception& e) {
qCritical() << "Failed to get workspace library list:" << e.getMsg();
}
if (showWarning) {
mNotifications->push(mNotificationNoLibrariesInstalled);
} else {
mNotificationNoLibrariesInstalled->dismiss();
}
}
void GuiApplication::updateDesktopIntegrationNotification() noexcept {
if (!mNotificationDesktopIntegration) return;
if (DesktopIntegration::isSupported() &&
(DesktopIntegration::getStatus() !=
DesktopIntegration::Status::InstalledThis)) {
mNotifications->push(mNotificationDesktopIntegration);
} else {
mNotificationDesktopIntegration->dismiss();
}
}
void GuiApplication::moveTab(int srcWindowId, int srcSectionIndex,
int srcTabIndex, int dstWindowId,
int dstSectionIndex, int dstTabIndex,
bool forceSwitchToTab) noexcept {
if ((srcWindowId == dstWindowId) && (srcSectionIndex == dstSectionIndex) &&
(dstTabIndex > srcTabIndex)) {
--dstTabIndex; // Moving to the right needs index correction.
}
if ((srcWindowId == dstWindowId) && (srcSectionIndex == dstSectionIndex) &&
(dstTabIndex == srcTabIndex)) {
return; // Tab is actually not moved (destination == source).
}
if ((srcSectionIndex == 0) && (srcTabIndex == 0)) {
return; // Home tab is not movable.
}
auto srcWindow = getWindowById(srcWindowId);
auto dstWindow = getWindowById(dstWindowId);
if (srcWindow && dstWindow) {
bool wasCurrentTab = false;
bool wasCurrentSection = false;
if (auto tab = srcWindow->removeTab(srcSectionIndex, srcTabIndex,
&wasCurrentTab, &wasCurrentSection)) {
if (dstTabIndex == -1) {
dstWindow->addSection(dstSectionIndex, true);
dstTabIndex = 0;
}
if ((dstSectionIndex == 0) && (dstTabIndex == 0)) {
dstTabIndex = 1; // Index 0 is the home tab.
}
const bool switchToTab = forceSwitchToTab ||
(wasCurrentTab &&
(wasCurrentSection || (dstWindowId != srcWindowId) ||
(dstSectionIndex != srcSectionIndex)));
const bool switchToSection = (wasCurrentSection && wasCurrentTab);
dstWindow->addTab(tab, dstSectionIndex, dstTabIndex, switchToTab,
switchToSection);
}
}
}
std::shared_ptr<MainWindow> GuiApplication::getWindowById(int id) noexcept {
for (const auto& win : mWindows) {
if (win->getId() == id) {
return win;
}
}
return nullptr;
}
/*******************************************************************************
* End of File
******************************************************************************/
} // namespace editor
} // namespace librepcb
acb84dbb7This file is pinned to the selected release. Download the original to open it in your design tool.