mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
Rename all header files with .h extension Fix function declaration in HamlibTransceiver.h that overrode member function but was not marked override Remove unused variable in Modulator.cpp Remove unused files from source tree Remove duplicate LazyFillComboBox files Reformat codebase to LLVM C++ standard
314 lines
10 KiB
C++
314 lines
10 KiB
C++
#include "WF.h"
|
|
#include "qt_helpers.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>
|
|
|
|
#include "ui_wf_palette_design_dialog.h"
|
|
|
|
/******************************************************************************/
|
|
// 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<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)
|