viewer: hit-feed polish — stacked docks, hidden processing, resolution mode

Follow-up to the image strip, addressing the review:

- Composition: stack the plots and the thumbnail strip vertically (plots on top
  with more height, strip below) instead of sharing horizontal space — both
  benefit from width, and the strip needs less height.
- Processing dock is hidden by default and narrower; it reveals itself only when
  a reprocessing job starts (new jobStarted signal), and the Processing
  perspective no longer force-shows it.
- Thumbnail spot overlays now use the same feature (indexed) / spot colours as
  the main viewer, and follow the side-panel colour pickers.
- New strip selection modes "Resolution" and "Background": pick images that span
  that metric's distribution (a quick histogram-representative selection).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 16:26:15 +02:00
co-authored by Claude Opus 4.8
parent c62cd99566
commit 7d24cdba4f
6 changed files with 57 additions and 7 deletions
+11 -1
View File
@@ -1035,6 +1035,16 @@ void JFJochImageReadingWorker::SetThumbnailColorMap(int color_map) {
thumb_color_scale_.Select(static_cast<ColorScaleEnum>(color_map));
}
void JFJochImageReadingWorker::SetThumbnailFeatureColor(QColor c) {
QMutexLocker locker(&m);
thumb_feature_color_ = c;
}
void JFJochImageReadingWorker::SetThumbnailSpotColor(QColor c) {
QMutexLocker locker(&m);
thumb_spot_color_ = c;
}
void JFJochImageReadingWorker::RenderThumbnails(QVector<qint64> image_numbers, bool show_spots) {
// File mode only; live HTTP cannot address arbitrary images cheaply.
if (http_mode)
@@ -1111,7 +1121,7 @@ QImage JFJochImageReadingWorker::RenderThumbnail_i(int64_t image_number, bool sh
QPainter p(&out);
p.setRenderHint(QPainter::Antialiasing);
for (const auto &sp : img->ImageData().spots) {
p.setPen(QPen(sp.indexed ? QColor(0xFA, 0x72, 0x68) : QColor(0x2A, 0x9D, 0x8F), 1.0));
p.setPen(QPen(sp.indexed ? thumb_feature_color_ : thumb_spot_color_, 1.0));
p.drawEllipse(QPointF(sp.x * s, sp.y * s), 1.6, 1.6);
}
}
+5
View File
@@ -11,6 +11,7 @@
#include <QTimer>
#include <QElapsedTimer>
#include <QImage>
#include <QColor>
#include "../common/ColorScale.h"
@@ -92,6 +93,8 @@ private:
SpotFindingSettings spot_finding_settings;
ColorScale thumb_color_scale_; // colour map used for the image-strip thumbnails
QColor thumb_feature_color_ = Qt::magenta; // indexed-spot overlay (matches the main viewer)
QColor thumb_spot_color_ = Qt::green; // non-indexed spot overlay
QImage RenderThumbnail_i(int64_t image_number, bool show_spots);
std::optional<int64_t> current_image;
@@ -224,4 +227,6 @@ public slots:
// for each; non-disruptive (does not change the displayed image).
void RenderThumbnails(QVector<qint64> image_numbers, bool show_spots);
void SetThumbnailColorMap(int color_map);
void SetThumbnailFeatureColor(QColor c);
void SetThumbnailSpotColor(QColor c);
};
+18 -6
View File
@@ -518,7 +518,8 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
connect(settingsPanel, &JFJochViewerSettingsDock::ringsFromCalibration,
side_panel, &JFJochViewerSidePanel::SetRings);
// Dock the processing panel in the bottom-right corner, next to the dataset-info plots.
// Processing panel: hidden by default and narrower; it reveals itself (to the right of the
// plots) only when a reprocessing job starts.
processingDock = new QDockWidget("Processing", this);
processingDock->setObjectName("processingDock");
processingDock->setAllowedAreas(Qt::BottomDockWidgetArea);
@@ -528,19 +529,26 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
addDockWidget(Qt::BottomDockWidgetArea, processingDock);
if (lastDatasetInfoDock) {
splitDockWidget(lastDatasetInfoDock, processingDock, Qt::Horizontal);
resizeDocks({lastDatasetInfoDock, processingDock}, {650, 350}, Qt::Horizontal);
// Give the bottom area generous height: the per-dataset plot is a headline feature, so it
// gets real room rather than a thin strip.
resizeDocks({lastDatasetInfoDock, processingDock}, {420, 420}, Qt::Vertical);
resizeDocks({lastDatasetInfoDock, processingDock}, {900, 280}, Qt::Horizontal);
}
processingDock->hide();
menuBar->AddDockEntry(processingDock, "Processing");
connect(processingJobsWindow, &JFJochProcessingJobsWindow::jobStarted, this, [this] {
processingDock->show();
processingDock->raise();
});
// Image strip / hit feed: thumbnails of representative images, rendered off-thread; click opens.
// Stacked below the plots — both want vertical room, and the strip needs less of it.
auto *imageStrip = new JFJochViewerImageStrip(this);
imageStripDock = new QDockWidget("Image strip", this);
imageStripDock->setObjectName("imageStripDock");
imageStripDock->setWidget(imageStrip);
addDockWidget(Qt::BottomDockWidgetArea, imageStripDock);
if (lastDatasetInfoDock) {
splitDockWidget(lastDatasetInfoDock, imageStripDock, Qt::Vertical);
resizeDocks({lastDatasetInfoDock, imageStripDock}, {340, 180}, Qt::Vertical);
}
menuBar->AddDockEntry(imageStripDock, "Image strip");
connect(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
imageStrip, &JFJochViewerImageStrip::datasetLoaded);
@@ -552,6 +560,10 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
reading_worker, &JFJochImageReadingWorker::LoadImage);
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::colorMapChanged,
reading_worker, &JFJochImageReadingWorker::SetThumbnailColorMap);
connect(side_panel, &JFJochViewerSidePanel::setFeatureColor,
reading_worker, &JFJochImageReadingWorker::SetThumbnailFeatureColor);
connect(side_panel, &JFJochViewerSidePanel::setSpotColor,
reading_worker, &JFJochImageReadingWorker::SetThumbnailSpotColor);
connect(menuBar, &JFJochViewerMenu::imageLayoutSelected, this,
[this] { ApplyPerspective(Perspective::Image); });
@@ -583,8 +595,8 @@ void JFJochViewerWindow::ApplyPerspective(Perspective p) {
const bool processing = (p == Perspective::Processing);
if (inspectorDock) inspectorDock->setVisible(true);
if (settingsDock) settingsDock->setVisible(processing);
if (processingDock) processingDock->setVisible(processing);
if (imageStripDock) imageStripDock->setVisible(processing);
if (processingDock && !processing) processingDock->hide(); // shows only when a job starts
for (auto *d : findChildren<QDockWidget *>())
if (d->objectName().startsWith("datasetInfoDock"))
d->setVisible(processing);
+21
View File
@@ -14,6 +14,7 @@
#include <algorithm>
#include <numeric>
#include <cmath>
JFJochViewerImageStrip::JFJochViewerImageStrip(QWidget *parent) : QWidget(parent) {
auto *layout = new QVBoxLayout(this);
@@ -25,6 +26,8 @@ JFJochViewerImageStrip::JFJochViewerImageStrip(QWidget *parent) : QWidget(parent
mode_->addItem("Evenly spaced", 0);
mode_->addItem("Most spots", 1);
mode_->addItem("Indexed", 2);
mode_->addItem("Resolution", 3); // representatives across the resolution range
mode_->addItem("Background", 4); // representatives across the background range
controls->addWidget(mode_);
spots_ = new QCheckBox("Spots", this);
spots_->setChecked(true);
@@ -76,6 +79,20 @@ QVector<qint64> JFJochViewerImageStrip::ComputeRepresentatives() const {
result.push_back(N == 1 ? 0 : static_cast<int64_t>(i) * (total - 1) / (N - 1));
};
// Pick N images that span a metric's distribution (sort by value, sample evenly across it) —
// a quick "histogram representative" selection.
auto spread = [&](const std::vector<float> &metric) {
std::vector<std::pair<float, int64_t>> vals;
for (size_t i = 0; i < metric.size(); ++i)
if (std::isfinite(metric[i]))
vals.push_back({metric[i], static_cast<int64_t>(i)});
if (vals.empty()) { evenly(); return; }
std::sort(vals.begin(), vals.end());
const int n = static_cast<int>(std::min<size_t>(N, vals.size()));
for (int i = 0; i < n; ++i)
result.push_back(vals[n == 1 ? 0 : static_cast<size_t>(i) * (vals.size() - 1) / (n - 1)].second);
};
if (mode == 1 && !sc.empty()) { // most spots
std::vector<int64_t> idx(sc.size());
std::iota(idx.begin(), idx.end(), 0);
@@ -96,6 +113,10 @@ QVector<qint64> JFJochViewerImageStrip::ComputeRepresentatives() const {
for (int i = 0; i < n; ++i)
result.push_back(indexed[n == 1 ? 0 : static_cast<size_t>(i) * (indexed.size() - 1) / (n - 1)]);
}
} else if (mode == 3 && !dataset_->resolution_estimate.empty()) { // resolution spread
spread(dataset_->resolution_estimate);
} else if (mode == 4 && !dataset_->bkg_estimate.empty()) { // background spread
spread(dataset_->bkg_estimate);
} else {
evenly();
}
@@ -295,6 +295,7 @@ void JFJochProcessingJobsWindow::newJob() {
job_timer_.start();
controller_->start(inputs.file, experiment, inputs.pixel_mask, config);
emit jobStarted();
emit writeStatusBar("Started processing job " + label);
}
@@ -34,6 +34,7 @@ signals:
void writeStatusBar(QString message, int timeout_ms = 0);
// Live per-image results while a job runs, for the dataset-info plots (nullptr clears it).
void liveDataset(std::shared_ptr<const JFJochReaderDataset> dataset);
void jobStarted(); // a reprocessing job was launched — reveal the (hidden-by-default) panel
public slots:
void onHttpConnectionChanged(bool connected, QString addr);