From d397717ff7bbe4e52aaadf7506211fe71e97b583 Mon Sep 17 00:00:00 2001 From: leonarski_f Date: Fri, 25 Sep 2026 13:47:28 +0200 Subject: [PATCH] Viewer: dock the magnifier, fixed zoom levels, Shift-driven with a frame The magnifier moves from a helper window into its own dock below the inspector. It has three fixed zoom levels (x64 and x32 with the pixel values written on the pixels, x10 without) instead of wheel zoom, and follows the cursor only while Shift is held over the diffraction image. Meanwhile the diffraction view draws a frame around the area it covers, just outside that area so it stays visible at low zoom without hiding the pixels; releasing Shift (seen by the application-wide key filter, so wherever the focus is), moving without it or leaving the image hides it. A "Pop out" button moves the magnifier into a separate window and back. Qt's own dock floating stays disabled: a floated dock is placed off-screen on WSLg. Whether it was popped out, and the window geometry, persist across sessions. The inspector's image statistics become a collapsible section so a small screen can give the space to the magnifier. kLayoutVersion is bumped for the new dock. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/CHANGELOG.md | 1 + docs/JFJOCH_VIEWER.md | 9 +-- viewer/CMakeLists.txt | 4 +- viewer/JFJochViewerSidePanel.cpp | 12 ++-- viewer/JFJochViewerWindow.cpp | 66 ++++++++++++++--- viewer/JFJochViewerWindow.h | 6 ++ .../image_viewer/JFJochDiffractionImage.cpp | 43 +++++++++++- viewer/image_viewer/JFJochDiffractionImage.h | 9 +++ viewer/image_viewer/JFJochFollowerImage.cpp | 29 ++++++-- viewer/image_viewer/JFJochFollowerImage.h | 11 ++- viewer/image_viewer/JFJochImage.cpp | 1 - viewer/image_viewer/JFJochImage.h | 1 - viewer/widgets/JFJochViewerMagnifier.cpp | 70 +++++++++++++++++++ viewer/widgets/JFJochViewerMagnifier.h | 35 ++++++++++ viewer/windows/JFJochMagnifierWindow.cpp | 40 ----------- viewer/windows/JFJochMagnifierWindow.h | 41 ----------- viewer/windows/JFJochMouseShortcutsWindow.cpp | 2 +- 17 files changed, 267 insertions(+), 113 deletions(-) create mode 100644 viewer/widgets/JFJochViewerMagnifier.cpp create mode 100644 viewer/widgets/JFJochViewerMagnifier.h delete mode 100644 viewer/windows/JFJochMagnifierWindow.cpp delete mode 100644 viewer/windows/JFJochMagnifierWindow.h diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4d51dcdf9..7e4820de0 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -18,6 +18,7 @@ * Rugnux keeps refining the detector geometry between passes where the fit's new geometry puts more validation spots on the lattice, not only where the held-out residual falls. * Rugnux also reports `R_MEAS_WEIGHTED`, the R_meas with each observation weighted as the merge weights it, so weak frames kept in the merge at low weight can be told from real disagreement; `R_MEAS` is unchanged. * Rugnux no longer stops per-frame scaling as unsettled because of a single frame that is about to be dropped as blank. +* The viewer's magnifier is a panel below the inspector (or, popped out, a window of its own) with three fixed zoom levels (×64, ×32, ×10), follows the cursor only while Shift is held, and a frame on the image shows the area it covers; the inspector's image statistics can now be folded. * `rugnux --polarization` is documented as the polarization degree (XDS `FRACTION_OF_POLARIZATION` = (1 + p)/2); the default 0.99 suits undulators. ### 1.0.0-rc.172 diff --git a/docs/JFJOCH_VIEWER.md b/docs/JFJOCH_VIEWER.md index bd2ff816d..8695453fd 100644 --- a/docs/JFJOCH_VIEWER.md +++ b/docs/JFJOCH_VIEWER.md @@ -56,9 +56,10 @@ install it. Bragg integration, scaling, powder calibration and a reference dataset — the same settings the CLI takes. - Auxiliary windows: image list, dataset metadata, spot list, reflection list, - 2D azimuthal-integration image, calibration-image viewer and a magnifier; plus the - *Inspector* (per-image statistics, image features, resolution rings, ROI statistics) and - dataset-info charts. + 2D azimuthal-integration image and calibration-image viewer; plus the + *Inspector* (per-image statistics, image features, resolution rings, ROI statistics), the + *Magnifier* below it (three zoom levels: ×64 and ×32 with the pixel values written on the pixels, + ×10 without; *Pop out* moves it to a window of its own) and dataset-info charts. - The *Inspector*'s **Image features** section decides what the overlay draws — spots, predictions, saturated and highest pixels, the beam stop — including whether the non-indexed spots and the spots that fall on an ice ring are drawn at all. @@ -116,6 +117,7 @@ The same list is available in the application under **Help ▸ Mouse Shortcuts** | `Page Up` / `Page Down` | Step one image forward / back | | Hover | Status bar shows the pixel position, its value and the resolution | | Drag | Pan the image | +| `Shift` + move | Move the magnifier panel to the cursor; a frame shows the area it covers | | `Shift` + drag | Draw a rectangular ROI | | `Shift` + `Ctrl` + drag | Draw a circular ROI | | Drag an ROI or its handle | Move or resize the selected ROI | @@ -138,7 +140,6 @@ The same list is available in the application under **Help ▸ Mouse Shortcuts** | Dataset-info plot: `Shift` + hover | Load the hovered image | | Spot / reflection list: double click | Zoom the diffraction image on that spot or prediction | | Image list: double click | Load that image | -| Magnifier: wheel | Zoom the magnifier; it follows the cursor on the main image | ## Opening data diff --git a/viewer/CMakeLists.txt b/viewer/CMakeLists.txt index 7431dd674..8affac6ea 100644 --- a/viewer/CMakeLists.txt +++ b/viewer/CMakeLists.txt @@ -60,6 +60,8 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView windows/JFJochMergeStatsWindow.h windows/JFJochCalibrationResultWindow.cpp windows/JFJochCalibrationResultWindow.h + widgets/JFJochViewerMagnifier.cpp + widgets/JFJochViewerMagnifier.h widgets/NumberLineEdit.cpp widgets/NumberLineEdit.h widgets/SliderPlusBox.cpp @@ -112,8 +114,6 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView widgets/PowderCalibrationWidget.cpp widgets/PowderCalibrationWidget.h ${APP_RESOURCES} - windows/JFJochMagnifierWindow.cpp - windows/JFJochMagnifierWindow.h windows/JFJochProcessingJobsWindow.cpp windows/JFJochProcessingJobsWindow.h ) diff --git a/viewer/JFJochViewerSidePanel.cpp b/viewer/JFJochViewerSidePanel.cpp index c92e0830f..4fc7aeee4 100644 --- a/viewer/JFJochViewerSidePanel.cpp +++ b/viewer/JFJochViewerSidePanel.cpp @@ -8,7 +8,6 @@ #include "JFJochViewerSidePanel.h" -#include "widgets/TitleLabel.h" #include "widgets/CollapsibleSection.h" #include "widgets/JFJochViewerImageStatistics.h" @@ -26,10 +25,15 @@ JFJochViewerSidePanel::JFJochViewerSidePanel(QWidget *parent) : QWidget(parent) auto layout = new QVBoxLayout(this); - layout->addWidget(new TitleLabel("Image statistics", this)); - + // Open at start, but foldable, so a small screen can give its space to the magnifier below + auto *statsSection = new CollapsibleSection("Image statistics", this); + auto *statsLayout = new QVBoxLayout(); + statsLayout->setContentsMargins(0, 0, 0, 0); auto stats = new JFJochViewerImageStatistics(this); - layout->addWidget(stats); + statsLayout->addWidget(stats); + statsSection->setContentLayout(statsLayout); + statsSection->setExpanded(true); + layout->addWidget(statsSection); connect(this, &JFJochViewerSidePanel::imageLoaded, stats, &JFJochViewerImageStatistics::loadImage); auto *featuresSection = new CollapsibleSection("Image features", this); diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index 7974c439c..0087d63f7 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -48,7 +48,8 @@ #include "toolbar/JFJochViewerToolbarDisplay.h" #include "toolbar/JFJochViewerToolbarImage.h" #include "windows/JFJoch2DAzintImageWindow.h" -#include "windows/JFJochMagnifierWindow.h" +#include "widgets/JFJochViewerMagnifier.h" +#include "windows/JFJochHelperWindow.h" #include "image_viewer/JFJochImage.h" #include "image_viewer/JFJochSimpleImage.h" #include @@ -57,7 +58,7 @@ // Dock-layout version for saveState/restoreState: bump whenever the dock set / structure changes so a // stale saved layout (which can restore a dock as a broken, non-responsive floating window) is rejected // and the default layout applies instead. -static constexpr int kLayoutVersion = 4; +static constexpr int kLayoutVersion = 5; JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString &file) : QMainWindow(parent) { image_tail_timer_ = new QTimer(this); @@ -147,6 +148,35 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString addDockWidget(Qt::RightDockWidgetArea, inspectorDock); menuBar->AddDockEntry(inspectorDock, "Inspector"); + // The magnifier sits under the inspector, whose sections are usually folded, and takes the rest + // of the column; as its own dock it can be moved elsewhere or closed. + magnifier = new JFJochViewerMagnifier(this); + magnifierDock = new QDockWidget("Magnifier", this); + magnifierDock->setObjectName("magnifierDock"); + magnifierDock->setAllowedAreas(Qt::AllDockWidgetAreas); + magnifierDock->setWidget(magnifier); + addDockWidget(Qt::RightDockWidgetArea, magnifierDock); + splitDockWidget(inspectorDock, magnifierDock, Qt::Vertical); + resizeDocks({inspectorDock, magnifierDock}, {600, 400}, Qt::Vertical); + menuBar->AddDockEntry(magnifierDock, "Magnifier"); + + magnifierWindow = new JFJochHelperWindow(this); + magnifierWindow->setWindowTitle("Magnifier"); + magnifierWindow->resize(500, 500); + connect(magnifier, &JFJochViewerMagnifier::popOutClicked, this, [this] { + if (magnifierWindow->isVisible()) + magnifierWindow->close(); // docks it back, below + else + PopOutMagnifier(); + }); + connect(magnifierWindow, &JFJochHelperWindow::closing, this, [this] { + if (!magnifierWindow->centralWidget()) + return; // closed while not popped out (a settings reset): the magnifier is in its dock + magnifierDock->setWidget(magnifierWindow->takeCentralWidget()); + magnifierDock->show(); + magnifier->setPoppedOut(false); + }); + reading_worker = new JFJochImageReadingWorker(spot_finding_settings, experiment); reading_thread = new QThread(this); reading_worker->moveToThread(reading_thread); @@ -159,7 +189,6 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString auto calibrationWindow = new JFJochCalibrationWindow(this); auto azintImageWindow = new JFJoch2DAzintImageWindow(this); - auto magnifierWindow = new JFJochMagnifierWindow(this); processingJobsWindow = new JFJochProcessingJobsWindow(reading_worker, this); @@ -172,7 +201,6 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString menuBar->AddWindowEntry(metadataWindow, "Image metadata"); menuBar->AddWindowEntry(calibrationWindow, "Calibration image viewer"); menuBar->AddWindowEntry(azintImageWindow, "Azimuthal integration 2D image"); - menuBar->AddWindowEntry(magnifierWindow, "Magnifier"); // processingJobsWindow is docked (bottom, next to the plots), not a standalone window - see below. #ifdef JFJOCH_VIEWER_DBUS @@ -423,15 +451,17 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString // --- Magnifier --- // The magnifier shows the frame the main view has already rendered - the same pixels with // its own zoom and centre - so it neither converts nor colours anything itself. - connect(viewer, &JFJochImage::frameRendered, magnifierWindow, [this, magnifierWindow] { - magnifierWindow->setFrame(viewer->Frame()); + connect(viewer, &JFJochImage::frameRendered, magnifier, [this] { + magnifier->setFrame(viewer->Frame()); }); // ... and the raw counts behind it, for the per-pixel labels. Stores a pointer, nothing more. connect(this, &JFJochViewerWindow::imageReady, - magnifierWindow, &JFJochHelperWindow::imageLoaded); - connect(viewer, &JFJochImage::hoverScenePos, - magnifierWindow, &JFJochMagnifierWindow::centerAt); + magnifier, &JFJochViewerMagnifier::setPixelValues); + connect(viewer, &JFJochDiffractionImage::magnifierMoved, + magnifier, &JFJochViewerMagnifier::centerAt); + connect(magnifier, &JFJochViewerMagnifier::fieldChanged, + viewer, &JFJochDiffractionImage::setMagnifierField); // Ensure worker is deleted in its own thread when the thread stops connect(reading_thread, &QThread::finished, reading_worker, &QObject::deleteLater); @@ -609,6 +639,10 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString QSettings settings("PSI", "jfjoch_viewer"); restoreGeometry(settings.value("geometry").toByteArray()); restoreState(settings.value("windowState").toByteArray(), kLayoutVersion); + magnifierWindow->restoreGeometry(settings.value("magnifierGeometry").toByteArray()); + // Once the main window is up: opened from here, the window would end up behind it + if (settings.value("magnifierPoppedOut", false).toBool()) + QTimer::singleShot(0, this, &JFJochViewerWindow::PopOutMagnifier); // Display settings (color map, Auto, HDR) persist across sessions the same way the layout does. const int colorMap = settings.value("colorMap", static_cast(ColorScaleEnum::Indigo)).toInt(); @@ -660,6 +694,8 @@ void JFJochViewerWindow::ResetAllSettings() { // Bring the running session to the state a first start builds, so normal saving on close is // correct again afterwards. Same calls as the constructor, in the same order. + magnifierWindow->close(); // docks the magnifier back, if it was popped out + magnifierWindow->resize(500, 500); if (!defaultLayoutState.isEmpty()) restoreState(defaultLayoutState, kLayoutVersion); showNormal(); @@ -702,12 +738,21 @@ void JFJochViewerWindow::closeEvent(QCloseEvent *event) { QSettings settings("PSI", "jfjoch_viewer"); settings.setValue("geometry", saveGeometry()); settings.setValue("windowState", saveState(kLayoutVersion)); + settings.setValue("magnifierGeometry", magnifierWindow->saveGeometry()); + settings.setValue("magnifierPoppedOut", magnifierWindow->isVisible()); settings.setValue("colorMap", toolBarDisplay->colorMap()); settings.setValue("autoForeground", toolBarDisplay->autoForeground()); settings.setValue("hdrMode", toolBarDisplay->hdrMode()); QMainWindow::closeEvent(event); } +void JFJochViewerWindow::PopOutMagnifier() { + magnifierWindow->setCentralWidget(magnifier); + magnifierDock->hide(); + magnifier->setPoppedOut(true); + magnifierWindow->open(); +} + void JFJochViewerWindow::LoadFile(const QString &filename, qint64 image_number, qint64 summation, bool retry) { emit LoadFileRequest(filename, image_number, summation, true); FollowInFileBrowser(filename); @@ -811,6 +856,9 @@ bool JFJochViewerWindow::eventFilter(QObject *obj, QEvent *event) { && QApplication::activeWindow() == this && !QApplication::activePopupWidget()) { const auto *ke = static_cast(event); const int key = ke->key(); + // Shift is released wherever the focus is; the magnifier frame must not outlive it + if (type == QEvent::KeyRelease && key == Qt::Key_Shift) + viewer->hideMagnifierFrame(); QWidget *focus = QApplication::focusWidget(); const bool editing = qobject_cast(focus) || qobject_cast(focus) || qobject_cast(focus) || qobject_cast(focus) diff --git a/viewer/JFJochViewerWindow.h b/viewer/JFJochViewerWindow.h index 86cabdb34..7e9ecfbbd 100644 --- a/viewer/JFJochViewerWindow.h +++ b/viewer/JFJochViewerWindow.h @@ -17,6 +17,8 @@ class JFJochProcessingJobsWindow; class JFJochDiffractionImage; +class JFJochHelperWindow; +class JFJochViewerMagnifier; class QDockWidget; class QScrollArea; @@ -56,6 +58,10 @@ private: QDockWidget *settingsDock = nullptr; // inline MX/AzInt settings panel QDockWidget *fileBrowserDock = nullptr; // data-root directory tree, tabbed with settingsDock QDockWidget *processingDock = nullptr; // processing jobs panel + JFJochViewerMagnifier *magnifier = nullptr; + QDockWidget *magnifierDock = nullptr; // where the magnifier normally sits + JFJochHelperWindow *magnifierWindow = nullptr; // ... and its window when popped out + void PopOutMagnifier(); QByteArray defaultLayoutState; // captured after construction, for "Reset layout" int datasetInfoCounter = 0; // gives each dataset-info dock a unique objectName diff --git a/viewer/image_viewer/JFJochDiffractionImage.cpp b/viewer/image_viewer/JFJochDiffractionImage.cpp index 221e0d6d3..6610cf330 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.cpp +++ b/viewer/image_viewer/JFJochDiffractionImage.cpp @@ -79,7 +79,14 @@ void JFJochDiffractionImage::azimuthalHandles(const ROIAzimuthal &az, const Diff phimax = pt(d_mid, phi1); } -void JFJochDiffractionImage::mouseHover(const QPointF &coord, Qt::KeyboardModifiers) { +void JFJochDiffractionImage::mouseHover(const QPointF &coord, Qt::KeyboardModifiers modifiers) { + // Shift + a button is ROI drawing, not magnifying + if ((modifiers & Qt::ShiftModifier) && mouse_event_type == MouseEventType::None) { + emit magnifierMoved(coord); + SetMagnifierFrame(QRectF(coord - QPointF(magnifier_field_.width(), magnifier_field_.height()) / 2, + magnifier_field_)); + } else + SetMagnifierFrame(QRectF()); if (image && (coord.x() >= 0) && (coord.x() < image->Dataset().experiment.GetXPixelsNum()) @@ -1144,6 +1151,17 @@ QString JFJochDiffractionImage::PixelLabel(int x, int y) const { void JFJochDiffractionImage::drawForeground(QPainter *painter, const QRectF &rect) { JFJochImage::drawForeground(painter, rect); + if (!magnifier_frame_.isEmpty()) { + // In viewport pixels, just outside the covered area: at low zoom the area is only a few + // screen pixels, and a line over it would hide exactly what the frame points at. + painter->save(); + painter->resetTransform(); + painter->setPen(QPen(feature_color, 2, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin)); + painter->setBrush(Qt::NoBrush); + painter->drawRect(QRectF(mapFromScene(magnifier_frame_).boundingRect()).adjusted(-1.5, -1.5, 1.5, 1.5)); + painter->restore(); + } + const QString label = HoverResolutionLabel(); if (label.isEmpty()) return; @@ -1156,6 +1174,28 @@ void JFJochDiffractionImage::drawForeground(QPainter *painter, const QRectF &rec painter->restore(); } +void JFJochDiffractionImage::setMagnifierField(QSizeF size) { + magnifier_field_ = size; +} + +void JFJochDiffractionImage::hideMagnifierFrame() { + SetMagnifierFrame(QRectF()); +} + +void JFJochDiffractionImage::SetMagnifierFrame(const QRectF &r) { + // Repaint only the outlines, old and new: the frame can cover much of a zoomed-in view + QRegion dirty; + for (const QRectF &f : {magnifier_frame_, r}) { + if (f.isEmpty()) + continue; + const QRect v = mapFromScene(f).boundingRect(); + dirty += QRegion(v.adjusted(-4, -4, 4, 4)).subtracted(QRegion(v.adjusted(2, 2, -2, -2))); + } + magnifier_frame_ = r; + if (!dirty.isEmpty()) + viewport()->update(dirty); +} + void JFJochDiffractionImage::scrollContentsBy(int dx, int dy) { JFJochImage::scrollContentsBy(dx, dy); if (hover_text_rect_.isEmpty()) @@ -1191,5 +1231,6 @@ void JFJochDiffractionImage::leaveEvent(QEvent *event) { hover_resolution = NAN; DrawResolutionText(); } + SetMagnifierFrame(QRectF()); JFJochImage::leaveEvent(event); } \ No newline at end of file diff --git a/viewer/image_viewer/JFJochDiffractionImage.h b/viewer/image_viewer/JFJochDiffractionImage.h index 778b1b579..ec3459279 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.h +++ b/viewer/image_viewer/JFJochDiffractionImage.h @@ -133,7 +133,14 @@ private: void mouseHover(const QPointF &scenePos, Qt::KeyboardModifiers modifiers) override; + // The magnifier follows the cursor only while Shift is held, and meanwhile a frame here + // shows the area it covers. magnifier_frame_ is empty while the frame is hidden. + QSizeF magnifier_field_; + QRectF magnifier_frame_; + void SetMagnifierFrame(const QRectF &r); + signals: + void magnifierMoved(QPointF scenePos); void roiGeometryEdited(ROIDefinition rois); void roiSelected(QString name); // user picked an ROI by clicking it on the image public slots: @@ -164,5 +171,7 @@ public slots: void hideIceRingSpots(bool input); void outlineSpots(bool input); void setHDRMode(bool input); + void setMagnifierField(QSizeF size); + void hideMagnifierFrame(); }; diff --git a/viewer/image_viewer/JFJochFollowerImage.cpp b/viewer/image_viewer/JFJochFollowerImage.cpp index 48e6041ce..9b1094acd 100644 --- a/viewer/image_viewer/JFJochFollowerImage.cpp +++ b/viewer/image_viewer/JFJochFollowerImage.cpp @@ -56,6 +56,8 @@ void JFJochFollowerImage::SetFrame(std::shared_ptr frame) { frame_size_ = frame_->size(); item_->refresh(); scene()->setSceneRect(0, 0, frame_size_.width(), frame_size_.height()); + // Until the user has pointed somewhere, show the middle rather than the top-left corner + centerOn(center_set_ ? center_ : scene()->sceneRect().center()); } viewport()->update(); @@ -66,25 +68,38 @@ void JFJochFollowerImage::SetPixelValues(std::shared_ptrisNull()) return; centerOn(scenePos); } -void JFJochFollowerImage::wheelEvent(QWheelEvent *event) { - constexpr double step = 1.15; - zoom_ *= (event->angleDelta().y() > 0) ? step : 1.0 / step; - zoom_ = std::clamp(zoom_, 1.0, 200.0); - +void JFJochFollowerImage::SetZoom(double zoom, int label_font_px) { const QPointF center = mapToScene(viewport()->rect().center()); + zoom_ = zoom; + label_font_px_ = label_font_px; setTransform(QTransform::fromScale(zoom_, zoom_)); centerOn(center); + emit fieldChanged(QSizeF(viewport()->width() / zoom_, viewport()->height() / zoom_)); +} + +void JFJochFollowerImage::wheelEvent(QWheelEvent *event) { + // Zoom comes in fixed levels; let the wheel scroll the panel the view sits in instead + event->ignore(); +} + +void JFJochFollowerImage::resizeEvent(QResizeEvent *event) { + QGraphicsView::resizeEvent(event); + if (center_set_) + centerOn(center_); + emit fieldChanged(QSizeF(viewport()->width() / zoom_, viewport()->height() / zoom_)); } void JFJochFollowerImage::drawForeground(QPainter *painter, const QRectF &rect) { QGraphicsView::drawForeground(painter, rect); - if (zoom_ < kLabelZoom || !values_ || !frame_ || frame_->isNull()) + if (label_font_px_ == 0 || !values_ || !frame_ || frame_->isNull()) return; const int W = frame_->width(); @@ -108,7 +123,7 @@ void JFJochFollowerImage::drawForeground(QPainter *painter, const QRectF &rect) QFont font("DejaVu Sans Mono"); font.setStyleHint(QFont::TypeWriter); - font.setPixelSize(std::clamp(static_cast(zoom_ * 0.3), 7, 16)); + font.setPixelSize(label_font_px_); painter->setFont(font); for (int y = y0; y < y1; y++) { diff --git a/viewer/image_viewer/JFJochFollowerImage.h b/viewer/image_viewer/JFJochFollowerImage.h index bcafb9901..5eab0ef80 100644 --- a/viewer/image_viewer/JFJochFollowerImage.h +++ b/viewer/image_viewer/JFJochFollowerImage.h @@ -24,8 +24,6 @@ class JFJochImageItem; class JFJochFollowerImage : public QGraphicsView { Q_OBJECT - // Per-pixel counts are only legible once a detector pixel is a few tens of screen pixels - static constexpr double kLabelZoom = 30.0; static constexpr int kMaxLabels = 2000; JFJochImageItem *item_ = nullptr; @@ -33,8 +31,12 @@ class JFJochFollowerImage : public QGraphicsView { QSize frame_size_; // size the scene rect was set from std::shared_ptr values_; double zoom_ = 12.0; + int label_font_px_ = 0; // 0 = no per-pixel labels + QPointF center_; + bool center_set_ = false; void wheelEvent(QWheelEvent *event) override; + void resizeEvent(QResizeEvent *event) override; void drawForeground(QPainter *painter, const QRectF &rect) override; public: explicit JFJochFollowerImage(QWidget *parent = nullptr); @@ -42,4 +44,9 @@ public: void SetFrame(std::shared_ptr frame); void SetPixelValues(std::shared_ptr image); void CenterAt(QPointF scenePos); + // Screen pixels per detector pixel, and the pixel size of the value labels (0 = none) + void SetZoom(double zoom, int label_font_px); +signals: + // The area of the image the view shows, in detector pixels + void fieldChanged(QSizeF size); }; diff --git a/viewer/image_viewer/JFJochImage.cpp b/viewer/image_viewer/JFJochImage.cpp index d58b6887d..0026afd84 100644 --- a/viewer/image_viewer/JFJochImage.cpp +++ b/viewer/image_viewer/JFJochImage.cpp @@ -89,7 +89,6 @@ void JFJochImage::onScroll(int value) { void JFJochImage::UpdateHover() { hover_rate_.restart(); mouseHover(hover_scene_pos_, hover_modifiers_); - emit hoverScenePos(hover_scene_pos_); } void JFJochImage::ScheduleHoverUpdate(const QPointF &scenePos, Qt::KeyboardModifiers modifiers) { diff --git a/viewer/image_viewer/JFJochImage.h b/viewer/image_viewer/JFJochImage.h index 73349a979..8df28da8e 100644 --- a/viewer/image_viewer/JFJochImage.h +++ b/viewer/image_viewer/JFJochImage.h @@ -255,7 +255,6 @@ signals: void roiBoxUpdated(QRect box); void roiCircleUpdated(double x, double y, double radius); void viewportChanged(QTransform transform, QPointF center); - void hoverScenePos(QPointF scenePos); // A new frame has been rendered into Frame(). Follower views repaint on this. void frameRendered(); // Asks for a move through the dataset (e.g. Home/End/PageUp/PageDown in a subclass' diff --git a/viewer/widgets/JFJochViewerMagnifier.cpp b/viewer/widgets/JFJochViewerMagnifier.cpp new file mode 100644 index 000000000..f072c1520 --- /dev/null +++ b/viewer/widgets/JFJochViewerMagnifier.cpp @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include "JFJochViewerMagnifier.h" + +#include +#include +#include +#include + +JFJochViewerMagnifier::JFJochViewerMagnifier(QWidget *parent) : QWidget(parent) { + auto layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + // Screen pixels per detector pixel and label font size; the labels are sized to fit a + // six-digit count inside its pixel. + struct Level { const char *name; const char *tooltip; double zoom; int font_px; }; + const Level levels[] = { + {"\u00d764", "Pixel values in large text", 64.0, 15}, + {"\u00d732", "Pixel values in small text", 32.0, 9}, + {"\u00d710", "Wider field, no pixel values", 10.0, 0}, + }; + + auto buttons = new QHBoxLayout(); + auto group = new QButtonGroup(this); + for (const auto &l : levels) { + auto b = new QToolButton(this); + b->setText(QString::fromUtf8(l.name)); + b->setToolTip(l.tooltip); + b->setCheckable(true); + group->addButton(b); + buttons->addWidget(b); + connect(b, &QToolButton::toggled, this, [this, l](bool on) { + if (on) + image_->SetZoom(l.zoom, l.font_px); + }); + } + buttons->addStretch(); + pop_out_ = new QToolButton(this); + connect(pop_out_, &QToolButton::clicked, this, &JFJochViewerMagnifier::popOutClicked); + setPoppedOut(false); + buttons->addWidget(pop_out_); + layout->addLayout(buttons); + + image_ = new JFJochFollowerImage(this); + image_->setMinimumHeight(200); + image_->setToolTip("Move the cursor over the diffraction image with Shift held to move the magnifier"); + image_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + layout->addWidget(image_, 1); + connect(image_, &JFJochFollowerImage::fieldChanged, this, &JFJochViewerMagnifier::fieldChanged); + + group->buttons().first()->setChecked(true); +} + +void JFJochViewerMagnifier::setPoppedOut(bool on) { + pop_out_->setText(on ? "Dock" : "Pop out"); + pop_out_->setToolTip(on ? "Put the magnifier back into its panel" : "Show the magnifier in a window of its own"); +} + +void JFJochViewerMagnifier::setFrame(std::shared_ptr frame) { + image_->SetFrame(std::move(frame)); +} + +void JFJochViewerMagnifier::setPixelValues(std::shared_ptr image) { + image_->SetPixelValues(std::move(image)); +} + +void JFJochViewerMagnifier::centerAt(QPointF scenePos) { + image_->CenterAt(scenePos); +} diff --git a/viewer/widgets/JFJochViewerMagnifier.h b/viewer/widgets/JFJochViewerMagnifier.h new file mode 100644 index 000000000..692b498a6 --- /dev/null +++ b/viewer/widgets/JFJochViewerMagnifier.h @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include + +#include +#include + +#include "../image_viewer/JFJochFollowerImage.h" + +// ADXV-style magnifier, its own dock below the inspector: a close-up of the diffraction image +// that moves when the cursor moves over the image with Shift held. It displays the frame the main +// view has already rendered, so it always agrees with it on colour map, contrast and HDR mode. +// Three fixed zoom levels: large value labels, small value labels over a wider field, and a still +// wider field with no labels. +class JFJochViewerMagnifier : public QWidget { + Q_OBJECT + + JFJochFollowerImage *image_; + QToolButton *pop_out_; +signals: + void fieldChanged(QSizeF size); + // Move to a window of its own, or back into the dock. Qt's own dock floating is disabled in + // this viewer (a floated dock goes dead on WSLg), so the main window moves the widget itself. + void popOutClicked(); +public: + explicit JFJochViewerMagnifier(QWidget *parent = nullptr); + void setPoppedOut(bool on); +public slots: + void setFrame(std::shared_ptr frame); + void setPixelValues(std::shared_ptr image); + void centerAt(QPointF scenePos); +}; diff --git a/viewer/windows/JFJochMagnifierWindow.cpp b/viewer/windows/JFJochMagnifierWindow.cpp deleted file mode 100644 index 7ba758ac2..000000000 --- a/viewer/windows/JFJochMagnifierWindow.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute -// SPDX-License-Identifier: GPL-3.0-only - -#include "JFJochMagnifierWindow.h" -#include "../image_viewer/JFJochFollowerImage.h" - -#include - -JFJochMagnifierWindow::JFJochMagnifierWindow(QWidget *parent) - : JFJochHelperWindow(parent) { - setWindowTitle("Magnifier"); - m_image = new JFJochFollowerImage(this); - setCentralWidget(m_image); - resize(320, 320); -} - -void JFJochMagnifierWindow::setFrame(std::shared_ptr frame) { - // Just a pointer assignment plus an update() that a hidden window never acts on, so this - // needs no visibility guard: there is nothing expensive left to skip. - m_image->SetFrame(std::move(frame)); -} - -void JFJochMagnifierWindow::imageLoaded(std::shared_ptr image) { - m_image->SetPixelValues(std::move(image)); -} - -void JFJochMagnifierWindow::centerAt(QPointF scenePos) { - m_last_scene_pos = scenePos; - if (!isVisible()) - return; - m_image->CenterAt(scenePos); -} - -void JFJochMagnifierWindow::showEvent(QShowEvent *event) { - JFJochHelperWindow::showEvent(event); - // setFrame keeps the pixels current while hidden, but the centre does not move, so without this - // the window re-opens showing wherever the cursor happened to be when it was closed. - if (m_last_scene_pos) - m_image->CenterAt(*m_last_scene_pos); -} diff --git a/viewer/windows/JFJochMagnifierWindow.h b/viewer/windows/JFJochMagnifierWindow.h deleted file mode 100644 index 78d349276..000000000 --- a/viewer/windows/JFJochMagnifierWindow.h +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute -// SPDX-License-Identifier: GPL-3.0-only - -#pragma once - -#include -#include - -#include "JFJochHelperWindow.h" -#include - -class JFJochFollowerImage; -class QImage; - -// ADXV-style magnifier: a small window showing a high-zoom close-up of the main image that -// follows the cursor. It displays the frame the main view has already rendered, so it does no -// conversion or colouring of its own and always agrees with the main view on colour map, -// contrast and HDR mode. -class JFJochMagnifierWindow : public JFJochHelperWindow { - Q_OBJECT - - JFJochFollowerImage *m_image; - // Where the cursor last was. Hover positions arriving while the window is hidden are remembered - // rather than dropped, so re-opening shows the region the cursor is over now and not the one it - // was over when the window was closed. - std::optional m_last_scene_pos; - -public: - explicit JFJochMagnifierWindow(QWidget *parent = nullptr); - - // Raw counts for the per-pixel labels. This only stores the pointer - the pixels are - // displayed from the frame the main view rendered, nothing is converted here. - void imageLoaded(std::shared_ptr image) override; - -protected: - void showEvent(QShowEvent *event) override; - -public slots: - void setFrame(std::shared_ptr frame); - void centerAt(QPointF scenePos); -}; diff --git a/viewer/windows/JFJochMouseShortcutsWindow.cpp b/viewer/windows/JFJochMouseShortcutsWindow.cpp index 3541d4a8d..448a7d7cd 100644 --- a/viewer/windows/JFJochMouseShortcutsWindow.cpp +++ b/viewer/windows/JFJochMouseShortcutsWindow.cpp @@ -23,6 +23,7 @@ namespace { {"Page Up / Page Down", "Step one image forward / back"}, {"Hover", "Status bar shows the pixel position, its value and the resolution"}, {"Drag", "Pan the image"}, + {"Shift + move", "Move the magnifier panel to the cursor; a frame shows the area it covers"}, {"Shift + drag", "Draw a rectangular ROI"}, {"Shift + Ctrl + drag", "Draw a circular ROI"}, {"Drag an ROI or its handle", "Move or resize the selected ROI"}, @@ -40,7 +41,6 @@ namespace { {"Dataset-info plot: Shift + hover", "Load the hovered image"}, {"Spot / reflection list: double click", "Zoom the diffraction image on that spot or prediction"}, {"Image list: double click", "Load that image"}, - {"Magnifier: wheel", "Zoom the magnifier; it follows the cursor on the main image"}, }}, };