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"}, }}, };