js8call/JS8_Main/WF.cpp
2026-01-18 19:19:15 -06:00

320 lines
10 KiB
C++

/**
* @file WF.cpp
* @brief implementation of waterfall spectrum display
*/
#include "WF.h"
#include "qt_helpers.h"
#include "ui_wf_palette_design_dialog.h"
#include <QAction>
#include <QBrush>
#include <QColor>
#include <QColorDialog>
#include <QDialog>
#include <QFile>
#include <QFileDialog>
#include <QLoggingCategory>
#include <QMenu>
#include <QMetaType>
#include <QObject>
#include <QPoint>
#include <QPushButton>
#include <QStandardPaths>
#include <QString>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QTextStream>
#include <memory>
/******************************************************************************/
// Private Implementation
/******************************************************************************/
namespace {
int constexpr points{256};
using Colours = WF::Palette::Colours;
// ensure that palette colours are useable for interpolation
Colours make_valid(Colours colours) {
if (colours.size() < 2) {
// allow single element by starting at black
colours.prepend(QColor{0, 0, 0});
}
if (1 == colours.size()) {
// allow empty list by using black to white
colours.append(QColor{255, 255, 255});
}
if (colours.size() > points) {
throw_qstring(QObject::tr("Too many colours in palette."));
}
return colours;
}
// load palette colours from a file
Colours load_palette(QString const &file_name) {
Colours colours;
QFile file{file_name};
if (file.open(QIODevice::ReadOnly)) {
unsigned count{0};
QTextStream in(&file);
int line_counter{0};
while (!in.atEnd()) {
auto line = in.readLine();
++line_counter;
if (++count >= points) {
throw_qstring(QObject::tr("Error reading waterfall palette "
"file \"%1:%2\" too many colors.")
.arg(file.fileName())
.arg(line_counter));
}
auto items = line.split(';');
if (items.size() != 3) {
throw_qstring(QObject::tr("Error reading waterfall palette "
"file \"%1:%2\" invalid triplet.")
.arg(file.fileName())
.arg(line_counter));
}
bool r_ok, g_ok, b_ok;
auto r = items[0].toInt(&r_ok);
auto g = items[1].toInt(&g_ok);
auto b = items[2].toInt(&b_ok);
if (!r_ok || !g_ok || !b_ok || r < 0 || r > 255 || g < 0 ||
g > 255 || b < 0 || b > 255) {
throw_qstring(QObject::tr("Error reading waterfall palette "
"file \"%1:%2\" invalid color.")
.arg(file.fileName())
.arg(line_counter));
}
colours.append(QColor{r, g, b});
}
} else {
throw_qstring(
QObject::tr("Error opening waterfall palette file \"%1\": %2.")
.arg(file.fileName())
.arg(file.errorString()));
}
return colours;
}
// GUI to design and manage waterfall palettes
class Designer : public QDialog {
Q_OBJECT;
public:
explicit Designer(Colours const &current, QWidget *parent = nullptr)
: QDialog{parent}, colours_{current} {
ui_.setupUi(this);
// context menu actions
auto import_button = ui_.button_box->addButton(
"&Import...", QDialogButtonBox::ActionRole);
connect(import_button, &QPushButton::clicked, this,
&Designer::import_palette);
auto export_button = ui_.button_box->addButton(
"&Export...", QDialogButtonBox::ActionRole);
connect(export_button, &QPushButton::clicked, this,
&Designer::export_palette);
// hookup the context menu handler
connect(ui_.colour_table_widget, &QWidget::customContextMenuRequested,
this, &Designer::context_menu);
load_table();
}
void load_table() {
// load the table items
ui_.colour_table_widget->clear();
ui_.colour_table_widget->setRowCount(colours_.size());
for (int i{0}; i < colours_.size(); ++i) {
insert_item(i);
}
}
Colours colours() const { return colours_; }
// invoke the colour editor
Q_SLOT void
on_colour_table_widget_itemDoubleClicked(QTableWidgetItem *item) {
auto new_colour =
QColorDialog::getColor(item->background().color(), this);
if (new_colour.isValid()) {
item->setBackground(QBrush{new_colour});
colours_[item->row()] = new_colour;
}
}
private:
void insert_item(int row) {
std::unique_ptr<QTableWidgetItem> item{new QTableWidgetItem{""}};
item->setBackground(QBrush{colours_[row]});
item->setFlags(Qt::ItemIsEnabled);
ui_.colour_table_widget->setItem(row, 0, item.release());
}
void insert_new_item(int row, QColor const &default_colour) {
// use the prior row colour as default if available
auto new_colour = QColorDialog::getColor(
row > 0 ? colours_[row - 1] : default_colour, this);
if (new_colour.isValid()) {
ui_.colour_table_widget->insertRow(row);
colours_.insert(row, new_colour);
insert_item(row);
}
}
void context_menu(QPoint const &p) {
context_menu_.clear();
if (ui_.colour_table_widget->itemAt(p)) {
auto delete_action = context_menu_.addAction(tr("&Delete"));
connect(delete_action, &QAction::triggered, [this]() {
auto row = ui_.colour_table_widget->currentRow();
ui_.colour_table_widget->removeRow(row);
colours_.removeAt(row);
});
}
auto insert_action = context_menu_.addAction(tr("&Insert ..."));
connect(insert_action, &QAction::triggered, [this]() {
auto item = ui_.colour_table_widget->itemAt(menu_pos_);
int row = item ? item->row() : colours_.size();
insert_new_item(row, QColor{0, 0, 0});
});
auto insert_after_action =
context_menu_.addAction(tr("Insert &after ..."));
connect(insert_after_action, &QAction::triggered, [this]() {
auto item = ui_.colour_table_widget->itemAt(menu_pos_);
int row = item ? item->row() + 1 : colours_.size();
insert_new_item(row, QColor{255, 255, 255});
});
menu_pos_ = p; // save for context menu action handlers
context_menu_.popup(ui_.colour_table_widget->mapToGlobal(p));
}
void import_palette() {
auto docs =
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
auto file_name = QFileDialog::getOpenFileName(
this, tr("Import Palette"), docs, tr("Palettes (*.pal)"));
if (!file_name.isEmpty()) {
colours_ = load_palette(file_name);
load_table();
}
}
void export_palette() {
auto docs =
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
auto file_name = QFileDialog::getSaveFileName(
this, tr("Export Palette"), docs, tr("Palettes (*.pal)"));
if (!file_name.isEmpty()) {
if (!QFile::exists(file_name) && !file_name.contains('.')) {
file_name += ".pal";
}
QFile file{file_name};
if (file.open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) {
QTextStream stream{&file};
Q_FOREACH (auto colour, colours_) {
stream << colour.red() << ';' << colour.green() << ';'
<< colour.blue() << Qt::endl;
}
} else {
throw_qstring(
QObject::tr(
"Error writing waterfall palette file \"%1\": %2.")
.arg(file.fileName())
.arg(file.errorString()));
}
}
}
Ui::wf_palette_design_dialog ui_;
Colours colours_;
QMenu context_menu_;
QPoint menu_pos_;
};
} // namespace
#include "WF.moc"
Q_DECLARE_LOGGING_CATEGORY(wf_js8)
/******************************************************************************/
// Public Implementation
/******************************************************************************/
namespace WF {
Palette::Palette(QString const &file_path)
: colours_{load_palette(file_path)} {}
Palette::Palette(Colours const &colour_list) : colours_{colour_list} {}
// generate an array of colours suitable for the waterfall plotter
QVector<QColor> Palette::interpolate() const {
Colours colours{make_valid(colours_)};
QVector<QColor> result;
result.reserve(points);
// do a linear-ish gradient between each supplied colour point
auto interval = qreal(points) / (colours.size() - 1);
for (int i{0}; i < points; ++i) {
int prior = i / interval;
if (prior >= (colours.size() - 1)) {
--prior;
}
auto next = prior + 1;
if (next >= colours.size()) {
--next;
}
// qCDebug (wf_js8) << "Palette::interpolate: prior:" << prior <<
// "total:" << colours.size ();
auto increment = i - qreal(interval) * prior;
qreal r{colours[prior].redF() +
(increment * (colours[next].redF() - colours[prior].redF())) /
interval};
qreal g{
colours[prior].greenF() +
(increment * (colours[next].greenF() - colours[prior].greenF())) /
interval};
qreal b{colours[prior].blueF() +
(increment * (colours[next].blueF() - colours[prior].blueF())) /
interval};
result.append(QColor::fromRgbF(r, g, b));
// qCDebug (wf_js8) << "Palette colour[" << (result.size () - 1) << "]
// =" << result[result.size () - 1] << "from: r:" << r << "g:" << g <<
// "b:" << b;
}
return result;
}
// invoke the palette designer
bool Palette::design() {
if (auto designer = Designer{colours_};
designer.exec() == QDialog::Accepted) {
colours_ = designer.colours();
return true;
}
return false;
}
} // namespace WF
/******************************************************************************/
Q_LOGGING_CATEGORY(wf_js8, "wf.js8", QtWarningMsg)