Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion App/Element/GraphicElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,6 @@ class GraphicElement : public QGraphicsObject, public ItemWithId
*/
bool simUpdateInputsAllowUnknown();


/**
* \brief Decodes \a count select-line statuses from simInputs() into a binary index.
* \param offset Index of the first select-line input in simInputs().
Expand Down
127 changes: 127 additions & 0 deletions App/Element/IC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "App/Element/IC.h"

#include <QDir>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
#include <QSaveFile>
Expand All @@ -15,6 +16,7 @@
#include "App/Core/Common.h"
#include "App/Element/ElementFactory.h"
#include "App/Element/ElementInfo.h"
#include "App/Element/ICPreviewPopup.h"
#include "App/Element/ICRegistry.h"
#include "App/IO/Serialization.h"
#include "App/IO/SerializationContext.h"
Expand All @@ -23,9 +25,21 @@
#include "App/Nodes/QNEPort.h"
#include "App/Scene/Scene.h"
#include "App/Simulation/Simulation.h"
#include "App/UI/MainWindow.h"

namespace {

// Returns the shared IC preview popup, or nullptr if the chain isn't fully
// alive (e.g. during early init or late teardown). The popup is created
// eagerly by MainWindow's constructor; null returns are exclusively a
// teardown-safety concern.
ICPreviewPopup *icPreviewPopup()
{
auto *app = Application::instance();
auto *mw = app ? app->mainWindow() : nullptr;
return mw ? mw->icPreviewPopup() : nullptr;
}

bool comparePorts(QNEPort *port1, QNEPort *port2)
{
auto *elem1 = port1->graphicElement();
Expand Down Expand Up @@ -112,10 +126,20 @@ IC::IC(QGraphicsItem *parent)
// ICs display their label vertically alongside the chip body, matching the physical
// convention of reading IC labels along the side of a physical DIP package
m_label->setRotation(90);

// Enable hover events so the preview popup can be shown/hidden
setAcceptHoverEvents(true);
}

IC::~IC()
Comment thread
darktorres marked this conversation as resolved.
{
// If this IC is destroyed while the shared popup is pending or showing it,
// cancel immediately — m_pendingIC is a QPointer so it auto-nulls, but the
// popup could still be visible with stale content.
if (auto *popup = icPreviewPopup(); popup && popup->pendingIC() == this) {
popup->cancelHide();
popup->hide();
}
qDeleteAll(m_internalConnections);
qDeleteAll(m_internalElements);
}
Expand Down Expand Up @@ -412,6 +436,12 @@ void IC::migrateFile(const QFileInfo &fileInfo, const QList<QGraphicsItem *> &it

void IC::processLoadedItems(const QList<QGraphicsItem *> &items)
{
// Snapshot the preview now, while the original Input/Output elements (buttons,
// switches, LEDs, …) are still alive in `items`. loadBoundaryElement() below
// replaces each of them with a proxy Node, so a later render would only see
// the simulation graph.
generatePreviewPixmap(items);

for (auto *item : items) {
if (auto *conn = qgraphicsitem_cast<QNEConnection *>(item)) {
m_internalConnections.append(conn);
Expand Down Expand Up @@ -537,10 +567,107 @@ void IC::generatePixmap()

void IC::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
// Hide the preview popup immediately on double-click so it doesn't overlap the sub-circuit tab
if (auto *popup = icPreviewPopup()) {
popup->cancelHide();
popup->hide();
}
event->accept();
emit requestOpenSubCircuit(id(), m_blobName, m_file);
}

// --- Hover preview ---

void IC::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
if (auto *popup = icPreviewPopup()) {
popup->showForIC(this, event->screenPos());
}
}

void IC::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
{
if (auto *popup = icPreviewPopup()) {
popup->updatePendingPos(event->screenPos());
}
}

void IC::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
{
Q_UNUSED(event)
if (auto *popup = icPreviewPopup()) {
popup->scheduleHide();
}
}

void IC::generatePreviewPixmap(const QList<QGraphicsItem *> &items)
{
// Split the freshly-deserialized items into elements and connections. The
// boundary Input/Output elements are still in their designed form here; the
// substitution to proxy Nodes happens later in processLoadedItems().
QVector<GraphicElement *> elements;
QVector<QNEConnection *> connections;
elements.reserve(items.size());
connections.reserve(items.size());
for (auto *item : items) {
if (auto *conn = qgraphicsitem_cast<QNEConnection *>(item)) {
connections.append(conn);
} else if (auto *elm = qgraphicsitem_cast<GraphicElement *>(item)) {
elements.append(elm);
}
}

// Skip for empty or very large circuits.
if (elements.isEmpty() || elements.size() > ICPreviewPopup::MaxElementCount) {
m_previewPixmap = QPixmap();
return;
}

// Temporarily borrow the items into a throwaway scene so QGraphicsScene::render()
// can be used without disturbing the real scene. The scope guard guarantees
// cleanup even if render() throws.
QGraphicsScene tempScene;
tempScene.setBackgroundBrush(QColor(42, 42, 42));

auto cleanup = qScopeGuard([&] {
for (auto *elm : std::as_const(elements)) { tempScene.removeItem(elm); }
for (auto *conn : std::as_const(connections)) { tempScene.removeItem(conn); }
});

for (auto *elm : std::as_const(elements)) {
tempScene.addItem(elm);
}
for (auto *conn : std::as_const(connections)) {
tempScene.addItem(conn);
}

// Compute the bounding rect with some padding
const QRectF sourceRect = tempScene.itemsBoundingRect().adjusted(-16, -16, 16, 16);

// Scale to fit within max preview dimensions while preserving aspect ratio
QSize targetSize = sourceRect.size().toSize();
targetSize.scale(ICPreviewPopup::MaxWidth, ICPreviewPopup::MaxHeight, Qt::KeepAspectRatio);

if (targetSize.isEmpty()) {
m_previewPixmap = QPixmap();
return;
}

// QPixmap(QSize) is uninitialised; tempScene.render() paints the background
// brush over the source→target affine, but subpixel rounding can leave a
// 1-pixel sliver unpainted at the right/bottom edge, exposing whatever was
// in memory (commonly white on Windows). Fill explicitly to avoid that.
QPixmap preview(targetSize);
preview.fill(QColor(42, 42, 42));

QPainter painter(&preview);
painter.setRenderHint(QPainter::Antialiasing);
tempScene.render(&painter, QRectF(), sourceRect);
painter.end();

m_previewPixmap = preview;
}

QRectF IC::boundingRect() const
{
return portsBoundingRect().united(QRectF(0, 0, 64, 64));
Expand Down
23 changes: 23 additions & 0 deletions App/Element/IC.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
#pragma once

#include <QFileInfo>
#include <QPixmap>
#include <QSet>

#include "App/Element/GraphicElement.h"
#include "App/IO/SerializationContext.h"

class QGraphicsSceneHoverEvent;
class QNEConnection;

/**
Expand Down Expand Up @@ -66,6 +68,12 @@ class IC : public GraphicElement
const QVector<QNEPort *> &internalInputs() const { return m_internalInputs; }
const QVector<QNEPort *> &internalOutputs() const { return m_internalOutputs; }

/// Returns the cached preview pixmap of the designed circuit.
/// The snapshot is taken once during load (before boundary elements are
/// substituted with proxy Nodes), so the preview reflects what the user
/// actually placed — buttons, switches, LEDs — not the simulation graph.
const QPixmap &previewPixmap() const { return m_previewPixmap; }

// --- Port metadata ---

/// Port count and label metadata extracted from Input/Output elements.
Expand Down Expand Up @@ -112,6 +120,16 @@ class IC : public GraphicElement
/// \reimp Opens the IC sub-circuit for editing on double-click.
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;

/// \reimp Schedules a floating preview of the internal circuit after a short hover delay.
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;

/// \reimp Updates the pending preview position so the popup appears near
/// the current cursor when the show delay elapses, not at the entry point.
void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override;

/// \reimp Dismisses the floating preview with a short delay.
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;

private:
// --- Utility methods ---

Expand All @@ -130,6 +148,7 @@ class IC : public GraphicElement
// --- Visual helpers ---

void generatePixmap();
void generatePreviewPixmap(const QList<QGraphicsItem *> &items);

// --- Members ---

Expand All @@ -145,5 +164,9 @@ class IC : public GraphicElement
QVector<GraphicElement *> m_sortedInternalElements;
QSet<GraphicElement *> m_boundaryInputElements;
bool m_internalHasFeedback = false;

// --- Members: Hover preview ---

QPixmap m_previewPixmap; ///< Snapshot of the designed circuit, rendered at load time.
};

133 changes: 133 additions & 0 deletions App/Element/ICPreviewPopup.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
// SPDX-License-Identifier: GPL-3.0-or-later

#include "App/Element/ICPreviewPopup.h"

#include <QGuiApplication>
#include <QScreen>
#include <QVBoxLayout>

#include "App/Element/IC.h"

ICPreviewPopup::ICPreviewPopup(QWidget *parent)
: QWidget(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{
setAttribute(Qt::WA_TranslucentBackground);
Comment thread
darktorres marked this conversation as resolved.
setAttribute(Qt::WA_NoSystemBackground); // Prevent solid black fill on Windows
setAttribute(Qt::WA_ShowWithoutActivating);

auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(4, 4, 4, 4);

m_imageLabel = new QLabel(this);
m_imageLabel->setObjectName(QStringLiteral("preview"));
m_imageLabel->setAlignment(Qt::AlignCenter);
layout->addWidget(m_imageLabel);

// Outer popup chrome + an inset frame around the preview pixmap so the
// rendered circuit reads as a framed image rather than floating content.
// The QLabel selector is scoped by objectName so future labels added to
// the popup don't silently inherit the inset frame styling.
setStyleSheet(
"ICPreviewPopup {"
" background-color: rgba(30, 30, 30, 230);"
" border: 1px solid rgba(120, 120, 120, 180);"
" border-radius: 6px;"
"}"
"QLabel#preview {"
" border: 1px solid rgba(170, 170, 170, 200);"
" border-radius: 3px;"
" padding: 6px;"
" background-color: rgba(15, 15, 15, 200);"
"}"
);

m_hideTimer.setSingleShot(true);
m_hideTimer.setInterval(300);
connect(&m_hideTimer, &QTimer::timeout, this, &QWidget::hide);

m_showTimer.setSingleShot(true);
m_showTimer.setInterval(1000);
connect(&m_showTimer, &QTimer::timeout, this, &ICPreviewPopup::executeShow);
}
Comment thread
darktorres marked this conversation as resolved.

void ICPreviewPopup::showForIC(IC *ic, const QPoint &screenPos)
{
cancelHide();

if (!ic) {
return;
}

m_pendingIC = ic;
m_pendingPos = screenPos;

if (isVisible()) {
// If the popup is already visible (e.g., moved quickly from another IC),
// update it immediately without a delay.
executeShow();
} else {
m_showTimer.start();
}
}

void ICPreviewPopup::executeShow()
{
if (!m_pendingIC) {
return;
}

// The pixmap is the single source of truth: it's null when the IC was
// empty or oversized at load time, or when generation otherwise failed.
const QPixmap &preview = m_pendingIC->previewPixmap();
if (preview.isNull()) {
hide();
return;
}

m_imageLabel->setPixmap(preview);
adjustSize();

// Position slightly offset from the cursor, then clamp to the available
// screen geometry so the popup never extends off-screen.
QPoint pos = m_pendingPos + QPoint(16, 16);
if (const auto *screen = QGuiApplication::screenAt(pos)) {
const QRect avail = screen->availableGeometry();
pos.setX(qMin(pos.x(), avail.right() - width()));
pos.setY(qMin(pos.y(), avail.bottom() - height()));
}
move(pos);
show();
}

void ICPreviewPopup::scheduleHide()
{
m_showTimer.stop();
m_hideTimer.start();
}

void ICPreviewPopup::cancelHide()
{
m_hideTimer.stop();
m_showTimer.stop();
}

void ICPreviewPopup::updatePendingPos(const QPoint &screenPos)
{
if (!isVisible()) {
m_pendingPos = screenPos;
}
}

void ICPreviewPopup::enterEvent(QEnterEvent *event)
{
Q_UNUSED(event)
cancelHide();
}

void ICPreviewPopup::leaveEvent(QEvent *event)
{
Q_UNUSED(event)
scheduleHide();
}

Loading
Loading