Loading files
ee84c48d4This file is pinned to the selected release. Download the original to open it in your design tool.
/*
* 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/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 </usr/include/qt6/QtGui/6.10.1/QtGui/qpa/qplatformnativeinterface.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 <slint-platform.h>
#include <xcb/xcb.h>
#include <QGuiApplication>
#include <QWindow>
#include <QtCore>
#include <QtPrivate>
#include <algorithm>
/*******************************************************************************
* Namespace
******************************************************************************/
namespace librepcb {
namespace editor {
static void update_timer() {
static QTimer timer;
static auto init = [] {
timer.callOnTimeout([] {
slint::platform::update_timers_and_animations();
update_timer();
});
return true;
}();
if (auto timeout = slint::platform::duration_until_next_timer_update()) {
timer.start(*timeout);
} else {
timer.stop();
}
}
slint::PointerEventButton convert_button(Qt::MouseButtons b) {
switch (b) {
case Qt::LeftButton:
return slint::PointerEventButton::Left;
case Qt::RightButton:
return slint::PointerEventButton::Right;
case Qt::MiddleButton:
return slint::PointerEventButton::Middle;
default:
return slint::PointerEventButton::Other;
}
}
static slint::platform::NativeWindowHandle window_handle_for_qt_window(
QWindow* window) {
// Ensure that the native window surface exists
window->create();
#ifdef __APPLE__
QPlatformNativeInterface* native = qApp->platformNativeInterface();
NSView* nsview = reinterpret_cast<NSView*>(
native->nativeResourceForWindow(QByteArray("nsview"), window));
NSWindow* nswindow = reinterpret_cast<NSWindow*>(
native->nativeResourceForWindow(QByteArray("nswindow"), window));
return slint::platform::NativeWindowHandle::from_appkit(nsview, nswindow);
#elif defined Q_OS_WIN
auto wid = Qt::HANDLE(window->winId());
return slint::platform::NativeWindowHandle::from_win32(
wid, GetModuleHandle(nullptr));
#else
// Try Wayland first, then XLib, then Xcb
auto wid = window->winId();
auto visual_id = 0; // FIXME
QPlatformNativeInterface* native = qApp->platformNativeInterface();
auto screen = quintptr(
native->nativeResourceForWindow(QByteArray("x11screen"), window));
if (auto* wayland_display = reinterpret_cast<wl_display*>(
native->nativeResourceForIntegration(QByteArray("wl_display")))) {
auto* wayland_surface = reinterpret_cast<wl_surface*>(
native->nativeResourceForWindow(QByteArray("surface"), window));
return slint::platform::NativeWindowHandle::from_wayland(wayland_surface,
wayland_display);
} else if (auto* x11_display = native->nativeResourceForWindow(
QByteArray("display"), window)) {
return slint::platform::NativeWindowHandle::from_x11_xlib(
wid, wid, x11_display, screen);
} else if (auto* xcb_connection = reinterpret_cast<xcb_connection_t*>(
native->nativeResourceForWindow(QByteArray("connection"),
window))) {
return slint::platform::NativeWindowHandle::from_x11_xcb(
wid, wid, xcb_connection, screen);
} else {
throw "Unsupported windowing system (tried wayland, xlib, and xcb)";
}
#endif
}
static slint::SharedString key_event_text(QKeyEvent* e) {
switch (e->key()) {
case Qt::Key::Key_Backspace:
return slint::platform::key_codes::Backspace;
case Qt::Key::Key_Tab:
return slint::platform::key_codes::Tab;
case Qt::Key::Key_Enter:
case Qt::Key::Key_Return:
return slint::platform::key_codes::Return;
case Qt::Key::Key_Escape:
return slint::platform::key_codes::Escape;
case Qt::Key::Key_Backtab:
return slint::platform::key_codes::Backtab;
case Qt::Key::Key_Delete:
return slint::platform::key_codes::Delete;
case Qt::Key::Key_Shift:
return slint::platform::key_codes::Shift;
case Qt::Key::Key_Control:
return slint::platform::key_codes::Control;
case Qt::Key::Key_Alt:
return slint::platform::key_codes::Alt;
case Qt::Key::Key_AltGr:
return slint::platform::key_codes::AltGr;
case Qt::Key::Key_CapsLock:
return slint::platform::key_codes::CapsLock;
case Qt::Key::Key_Meta:
return slint::platform::key_codes::Meta;
case Qt::Key::Key_Up:
return slint::platform::key_codes::UpArrow;
case Qt::Key::Key_Down:
return slint::platform::key_codes::DownArrow;
case Qt::Key::Key_Left:
return slint::platform::key_codes::LeftArrow;
case Qt::Key::Key_Right:
return slint::platform::key_codes::RightArrow;
case Qt::Key::Key_F1:
return slint::platform::key_codes::F1;
case Qt::Key::Key_F2:
return slint::platform::key_codes::F2;
case Qt::Key::Key_F3:
return slint::platform::key_codes::F3;
case Qt::Key::Key_F4:
return slint::platform::key_codes::F4;
case Qt::Key::Key_F5:
return slint::platform::key_codes::F5;
case Qt::Key::Key_F6:
return slint::platform::key_codes::F6;
case Qt::Key::Key_F7:
return slint::platform::key_codes::F7;
case Qt::Key::Key_F8:
return slint::platform::key_codes::F8;
case Qt::Key::Key_F9:
return slint::platform::key_codes::F9;
case Qt::Key::Key_F10:
return slint::platform::key_codes::F10;
case Qt::Key::Key_F11:
return slint::platform::key_codes::F11;
case Qt::Key::Key_F12:
return slint::platform::key_codes::F12;
case Qt::Key::Key_F13:
return slint::platform::key_codes::F13;
case Qt::Key::Key_F14:
return slint::platform::key_codes::F14;
case Qt::Key::Key_F15:
return slint::platform::key_codes::F15;
case Qt::Key::Key_F16:
return slint::platform::key_codes::F16;
case Qt::Key::Key_F17:
return slint::platform::key_codes::F17;
case Qt::Key::Key_F18:
return slint::platform::key_codes::F18;
case Qt::Key::Key_F19:
return slint::platform::key_codes::F19;
case Qt::Key::Key_F20:
return slint::platform::key_codes::F20;
case Qt::Key::Key_F21:
return slint::platform::key_codes::F21;
case Qt::Key::Key_F22:
return slint::platform::key_codes::F22;
case Qt::Key::Key_F23:
return slint::platform::key_codes::F23;
case Qt::Key::Key_F24:
return slint::platform::key_codes::F24;
case Qt::Key::Key_Insert:
return slint::platform::key_codes::Insert;
case Qt::Key::Key_Home:
return slint::platform::key_codes::Home;
case Qt::Key::Key_End:
return slint::platform::key_codes::End;
case Qt::Key::Key_PageUp:
return slint::platform::key_codes::PageUp;
case Qt::Key::Key_PageDown:
return slint::platform::key_codes::PageDown;
case Qt::Key::Key_ScrollLock:
return slint::platform::key_codes::ScrollLock;
case Qt::Key::Key_Pause:
return slint::platform::key_codes::Pause;
case Qt::Key::Key_SysReq:
return slint::platform::key_codes::SysReq;
case Qt::Key::Key_Stop:
return slint::platform::key_codes::Stop;
case Qt::Key::Key_Menu:
return slint::platform::key_codes::Menu;
default:
if (e->modifiers() & Qt::ControlModifier) {
// e->text() is not the key when Ctrl is pressed
return QKeySequence(e->key()).toString().toLower().toUtf8().data();
}
return e->text().toUtf8().data();
}
}
class MyWindow : public QWindow, public slint::platform::WindowAdapter {
std::optional<slint::platform::SkiaRenderer> m_renderer;
bool m_visible = false;
public:
MyWindow(QWindow* parentWindow = nullptr) : QWindow(parentWindow) {
resize(640, 480);
m_renderer.emplace(window_handle_for_qt_window(this), size());
}
slint::platform::AbstractRenderer& renderer() override {
return m_renderer.value();
}
void paintEvent(QPaintEvent* ev) override {
slint::platform::update_timers_and_animations();
m_renderer->render();
if (window().has_active_animations()) {
requestUpdate();
}
update_timer();
}
void closeEvent(QCloseEvent* event) override {
if (m_visible) {
event->ignore();
window().dispatch_close_requested_event();
}
}
bool event(QEvent* e) override {
if (e->type() == QEvent::UpdateRequest) {
paintEvent(static_cast<QPaintEvent*>(e));
return true;
} else if (e->type() == QEvent::KeyPress) {
auto ke = static_cast<QKeyEvent*>(e);
if (ke->isAutoRepeat())
window().dispatch_key_press_repeat_event(key_event_text(ke));
else
window().dispatch_key_press_event(key_event_text(ke));
return true;
} else if (e->type() == QEvent::KeyRelease) {
window().dispatch_key_release_event(
key_event_text(static_cast<QKeyEvent*>(e)));
return true;
} else if (e->type() == QEvent::WindowActivate) {
window().dispatch_window_active_changed_event(true);
return true;
} else if (e->type() == QEvent::WindowDeactivate) {
window().dispatch_window_active_changed_event(false);
return true;
} else {
return QWindow::event(e);
}
}
void set_visible(bool visible) override {
m_visible = visible;
if (visible) {
window().dispatch_scale_factor_change_event(devicePixelRatio());
QWindow::show();
} else {
QWindow::close();
}
}
void set_size(slint::PhysicalSize size) override {
float scale_factor = devicePixelRatio();
resize(size.width / scale_factor, size.height / scale_factor);
}
slint::PhysicalSize size() override {
auto windowSize = slint::LogicalSize({float(width()), float(height())});
float scale_factor = devicePixelRatio();
return slint::PhysicalSize({uint32_t(windowSize.width * scale_factor),
uint32_t(windowSize.height * scale_factor)});
}
void set_position(slint::PhysicalPosition position) override {
float scale_factor = devicePixelRatio();
setFramePosition(
QPointF(position.x / scale_factor, position.y / scale_factor)
.toPoint());
}
std::optional<slint::PhysicalPosition> position() override {
auto pos = framePosition();
float scale_factor = devicePixelRatio();
return {slint::PhysicalPosition(
{int32_t(pos.x() * scale_factor), int32_t(pos.y() * scale_factor)})};
}
void request_redraw() override { requestUpdate(); }
void update_window_properties(const WindowProperties& props) override {
QWindow::setTitle(QString::fromUtf8(props.title().data()));
auto c = props.layout_constraints();
QWindow::setMaximumSize(c.max ? QSize(c.max->width, c.max->height)
: QSize(1 << 15, 1 << 15));
QWindow::setMinimumSize(c.min ? QSize(c.min->width, c.min->height)
: QSize());
Qt::WindowStates states = windowState() & Qt::WindowActive;
if (props.is_fullscreen()) states |= Qt::WindowFullScreen;
if (props.is_minimized()) states |= Qt::WindowMinimized;
if (props.is_maximized()) states |= Qt::WindowMaximized;
setWindowStates(states);
}
void resizeEvent(QResizeEvent* ev) override {
auto logicalSize = ev->size();
window().dispatch_resize_event(slint::LogicalSize(
{float(logicalSize.width()), float(logicalSize.height())}));
}
void mousePressEvent(QMouseEvent* event) override {
slint::platform::update_timers_and_animations();
window().dispatch_pointer_press_event(
slint::LogicalPosition(
{float(event->pos().x()), float(event->pos().y())}),
convert_button(event->button()));
update_timer();
}
void mouseReleaseEvent(QMouseEvent* event) override {
slint::platform::update_timers_and_animations();
window().dispatch_pointer_release_event(
slint::LogicalPosition(
{float(event->pos().x()), float(event->pos().y())}),
convert_button(event->button()));
update_timer();
}
void mouseMoveEvent(QMouseEvent* event) override {
slint::platform::update_timers_and_animations();
window().dispatch_pointer_move_event(slint::LogicalPosition(
{float(event->pos().x()), float(event->pos().y())}));
update_timer();
}
};
struct MyPlatform : public slint::platform::Platform {
std::unique_ptr<QWindow> parentWindow;
std::unique_ptr<slint::platform::WindowAdapter> create_window_adapter()
override {
return std::make_unique<MyWindow>(parentWindow.get());
}
void set_clipboard_text(
const slint::SharedString& str,
slint::platform::Platform::Clipboard clipboard) override {
switch (clipboard) {
case slint::platform::Platform::Clipboard::DefaultClipboard:
qApp->clipboard()->setText(QString::fromUtf8(str.data()),
QClipboard::Clipboard);
break;
case slint::platform::Platform::Clipboard::SelectionClipboard:
qApp->clipboard()->setText(QString::fromUtf8(str.data()),
QClipboard::Selection);
break;
}
}
std::optional<slint::SharedString> clipboard_text(
Clipboard clipboard) override {
QString text;
switch (clipboard) {
case slint::platform::Platform::Clipboard::DefaultClipboard:
text = qApp->clipboard()->text(QClipboard::Clipboard);
break;
case slint::platform::Platform::Clipboard::SelectionClipboard:
text = qApp->clipboard()->text(QClipboard::Selection);
break;
default:
return {};
}
if (text.isNull()) {
return {};
} else {
return slint::SharedString(text.toUtf8().data());
}
}
};
// Translation callback for Slint. Needs to convert gettext placeholders
// to Qt placeholders ("{1}" -> "%1"). Not very elegant for now, could
// probably be improved a lot...
void slintTr(slint::private_api::Slice<uint8_t> string,
slint::private_api::Slice<uint8_t> ctx,
slint::private_api::Slice<uint8_t> domain, int32_t n,
slint::private_api::Slice<uint8_t> plural,
slint::SharedString* out) noexcept {
Q_UNUSED(domain);
const QByteArray context =
"ui::" + QByteArray(reinterpret_cast<const char*>(ctx.ptr), ctx.len);
QByteArray str =
(plural.len
? QByteArray(reinterpret_cast<const char*>(plural.ptr), plural.len)
: QByteArray(reinterpret_cast<const char*>(string.ptr), string.len));
// Helpers to build pattern strings "{n}" and "%n".
QByteArray pattern;
auto buildSlintPattern = [&pattern](int i) -> const QByteArray& {
pattern = "{" + QByteArray::number(i) + "}";
return pattern;
};
auto buildQtPattern = [&pattern](int i) -> const QByteArray& {
pattern = "%" + QByteArray::number(i);
return pattern;
};
str.replace("{n}", "%n");
for (int i = 0; str.contains(buildSlintPattern(i)); ++i) {
str.replace(pattern, "%" + QByteArray::number(i + 1));
}
for (int i = 1; str.contains("{}"); ++i) {
str.replace(str.indexOf("{}"), 2, "%" + QByteArray::number(i));
}
str = QCoreApplication::translate(context.data(), str.data(), nullptr,
plural.len ? n : -1)
.toUtf8();
str.replace("%n", "{n}");
for (int i = 1; str.contains(buildQtPattern(i)); ++i) {
str.replace(pattern, "{" + QByteArray::number(i - 1) + "}");
}
*out = std::string_view(str.data(), str.size());
}
/*******************************************************************************
* Constructors / Destructor
******************************************************************************/
GuiApplication::GuiApplication(Workspace& ws, bool fileFormatIsOutdated,
QObject* parent) noexcept
: QObject(parent),
mWorkspace(ws),
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>()) {
// Open windows.
QSettings cs;
for (const QString& idStr : cs.value("global/windows").toStringList()) {
if (int id = idStr.toInt()) {
createNewWindow(id);
}
}
if (mWindows.isEmpty()) {
createNewWindow();
}
// It seems registering the callback *before* the first Slint window is
// created, doesn't work for some reason so we do it here. Maybe the reason
// is that that's not an official Slint feature but a hack from myself ;-)
slint::private_api::slint_translate_set_translate_callback(&slintTr);
// 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();
// 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(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(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(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 {
static MyPlatform* platform = [] {
auto platform = std::make_unique<MyPlatform>();
auto p2 = platform.get();
slint::platform::set_platform(std::move(platform));
return p2;
}();
slint::platform::update_timers_and_animations();
// 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_event(std::bind(&SlintKeyEventTextBuilder::process,
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;
});
// 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() {
qApp->exec();
}
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) {
if (mProjects->at(i)->getProject().getFilepath() == fp) {
closeProject(i);
break;
}
}
return true;
}));
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