viewer: palette theming, Original as a run row, stable plot colours

Styling (now via the application palette in main(), not a stylesheet, so it
applies to every widget incl. dialogs):
- Entry fields and item views (line edits, spin boxes, combos, tables) are white
  when enabled and salmon (the GUI default) when disabled - so it's clear where
  you can type. Panels stay salmon (Window role).
- Teal (#2a9d8f) accent via the Highlight role: selections and progress-bar chunks.

Runs list / plots:
- The original file is listed as a first "Original" run (Mode "HDF5"); double-click
  any run row to show it (replaces the "Show original" button). The currently shown
  run is bold (driven by snapshotsChanged).
- Fix the colour "swap" when switching runs: the primary line is matched by the
  active-dataset pointer (not active_id_), so a run keeps its colour even while a
  datasetLoaded/runsChanged pair is mid-flight.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 15:34:06 +02:00
co-authored by Claude Opus 4.8
parent 95fe2ce781
commit 67ad43a2b2
6 changed files with 87 additions and 53 deletions
+8 -6
View File
@@ -231,15 +231,17 @@ void JFJochViewerDatasetInfo::UpdatePlot() {
std::vector<float> data = ExtractMetric(*dataset, val, one_over_d2);
// One overlay line per other run, plus the in-progress live run. Each run keeps a stable colour
// tied to its position in the list, so colours don't swap when the active run changes.
// tied to its position in the list, so colours don't swap when the active run changes. The
// primary is matched by dataset pointer (not active_id_) so colour/name stay consistent even
// while a datasetLoaded/runsChanged pair is still arriving.
std::vector<JFJochDatasetInfoChartView::NamedSeries> overlays;
QString primary_name;
QColor primary_color;
int run_index = 0;
for (const auto &run: runs_) {
const QColor color = RunColor(run_index++);
if (run.id == active_id_) { primary_name = run.label; primary_color = color; continue; }
if (!run.dataset || run.dataset == dataset) continue; // never draw the active run twice
for (int i = 0; i < runs_.size(); i++) {
const auto &run = runs_[i];
if (!run.dataset) continue;
const QColor color = RunColor(i);
if (run.dataset == dataset) { primary_name = run.label; primary_color = color; continue; }
bool ignore = false;
overlays.push_back({run.label, ExtractMetric(*run.dataset, val, ignore), color, XForRun(*run.dataset)});
}
+2 -1
View File
@@ -54,7 +54,6 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
setDockOptions(dockOptions() & ~QMainWindow::DockOption::AllowTabbedDocks);
setStyleSheet(stylesheet);
setWindowTitle("Jungfraujoch image viewer");
// Start large on a big display but fit within a laptop screen.
const QRect avail = QGuiApplication::primaryScreen()->availableGeometry();
@@ -409,6 +408,8 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
processingJobsWindow, &JFJochProcessingJobsWindow::onHttpConnectionChanged);
connect(reading_worker, &JFJochImageReadingWorker::fileOpened,
processingJobsWindow, &JFJochProcessingJobsWindow::clearJobs);
connect(reading_worker, &JFJochImageReadingWorker::snapshotsChanged, processingJobsWindow,
[pw = processingJobsWindow](QStringList, QString active) { pw->setActiveRun(active); });
connect(reading_worker, &JFJochImageReadingWorker::runsChanged, this,
[this](QVector<RunData> runs, QString active) {
lastRuns = std::move(runs);
+2 -25
View File
@@ -18,31 +18,8 @@ class QDockWidget;
class JFJochViewerWindow : public QMainWindow {
Q_OBJECT
// Salmon (GUI default) everywhere; entry widgets (line edits, spin boxes, combos, tables) turn
// white while focused/active. A teal accent (selection + progress chunks) adds some colour.
const QString stylesheet = R"(
background-color: rgb(255, 235, 230);
QLineEdit, QPlainTextEdit, QTextEdit, QAbstractSpinBox, QComboBox, QAbstractItemView {
background-color: rgb(255, 235, 230);
selection-background-color: #2a9d8f;
selection-color: white;
}
QLineEdit:focus, QPlainTextEdit:focus, QTextEdit:focus, QAbstractSpinBox:focus,
QComboBox:focus, QAbstractItemView:focus {
background-color: white;
}
QProgressBar {
border: 1px solid #d9b3aa;
border-radius: 3px;
background-color: rgb(255, 235, 230);
text-align: center;
}
QProgressBar::chunk {
background-color: #2a9d8f;
border-radius: 2px;
}
)";
// Theming is done via the application palette in main() (salmon panels, white entry fields,
// teal accent) so it applies consistently to every widget, including dialogs.
public:
explicit JFJochViewerWindow(QWidget *parent = nullptr, bool dbus = true, const QString &file = "");
~JFJochViewerWindow() override;
+14
View File
@@ -3,6 +3,7 @@
#include <QCommandLineOption>
#include <QSplashScreen>
#include <QTimer>
#include <QPalette>
#include "JFJochViewerWindow.h"
#include "../writer/HDF5Objects.h"
@@ -16,6 +17,19 @@ int main(int argc, char *argv[]) {
RegisterHDF5Filter();
QApplication app(argc, argv);
// Theme via the palette (robust across widgets): salmon panels, white entry fields that fall back
// to salmon when disabled, and a teal accent for selections and progress-bar chunks.
app.setStyle("Fusion");
QPalette pal = app.palette();
pal.setColor(QPalette::Window, QColor(255, 235, 230));
pal.setColor(QPalette::Base, Qt::white);
pal.setColor(QPalette::AlternateBase, QColor(255, 245, 242));
pal.setColor(QPalette::Button, QColor(255, 235, 230));
pal.setColor(QPalette::Disabled, QPalette::Base, QColor(255, 235, 230));
pal.setColor(QPalette::Highlight, QColor(0x2a, 0x9d, 0x8f));
pal.setColor(QPalette::HighlightedText, Qt::white);
app.setPalette(pal);
QIcon appIcon(":/jfjoch.png");
app.setWindowIcon(appIcon);
+58 -19
View File
@@ -10,6 +10,7 @@
#include <QComboBox>
#include <QDateTime>
#include <QDialog>
#include <QFont>
#include <QDialogButtonBox>
#include <QFileInfo>
#include <QFormLayout>
@@ -35,6 +36,12 @@ namespace {
const unsigned hc = std::thread::hardware_concurrency();
return hc == 0 ? 4 : static_cast<int>(hc);
}
QTableWidgetItem *fixedItem(const QString &text) { // a non-editable cell
auto *cell = new QTableWidgetItem(text);
cell->setFlags(cell->flags() & ~Qt::ItemIsEditable);
return cell;
}
}
JFJochProcessingJobsWindow::JFJochProcessingJobsWindow(JFJochImageReadingWorker *worker, QWidget *parent)
@@ -54,8 +61,7 @@ JFJochProcessingJobsWindow::JFJochProcessingJobsWindow(JFJochImageReadingWorker
toolbar_->addAction("Cancel", this, &JFJochProcessingJobsWindow::cancelJob);
toolbar_->addAction("Remove result", this, &JFJochProcessingJobsWindow::removeResult);
toolbar_->addSeparator();
toolbar_->addAction("View results", this, &JFJochProcessingJobsWindow::viewResults);
toolbar_->addAction("Show original", this, &JFJochProcessingJobsWindow::showOriginal);
toolbar_->addAction("Show selected", this, &JFJochProcessingJobsWindow::viewResults);
table_ = new QTableWidget(0, COL_COUNT, this);
table_->setMinimumHeight(60); // let the bottom dock shrink freely
@@ -74,6 +80,13 @@ JFJochProcessingJobsWindow::JFJochProcessingJobsWindow(JFJochImageReadingWorker
jobs_[row].label = item->text();
emit renameRun(jobs_[row].id, jobs_[row].label);
});
// Double-click a row (other than its editable Name) to show that run in the plots/image.
connect(table_, &QTableWidget::cellDoubleClicked, this, [this](int row, int col) {
if (col == COL_NAME)
return; // double-clicking Name edits the label
if (row >= 0 && row < static_cast<int>(jobs_.size()) && jobs_[row].has_result)
emit activateSnapshot(jobs_[row].id);
});
auto *message = new QLabel("Dataset re-processing is currently available only in File mode.\n\n"
"Open a stored HDF5 file to run processing jobs.", this);
@@ -245,12 +258,6 @@ void JFJochProcessingJobsWindow::newJob() {
table_->insertRow(row);
jobs_.push_back(info); // jobs_[row] must exist before setItem(COL_NAME) fires itemChanged
auto fixed = [](const QString &text) { // a non-editable cell
auto *cell = new QTableWidgetItem(text);
cell->setFlags(cell->flags() & ~Qt::ItemIsEditable);
return cell;
};
const QString range_text = (spec.start_image == 0 && spec.end_image == 0)
? QStringLiteral("all")
: QStringLiteral("%1%2").arg(spec.start_image)
@@ -258,12 +265,12 @@ void JFJochProcessingJobsWindow::newJob() {
const int expected = spec.end_image > spec.start_image ? spec.end_image - spec.start_image : 0;
table_->setItem(row, COL_NAME, new QTableWidgetItem(label)); // editable (default flags)
table_->setItem(row, COL_STARTED, fixed(QDateTime::currentDateTime().toString("HH:mm:ss")));
table_->setItem(row, COL_MODE, fixed(full ? "Full" : "AzInt"));
table_->setItem(row, COL_IMAGES, fixed(range_text));
table_->setItem(row, COL_STATUS, fixed("queued"));
table_->setItem(row, COL_INDEX, fixed("-"));
table_->setItem(row, COL_CELL, fixed("-"));
table_->setItem(row, COL_STARTED, fixedItem(QDateTime::currentDateTime().toString("HH:mm:ss")));
table_->setItem(row, COL_MODE, fixedItem(full ? "Full" : "AzInt"));
table_->setItem(row, COL_IMAGES, fixedItem(range_text));
table_->setItem(row, COL_STATUS, fixedItem("queued"));
table_->setItem(row, COL_INDEX, fixedItem("-"));
table_->setItem(row, COL_CELL, fixedItem("-"));
// A progress bar lives in the Status cell while the job runs; it shows the phase as text until
// image processing starts, then "<done> / <expected>" with the bar filling in the background.
@@ -292,11 +299,13 @@ void JFJochProcessingJobsWindow::removeResult() {
const int row = table_->currentRow();
if (row < 0 || row >= static_cast<int>(jobs_.size()))
return;
if (jobs_[row].id == "Original")
return; // the original file is always kept
if (row == running_row_) {
QMessageBox::information(this, "Processing", "Cancel the running job before removing it.");
return;
}
emit removeRun(jobs_[row].id); // drops the snapshot from the reader (no-op for Original)
emit removeRun(jobs_[row].id); // drops the snapshot from the reader
QSignalBlocker block(table_); // row removal must not look like a rename
table_->removeRow(row);
@@ -316,6 +325,40 @@ void JFJochProcessingJobsWindow::clearJobs() {
job_counter_ = 0;
running_row_ = -1;
running_bar_ = nullptr;
addOriginalRow();
}
void JFJochProcessingJobsWindow::addOriginalRow() {
// The file's own data, listed as the first run so it can be shown like any reprocessing run.
JobInfo info;
info.id = "Original";
info.label = "Original";
info.has_result = true;
const int row = table_->rowCount();
table_->insertRow(row);
jobs_.push_back(info);
QSignalBlocker block(table_);
table_->setItem(row, COL_NAME, fixedItem("Original")); // reserved name, not editable
table_->setItem(row, COL_STATUS, fixedItem(""));
table_->setItem(row, COL_STARTED, fixedItem(""));
table_->setItem(row, COL_MODE, fixedItem("HDF5"));
table_->setItem(row, COL_IMAGES, fixedItem("all"));
table_->setItem(row, COL_INDEX, fixedItem("-"));
table_->setItem(row, COL_CELL, fixedItem("-"));
}
void JFJochProcessingJobsWindow::setActiveRun(QString active_id) {
QSignalBlocker block(table_);
for (int row = 0; row < static_cast<int>(jobs_.size()); row++) {
auto *item = table_->item(row, COL_NAME);
if (!item)
continue;
QFont f = item->font();
f.setBold(jobs_[row].id == active_id);
item->setFont(f);
}
}
void JFJochProcessingJobsWindow::viewResults() {
@@ -329,10 +372,6 @@ void JFJochProcessingJobsWindow::viewResults() {
emit activateSnapshot(jobs_[row].id);
}
void JFJochProcessingJobsWindow::showOriginal() {
emit activateSnapshot("Original");
}
void JFJochProcessingJobsWindow::setStatus(int row, const QString &text) {
if (row >= 0 && row < table_->rowCount())
table_->item(row, COL_STATUS)->setText(text);
+3 -2
View File
@@ -37,14 +37,14 @@ signals:
public slots:
void onHttpConnectionChanged(bool connected, QString addr);
void clearJobs(); // reset the table on a new file
void clearJobs(); // reset the table on a new file (re-adds the Original row)
void setActiveRun(QString active_id); // bold the row of the run currently shown in the plots
private slots:
void newJob();
void cancelJob();
void removeResult();
void viewResults();
void showOriginal();
void onPhase(QString phase);
void onProgress(quint64 done, quint64 total);
void onFinished(ProcessResult result);
@@ -72,6 +72,7 @@ private:
int askJob(const ReprocessingInputs &inputs, JobSpec &spec);
ProcessConfig buildConfig(const JobSpec &spec, const ReprocessingInputs &inputs) const;
void setStatus(int row, const QString &text);
void addOriginalRow(); // the file's own data as the first, always-present run
JFJochImageReadingWorker *worker_;
JFJochProcessController *controller_;