Viewer: dock the magnifier, fixed zoom levels, Shift-driven with a frame
Build Packages / Unit tests (push) Successful in 1h11m40s
Build Packages / build:windows:nocuda (push) Successful in 17m39s
Build Packages / build:windows:cuda (push) Successful in 20m10s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m2s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m28s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m4s
Build Packages / build:rugnux:windows (push) Successful in 11m20s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m46s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 16m15s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 16m21s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 18m1s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m19s
Build Packages / build:rpm (rocky8) (push) Successful in 18m17s
Build Packages / build:rpm (rocky9) (push) Successful in 18m48s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 15m59s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 16m19s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m2s
Build Packages / Generate python client (push) Successful in 1m3s
Build Packages / Build documentation (push) Successful in 1m28s
Build Packages / Create release (push) Successful in 18s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m50s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 11m15s
Build Packages / Unit tests (push) Successful in 1h11m40s
Build Packages / build:windows:nocuda (push) Successful in 17m39s
Build Packages / build:windows:cuda (push) Successful in 20m10s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m2s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m28s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m4s
Build Packages / build:rugnux:windows (push) Successful in 11m20s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m46s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 16m15s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 16m21s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 18m1s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m19s
Build Packages / build:rpm (rocky8) (push) Successful in 18m17s
Build Packages / build:rpm (rocky9) (push) Successful in 18m48s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 15m59s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 16m19s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m2s
Build Packages / Generate python client (push) Successful in 1m3s
Build Packages / Build documentation (push) Successful in 1m28s
Build Packages / Create release (push) Successful in 18s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m50s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 11m15s
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <QMessageBox>
|
||||
@@ -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<int>(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<QKeyEvent *>(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<QLineEdit *>(focus) || qobject_cast<QAbstractSpinBox *>(focus)
|
||||
|| qobject_cast<QComboBox *>(focus) || qobject_cast<QTextEdit *>(focus)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ void JFJochFollowerImage::SetFrame(std::shared_ptr<const QImage> 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_ptr<const JFJochReaderImage
|
||||
}
|
||||
|
||||
void JFJochFollowerImage::CenterAt(QPointF scenePos) {
|
||||
center_ = scenePos;
|
||||
center_set_ = true;
|
||||
if (!frame_ || frame_->isNull())
|
||||
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<int>(zoom_ * 0.3), 7, 16));
|
||||
font.setPixelSize(label_font_px_);
|
||||
painter->setFont(font);
|
||||
|
||||
for (int y = y0; y < y1; y++) {
|
||||
|
||||
@@ -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<const JFJochReaderImage> 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<const QImage> frame);
|
||||
void SetPixelValues(std::shared_ptr<const JFJochReaderImage> 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);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "JFJochViewerMagnifier.h"
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QHBoxLayout>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
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<const QImage> frame) {
|
||||
image_->SetFrame(std::move(frame));
|
||||
}
|
||||
|
||||
void JFJochViewerMagnifier::setPixelValues(std::shared_ptr<const JFJochReaderImage> image) {
|
||||
image_->SetPixelValues(std::move(image));
|
||||
}
|
||||
|
||||
void JFJochViewerMagnifier::centerAt(QPointF scenePos) {
|
||||
image_->CenterAt(scenePos);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QToolButton>
|
||||
#include <QWidget>
|
||||
|
||||
#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<const QImage> frame);
|
||||
void setPixelValues(std::shared_ptr<const JFJochReaderImage> image);
|
||||
void centerAt(QPointF scenePos);
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "JFJochMagnifierWindow.h"
|
||||
#include "../image_viewer/JFJochFollowerImage.h"
|
||||
|
||||
#include <QShowEvent>
|
||||
|
||||
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<const QImage> 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<const JFJochReaderImage> 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);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "JFJochHelperWindow.h"
|
||||
#include <QPointF>
|
||||
|
||||
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<QPointF> 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<const JFJochReaderImage> image) override;
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
public slots:
|
||||
void setFrame(std::shared_ptr<const QImage> frame);
|
||||
void centerAt(QPointF scenePos);
|
||||
};
|
||||
@@ -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"},
|
||||
}},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user