/** * @file WF.cpp * @brief implementation of waterfall spectrum display */ #include "WF.h" #include "qt_helpers.h" #include "ui_wf_palette_design_dialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /******************************************************************************/ // 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 ¤t, 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 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 Palette::interpolate() const { Colours colours{make_valid(colours_)}; QVector 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)