Loading files
6426b5c96This 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 "editortoolbox.h"
#include "../project/partinformationprovider.h"
#include "../workspace/desktopservices.h"
#include "menubuilder.h"
#include <librepcb/core/3d/occmodel.h>
#include <librepcb/core/application.h>
#include <librepcb/core/geometry/path.h>
#include <librepcb/core/library/cmp/component.h>
#include <librepcb/core/library/dev/device.h>
#include <librepcb/core/project/board/items/bi_device.h>
#include <librepcb/core/project/circuit/circuit.h>
#include <librepcb/core/project/circuit/componentinstance.h>
#include <librepcb/core/project/project.h>
#include <librepcb/core/project/projectlibrary.h>
#include <librepcb/core/systeminfo.h>
#include <librepcb/core/workspace/workspace.h>
#include <librepcb/core/workspace/workspacelibrarydb.h>
#include <librepcb/core/workspace/workspacesettings.h>
#include <QSvgRenderer>
#include <QtCore>
#include <QtWidgets>
/*******************************************************************************
* Namespace
******************************************************************************/
namespace librepcb {
namespace editor {
/*******************************************************************************
* Class MonochromeSvgIconEngine
******************************************************************************/
// Custom icon engine to change the color of monochrome SVG icons on-thy-fly
// to the theme's text color. Works with Bootstrap Icons and Font Awesome.
class MonochromeSvgIconEngine : public QIconEngine {
QString mFilePath;
QByteArray mSvgContent;
public:
explicit MonochromeSvgIconEngine(const QString& fp) : mFilePath(fp) {}
void paint(QPainter* painter, const QRect& rect, QIcon::Mode mode,
QIcon::State state) override {
Q_UNUSED(state);
if (mSvgContent.isNull() && (!mFilePath.isEmpty())) {
QFile file(mFilePath);
if (file.open(QFile::ReadOnly)) {
mSvgContent = file.readAll();
// Font Awesome icons have no "fill" attribute, so we add it now.
if (!mSvgContent.contains("fill=\"currentColor\"")) {
mSvgContent.replace("<svg", "<svg fill=\"currentColor\"");
}
}
mFilePath.clear();
}
const QPalette palette = qApp->palette();
QColor color = palette.color(QPalette::Text);
if ((mode == QIcon::Mode::Active) || (mode == QIcon::Mode::Selected)) {
color = palette.color(QPalette::HighlightedText);
} else if (mode == QIcon::Mode::Disabled) {
color = palette.color(QPalette::Disabled, QPalette::Text);
}
QByteArray content = mSvgContent;
content.replace("fill=\"currentColor\"",
QString("fill=\"%1\"").arg(color.name()).toUtf8());
QSvgRenderer renderer(content);
const QSize svgSize = renderer.defaultSize().scaled(
rect.width(), rect.height(), Qt::KeepAspectRatio);
const QRectF targetRect(rect.x() + (rect.width() - svgSize.width()) / 2,
rect.y() + (rect.height() - svgSize.height()) / 2,
svgSize.width(), svgSize.height());
renderer.render(painter, targetRect);
}
QPixmap pixmap(const QSize& size, QIcon::Mode mode,
QIcon::State state) override {
QImage img(size, QImage::Format_ARGB32);
img.fill(Qt::transparent);
QPixmap pix = QPixmap::fromImage(img, Qt::NoFormatConversion);
{
QPainter painter(&pix);
painter.setRenderHint(QPainter::Antialiasing);
const QRect rext(QPoint(0, 0), size);
this->paint(&painter, rext, mode, state);
}
return pix;
}
QIconEngine* clone() const override {
return new MonochromeSvgIconEngine(*this);
}
};
/*******************************************************************************
* Static Methods
******************************************************************************/
bool EditorToolbox::isSystemThemeDark() noexcept {
auto detect = []() {
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
switch (qApp->styleHints()->colorScheme()) {
case Qt::ColorScheme::Dark:
return true;
case Qt::ColorScheme::Light:
return false;
default:
qWarning() << "Failed to detect system theme, assuming light.";
return false;
}
#else
QImage image(10, 10, QImage::Format_ARGB32);
image.fill(qApp->palette().color(QPalette::Window)); // Fallback
QWidget widget;
widget.resize(image.size());
widget.render(&image);
const QColor bgColor = image.pixelColor(image.rect().center());
const bool dark = (bgColor.alphaF() > 0.2) && (bgColor.blackF() > 0.5);
qDebug().nospace().noquote()
<< "Detected " << (dark ? "dark" : "light")
<< " theme based on window background color " << bgColor.name() << ".";
return dark;
#endif
};
static bool value = detect();
return value;
}
QString EditorToolbox::toSingleLine(const QString& s) noexcept {
return QString(s).replace("\n", "\\n");
}
QString EditorToolbox::toMultiLine(const QString& s) noexcept {
return s.trimmed().replace("\\n", "\n");
}
QString EditorToolbox::cleanKeywords(const QString& userInput) noexcept {
QStringList list;
for (QString s : userInput.split(",")) {
s = s.toLower().trimmed();
if ((!s.isEmpty()) && (!list.contains(s))) {
list.append(s);
}
}
return list.join(",");
}
QString EditorToolbox::sortableCircuitIdentifier(QString identifier) noexcept {
const int count = identifier.count('!');
identifier.remove('!');
identifier.append(QString(" ").repeated(count));
return identifier;
}
QSet<Point> EditorToolbox::snapCandidatesFromPath(const Path& path) noexcept {
QSet<Point> candidates;
for (int i = 0; i < path.getVertices().count(); ++i) {
const Vertex& v = path.getVertices().at(i);
candidates.insert(v.getPos());
if ((v.getAngle().abs() == Angle::deg180()) &&
(i < (path.getVertices().count() - 1))) {
const Point& p2 = path.getVertices().at(i + 1).getPos();
const Point center = (v.getPos() + p2) / 2;
const Point middle = v.getPos().rotated(v.getAngle() / 2, center);
candidates.insert(center);
candidates.insert(middle);
}
}
return candidates;
}
QSet<Point> EditorToolbox::snapCandidatesFromCircle(
const Point& center, PositiveLength diameter) noexcept {
QSet<Point> candidates;
candidates.insert(center);
candidates.insert(center + Point(0, diameter / 2));
candidates.insert(center + Point(0, -diameter / 2));
candidates.insert(center + Point(diameter / 2, 0));
candidates.insert(center + Point(-diameter / 2, 0));
return candidates;
}
Point EditorToolbox::snapPosition(Point cursorPos, PositiveLength gridInterval,
const QSet<Point>& snapCandidates,
bool* snapped) noexcept {
if (snapCandidates.contains(cursorPos)) {
if (snapped) *snapped = true;
return cursorPos;
} else if (cursorPos.isOnGrid(gridInterval)) {
if (snapped) *snapped = false;
return cursorPos;
}
Point nearestCandidate;
Length nearestDistance(-1);
for (const Point& candidate : snapCandidates) {
const UnsignedLength distance = (cursorPos - candidate).getLength();
if ((nearestDistance < 0) || (distance < nearestDistance)) {
nearestCandidate = candidate;
nearestDistance = *distance;
}
}
const Point posOnGrid = cursorPos.mappedToGrid(gridInterval);
const Length gridDistance = *(cursorPos - posOnGrid).getLength();
if ((nearestDistance >= 0) && (nearestDistance <= gridDistance)) {
cursorPos = nearestCandidate;
if (snapped) *snapped = true;
} else {
cursorPos = posOnGrid;
if (snapped) *snapped = false;
}
return cursorPos;
}
QIcon EditorToolbox::svgIcon(const QString& file) noexcept {
return QIcon(new MonochromeSvgIconEngine(file));
}
void EditorToolbox::removeFormLayoutRow(QLabel& label) noexcept {
if (auto layout = label.parentWidget()->layout()) {
if (removeFormLayoutRow(*layout, label)) {
return;
}
}
qWarning().nospace() << "Failed to remove form layout row "
<< label.objectName() << ".";
}
void EditorToolbox::deleteLayoutItemRecursively(QLayoutItem* item) noexcept {
Q_ASSERT(item);
if (QWidget* widget = item->widget()) {
delete widget;
} else if (QLayout* layout = item->layout()) {
for (int i = layout->count() - 1; i >= 0; --i) {
deleteLayoutItemRecursively(layout->takeAt(i));
}
} else if (QSpacerItem* spacer = item->spacerItem()) {
delete spacer;
}
delete item;
}
bool EditorToolbox::startToolBarTabFocusCycle(
QToolBar& toolBar, QWidget& returnFocusToWidget) noexcept {
QWidget* previousWidget = nullptr;
foreach (QAction* action, toolBar.actions()) {
QWidget* widget = toolBar.widgetForAction(action);
if (widget && (widget->focusPolicy() & Qt::TabFocus)) {
if (!previousWidget) {
widget->setFocus(Qt::TabFocusReason);
} else {
toolBar.setTabOrder(previousWidget, widget);
}
previousWidget = widget;
}
}
if (previousWidget) {
toolBar.setTabOrder(previousWidget, &returnFocusToWidget);
return true;
} else {
return false;
}
}
ResourceList EditorToolbox::getComponentResources(
const WorkspaceLibraryDb& db, const ComponentInstance& cmp,
const std::optional<Uuid>& filterDev) noexcept {
// Helper to skip duplicate URLs.
ResourceList resources;
QSet<QUrl> urls;
auto addResources = [&](const ResourceList& list) {
for (const Resource& res : list) {
if (res.getUrl().isValid() && (!urls.contains(res.getUrl()))) {
resources.append(std::make_shared<Resource>(res));
urls.insert(res.getUrl());
}
}
};
// Helper to catch exceptions and provide fallback resources.
auto tryAddResources = [&](std::function<ResourceList()> getter,
const ResourceList& fallback) {
ResourceList lst;
try {
lst = getter(); // can throw
} catch (const Exception& e) {
qWarning() << "Failed to get resources:" << e.getMsg();
}
addResources(lst.isEmpty() ? fallback : lst);
};
// Get resources of component.
tryAddResources(
[&]() {
return db.getResources<Component>(
db.getLatest<Component>(cmp.getLibComponent().getUuid()));
},
cmp.getLibComponent().getResources());
// Determine relevant devices.
QList<Uuid> devices;
if (filterDev) {
devices.append(*filterDev);
} else {
for (const BI_Device* dev : cmp.getDevices()) {
if (!devices.contains(dev->getLibDevice().getUuid())) {
devices.append(dev->getLibDevice().getUuid());
}
}
for (const ComponentAssemblyOption& option : cmp.getAssemblyOptions()) {
if (!devices.contains(option.getDevice())) {
devices.append(option.getDevice());
}
}
}
// Get resources of devices.
for (const Uuid& uuid : devices) {
const Device* dev =
cmp.getCircuit().getProject().getLibrary().getDevice(uuid);
tryAddResources(
[&]() { return db.getResources<Device>(db.getLatest<Device>(uuid)); },
dev ? dev->getResources() : ResourceList());
}
return resources;
}
void EditorToolbox::addResourcesToMenu(const Workspace& ws, MenuBuilder& mb,
const ComponentInstance& cmp,
const std::optional<Uuid>& filterDev,
QPointer<QWidget> editor,
QMenu& root) noexcept {
// Get all relevant resources.
ResourceList resources =
getComponentResources(ws.getLibraryDb(), cmp, filterDev);
// Limit number of resources.
while (resources.count() > 15) {
resources.remove(resources.count() - 1);
}
// Detect duplicate names.
QStringList names;
for (const auto& res : resources) {
names.append(*res.getName());
}
// Build list of actions.
QList<QAction*> actions;
for (const Resource& res : resources) {
QString name = *res.getName();
if (names.count(name) > 1) {
name += " (" % res.getUrl().fileName() % ")";
}
if (name.length() > 100) {
name = name.left(97) + "…";
}
QAction* a =
new QAction(svgIcon(":/fa/solid/file-pdf.svg"), name % "...", &root);
QObject::connect(a, &QAction::triggered, editor, [&ws, res, editor]() {
DesktopServices::downloadAndOpenResourceAsync(
ws.getSettings(), *res.getName(), res.getMediaType(), res.getUrl(),
editor);
});
actions.append(a);
}
// If MPNs are available, provide search through API.
if (ws.getSettings().getApiEndpointForPartsInfo()) {
QList<Part> parts;
for (const ComponentAssemblyOption& ao : cmp.getAssemblyOptions()) {
for (const Part& part : ao.getParts()) {
std::shared_ptr<PartInformationProvider::PartInformation> info =
PartInformationProvider::instance().getPartInfo(
PartInformationProvider::Part{*part.getMpn(),
*part.getManufacturer()});
if ((!part.getMpn()->isEmpty()) &&
(!part.getManufacturer()->isEmpty()) &&
((!info) || (info->resources.value(0).url.isValid())) &&
(!parts.contains(part)) && (actions.count() < 20)) {
QAction* a = new QAction(
svgIcon(":/fa/solid/magnifying-glass.svg"),
tr("Search datasheet for '%1'").arg(*part.getMpn()) % "...",
&root);
QObject::connect(
a, &QAction::triggered, editor, [&ws, part, editor]() {
searchAndOpenDatasheet(ws, *part.getMpn(),
*part.getManufacturer(), editor);
});
actions.append(a);
parts.append(part);
}
}
}
}
// Add menu items.
if (!actions.isEmpty()) {
mb.addSeparator();
}
const int nRoot = actions.count() > 3 ? 2 : 3;
for (int i = 0; i < std::min(static_cast<int>(actions.count()), nRoot); ++i) {
mb.addAction(actions.at(i));
}
if (actions.count() > nRoot) {
QMenu* sm = mb.addSubMenu(&MenuBuilder::createMoreResourcesMenu);
MenuBuilder smb(sm);
for (int i = nRoot; i < actions.count(); ++i) {
smb.addAction(actions.at(i));
}
}
}
/*******************************************************************************
* Private Methods
******************************************************************************/
bool EditorToolbox::removeFormLayoutRow(QLayout& layout,
QLabel& label) noexcept {
if (auto formLayout = dynamic_cast<QFormLayout*>(&layout)) {
for (int i = 0; i < formLayout->rowCount(); ++i) {
QLayoutItem* labelItem = formLayout->itemAt(i, QFormLayout::LabelRole);
QLayoutItem* fieldItem = formLayout->itemAt(i, QFormLayout::FieldRole);
if ((labelItem) && (labelItem->widget() == &label) && (fieldItem)) {
hideLayoutItem(*labelItem);
hideLayoutItem(*fieldItem);
formLayout->takeRow(i); // Avoid ugly space caused by the empty rows.
return true;
}
}
}
for (int i = 0; i < layout.count(); ++i) {
if (QLayoutItem* item = layout.itemAt(i)) {
if (QLayout* child = item->layout()) {
if (removeFormLayoutRow(*child, label)) {
return true;
}
}
}
}
return false;
}
void EditorToolbox::hideLayoutItem(QLayoutItem& item) noexcept {
if (QWidget* widget = item.widget()) {
widget->hide();
} else if (QLayout* layout = item.layout()) {
for (int i = 0; i < layout->count(); ++i) {
if (QLayoutItem* child = layout->itemAt(i)) {
hideLayoutItem(*child);
}
}
}
}
void EditorToolbox::searchAndOpenDatasheet(const Workspace& ws,
const QString& mpn,
const QString& manufacturer,
QPointer<QWidget> parent) noexcept {
auto openPartDatasheet =
[&ws,
parent](std::shared_ptr<PartInformationProvider::PartInformation> info) {
if (info && (!info->resources.isEmpty()) &&
(info->resources[0].url.isValid())) {
DesktopServices::downloadAndOpenResourceAsync(
ws.getSettings(), info->mpn, info->resources[0].mediaType,
info->resources[0].url, parent);
} else {
QMessageBox::information(
parent, tr("No datasheet found"),
tr("Sorry, no datasheet found for the requested part :-("));
}
};
PartInformationProvider& pip = PartInformationProvider::instance();
const PartInformationProvider::Part pipPart{mpn, manufacturer};
if (auto info = pip.getPartInfo(pipPart)) {
openPartDatasheet(info);
return;
}
QGuiApplication::setOverrideCursor(Qt::WaitCursor);
if ((!pip.isOperational()) && (!pip.startOperation(5000))) {
QGuiApplication::restoreOverrideCursor();
QMessageBox::critical(parent, tr("Error"),
"Sorry, the API server is currently not "
"available. Please try again later.");
return;
}
if (!pip.isOngoing(pipPart)) {
pip.scheduleRequest(pipPart);
}
pip.requestScheduledParts();
auto part = pip.waitForPartInfo(pipPart, 5000);
QGuiApplication::restoreOverrideCursor();
openPartDatasheet(part);
}
/*******************************************************************************
* End of File
******************************************************************************/
} // namespace editor
} // namespace librepcb