mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-11-30 16:18:54 -05:00
1e107e7e1e
Yet another rework of waterfall palette interpolation to try and avoid invalid colour values. git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@3999 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
331 lines
9.7 KiB
C++
331 lines
9.7 KiB
C++
#include "WFPalette.hpp"
|
|
|
|
#include <stdexcept>
|
|
#include <memory>
|
|
#include <algorithm>
|
|
|
|
#include <QMetaType>
|
|
#include <QObject>
|
|
#include <QFile>
|
|
#include <QTextStream>
|
|
#include <QString>
|
|
#include <QDialog>
|
|
#include <QTableWidget>
|
|
#include <QTableWidgetItem>
|
|
#include <QColorDialog>
|
|
#include <QColor>
|
|
#include <QBrush>
|
|
#include <QPoint>
|
|
#include <QMenu>
|
|
#include <QAction>
|
|
#include <QPushButton>
|
|
#include <QStandardPaths>
|
|
#include <QFileDialog>
|
|
#include <QFile>
|
|
#include <QTextStream>
|
|
#include <QDebug>
|
|
|
|
#include "qt_helpers.hpp"
|
|
|
|
#include "ui_wf_palette_design_dialog.h"
|
|
|
|
namespace
|
|
{
|
|
int constexpr points {256};
|
|
|
|
struct init
|
|
{
|
|
init ()
|
|
{
|
|
qRegisterMetaTypeStreamOperators<WFPalette::Colours> ("Colours");
|
|
}
|
|
} static_initaializer;
|
|
|
|
using Colours = WFPalette::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\".").arg (file.fileName ()));
|
|
}
|
|
|
|
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 () << endl;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw_qstring (QObject::tr ("Error writing waterfall palette file \"%1\".").arg (file.fileName ()));
|
|
}
|
|
}
|
|
}
|
|
|
|
Ui::wf_palette_design_dialog ui_;
|
|
Colours colours_;
|
|
QMenu context_menu_;
|
|
QPoint menu_pos_;
|
|
};
|
|
}
|
|
|
|
#include "WFPalette.moc"
|
|
|
|
WFPalette::WFPalette (QString const& file_path)
|
|
: colours_ {load_palette (file_path)}
|
|
{
|
|
}
|
|
|
|
WFPalette::WFPalette (QList<QColor> const& colour_list)
|
|
: colours_ {colour_list}
|
|
{
|
|
}
|
|
|
|
// generate an array of colours suitable for the waterfall plotter
|
|
QVector<QColor> WFPalette::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;
|
|
}
|
|
|
|
// qDebug () << "WFPalette::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));
|
|
|
|
// qDebug () << "Palette colour[" << (result.size () - 1) << "] =" << result[result.size () - 1] << "from: r:" << r << "g:" << g << "b:" << b;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// invoke the palette designer
|
|
bool WFPalette::design ()
|
|
{
|
|
Designer designer {colours_};
|
|
if (QDialog::Accepted == designer.exec ())
|
|
{
|
|
colours_ = designer.colours ();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|