WSJT-X/WFPalette.cpp
Bill Somerville 85c4f6722b Add user defined waterfall palette.
The  new  class WFPalette  encapsulates  waterfall palettes  including
exporting and importing to disk files and interpolation for use in the
waterfall plotter.

A special  entry in the palette  list "User Defined"  is now available
along with  a button  to invoke a  colour palette designer.   The user
defined palette definition is persistent across runs as it is saved in
the application settings file.

Palettes can now have any number of colours up to 256.

The export function may be used  to save user designed palettes; to be
added  to the built  in resource  palettes that  are shipped  with the
application.

git-svn-id: svn+ssh://svn.code.sf.net/p/wsjt/wsjt/branches/wsjtx@3964 ab8295b8-cf94-4d9e-aec4-7959e3be5d79
2014-03-31 00:03:44 +00:00

308 lines
8.9 KiB
C++

#include "WFPalette.hpp"
#include <stdexcept>
#include <memory>
#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 "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 (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);
// load the table items
ui_.colour_table_widget->setRowCount (colours_.size ());
for (int i {0}; i < colours_.size (); ++i)
{
insert_item (i);
}
// hookup the context menu handler
connect (ui_.colour_table_widget, &QWidget::customContextMenuRequested, this, &Designer::context_menu);
}
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 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 ();
auto default_colour = QColor {0, 0, 0};
if (row > 0)
{
// use the prior row colour
default_colour = colours_[row - 1];
}
auto new_colour = QColorDialog::getColor (default_colour, this);
if (new_colour.isValid ())
{
ui_.colour_table_widget->insertRow (row);
colours_.insert (row, new_colour);
insert_item (row);
}
});
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);
}
}
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 gradient between each supplied colour point
int interval = points / (colours.size () - 1);
for (int i {0}; i < points; ++i)
{
int prior {i / interval};
int next {prior + 1};
if (next >= colours.size ())
{
--next;
--prior;
}
int increment {i - interval * prior};
int r {colours[prior].red () + int((increment * (colours[next].red () - colours[prior].red ()))/interval)};
int g {colours[prior].green () + int((increment * (colours[next].green () - colours[prior].green ()))/interval)};
int b {colours[prior].blue () + int((increment * (colours[next].blue () - colours[prior].blue ()))/interval)};
result.append (QColor {r, g, 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;
}