mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
315 lines
10 KiB
C++
315 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)
|