#pragma once
#include "FractoriumPch.h"
#include "FractoriumSettings.h"
#include "PaletteTableWidgetItem.h"
///
/// Fractorium global utility functions.
///
#define PALETTE_CELL_HEIGHT 16
///
/// Setup a spinner to be placed in a table cell.
/// Due to a serious compiler bug in MSVC, this must be declared as an outside function instead of a static member of Fractorium.
/// The reason is that the default arguments of type valType will not be interpreted correctly by the compiler.
/// If the bug is ever fixed, put it back as a static member function.
///
/// The table the spinner belongs to
/// The receiver object
/// The row in the table where this spinner resides
/// The col in the table where this spinner resides
/// Double pointer to spin box which will hold the spinner upon exit
/// The height of the spinner
/// The minimum value of the spinner
/// The maximum value of the spinner
/// The step of the spinner
/// The signal the spinner emits
/// The slot to receive the signal
/// Whether to increment the row value
/// The default value for the spinner
/// When the spinner has a value of zero and is double clicked, assign this value
/// When the spinner has a value of non-zero and is double clicked, assign this value
template
static void SetupSpinner(QTableWidget* table, const QObject* receiver, int& row, int col, spinType*& spinBox, int height, valType min, valType max, valType step, const char* signal, const char* slot, bool incRow = true, valType val = 0, valType doubleClickZero = -999, valType doubleClickNonZero = -999)
{
const auto settings = FractoriumSettings::DefInstance();
spinBox = new spinType(table, height, step);
spinBox->setRange(min, max);
spinBox->setValue(val);
if (col >= 0)
table->setCellWidget(row, col, spinBox);
if (string(signal) != "" && string(slot) != "")
receiver->connect(spinBox, signal, receiver, slot, Qt::QueuedConnection);
if (doubleClickNonZero != -999 && doubleClickZero != -999)
{
spinBox->DoubleClick(true);
spinBox->DoubleClickZero(static_cast(doubleClickZero));
spinBox->DoubleClickNonZero(static_cast(doubleClickNonZero));
}
if (incRow)
row++;
}
///
/// Wrapper around QWidget::setTabOrder() to return the second widget.
/// This makes it easy to chain multiple calls without having to retype
/// all of them if the order changes or if a new widget is inserted.
///
/// The parent widget that w1 and w2 belong to
/// The widget to come first in the tab order
/// The widget to come second in the tab order
static QWidget* SetTabOrder(QWidget* p, QWidget* w1, QWidget* w2)
{
p->setTabOrder(w1, w2);
return w2;
}
///
/// Truncates the precision of the value to the specified number of digits
/// after the decimal place.
///
/// The value to truncate
/// The number of digits to leave after the decimal place
/// The truncated value
static double TruncPrecision(double val, uint digits)
{
const auto mult = std::pow(10, digits);
return std::round(mult * val) / mult;
}
///
/// Wrapper around QLocale::system().toDouble().
///
/// The string to convert
/// Pointer to boolean which stores the success value of the conversion
/// The converted value if successful, else 0.
static double ToDouble(const QString& s, bool* ok)
{
return QLocale::system().toDouble(s, ok);
}
///
/// Wrapper around QLocale::system().toString().
///
/// The value to convert
/// The string value if successful, else "".
template
static QString ToString(T val)
{
return QLocale::system().toString(val);
}
///
/// Force a QString to end with the specified value.
///
/// The string to append a suffix to
/// The suffix to append
/// The original string value if it already ended in e, else the original value appended with e.
template
static QString MakeEnd(const QString& s, T e)
{
if (!s.endsWith(e))
return s + e;
else
return s;
}
///
/// Check if a path is not empty and exists on the file system.
///
/// The path to check
/// True if s was not empty and existed, else false.
static bool Exists(const QString& s)
{
return s != "" && QDir(s).exists();
}
///
/// Convert a color to one that is displayable on any background.
///
/// The color to convert
/// The converted color
static QColor VisibleColor(const QColor& color)
{
constexpr int threshold = 105;
const auto delta = (color.red() * 0.299) + //Magic numbers gotten from a Stack Overflow post.
(color.green() * 0.587) +
(color.blue() * 0.114);
return (255 - delta < threshold) ? QColor(0, 0, 0) : QColor(255, 255, 255);
}
///
/// Determine whether an xform in an ember is linked to any other xform
/// in the ember.
///
/// The ember which contains the xform
/// The xform to inspect
/// The index of the xform that the xform argument is linked to, else -1
template
static intmax_t IsXformLinked(Ember& ember, Xform* xform)
{
const auto count = ember.XformCount();
const auto index = ember.GetXformIndex(xform);
intmax_t linked = -1;
size_t toOneCount = 0;
size_t toZeroCount = 0;
size_t toOneIndex = 0;
size_t fromOneCount = 0;
size_t fromZeroCount = 0;
size_t fromOneIndex = 0;
if (index >= 0)
{
for (auto i = 0; i < count; i++)
{
if (xform->Xaos(i) == 0)
toZeroCount++;
else if (xform->Xaos(i) == 1)
{
toOneIndex = i;
toOneCount++;
}
}
if ((toZeroCount == (count - 1)) && toOneCount == 1)
{
for (auto i = 0; i < count; i++)
{
if (auto fromXform = ember.GetXform(i))
{
if (fromXform->Xaos(toOneIndex) == 0)
fromZeroCount++;
else if (fromXform->Xaos(toOneIndex) == 1)
{
fromOneIndex = i;
fromOneCount++;
}
}
}
if ((fromZeroCount == (count - 1)) && fromOneCount == 1)
{
linked = toOneIndex;
}
}
}
return linked;
}
///
/// Convert the passed in QList of absolute device indices to a vector> of platform,device
/// index pairs.
///
/// The absolute device indices
/// The converted device vector of platform,device index pairs
static vector> Devices(const QList& selectedDevices)
{
vector> vec;
auto& devices = OpenCLInfo::Instance()->DeviceIndices();
vec.reserve(selectedDevices.size());
for (int i = 0; i < selectedDevices.size(); i++)
{
const auto index = selectedDevices[i].toUInt();
if (index < devices.size())
vec.push_back(devices[index]);
}
return vec;
}
///
/// Setup a table showing all available OpenCL devices on the system.
/// Create checkboxes and radio buttons which allow the user to specify
/// which devices to use, and which one to make the primary device.
/// Used in the options dialog and the final render dialog.
///
/// The QTableWidget to setup
/// The absolute indices of the devices to use, with the first being the primary.
static void SetupDeviceTable(QTableWidget* table, const QList& settingsDevices)
{
bool primary = false;
auto& deviceNames = OpenCLInfo::Instance()->AllDeviceNames();
table->clearContents();
table->setRowCount(static_cast(deviceNames.size()));
for (int i = 0; i < deviceNames.size(); i++)
{
const auto checkItem = new QTableWidgetItem();
const auto radio = new QRadioButton();
const auto deviceItem = new QTableWidgetItem(QString::fromStdString(deviceNames[i]));
table->setItem(i, 0, checkItem);
table->setCellWidget(i, 1, radio);
table->setItem(i, 2, deviceItem);
if (settingsDevices.contains(QVariant::fromValue(i)))
{
checkItem->setCheckState(Qt::Checked);
if (!primary)
{
radio->setChecked(true);
primary = true;
}
}
else
checkItem->setCheckState(Qt::Unchecked);
}
if (!primary && table->rowCount() > 0)//Primary was never set, so just default to the first device and hope it was the one detected as the main display.
{
table->item(0, 0)->setCheckState(Qt::Checked);
qobject_cast(table->cellWidget(0, 1))->setChecked(true);
}
}
///
/// Copy the passed in selected absolute device indices to the controls on the passed in table.
/// Used in the options dialog and the final render dialog.
///
/// The QTableWidget to copy values to
/// The absolute indices of the devices to use, with the first being the primary.
static void SettingsToDeviceTable(QTableWidget* table, const QList& settingsDevices)
{
if (settingsDevices.empty() && table->rowCount() > 0)
{
table->item(0, 0)->setCheckState(Qt::Checked);
qobject_cast(table->cellWidget(0, 1))->setChecked(true);
for (auto row = 1; row < table->rowCount(); row++)
if (auto item = table->item(row, 0))
item->setCheckState(Qt::Unchecked);
}
else
{
for (auto row = 0; row < table->rowCount(); row++)
{
if (const auto item = table->item(row, 0))
{
if (settingsDevices.contains(row))
{
item->setCheckState(Qt::Checked);
if (!settingsDevices.indexOf(QVariant::fromValue(row)))
if (const auto radio = qobject_cast(table->cellWidget(row, 1)))
radio->setChecked(true);
}
else
{
item->setCheckState(Qt::Unchecked);
}
}
}
}
}
///
/// Copy the values of the controls on the passed in table to a list of absolute device indices.
/// Used in the options dialog and the final render dialog.
///
/// The QTableWidget to copy values from
/// The list of absolute device indices
static QList DeviceTableToSettings(QTableWidget* table)
{
QList devices;
const auto rows = table->rowCount();
for (int row = 0; row < rows; row++)
{
const auto checkItem = table->item(row, 0);
const auto radio = qobject_cast(table->cellWidget(row, 1));
if (checkItem->checkState() == Qt::Checked)
{
if (radio && radio->isChecked())
devices.push_front(row);
else
devices.push_back(row);
}
}
return devices;
}
///
/// Ensure device selection on the passed in table make sense.
///
/// The QTableWidget to setup
/// The row of the cell
/// The column of the cell
static void HandleDeviceTableCheckChanged(QTableWidget* table, int row, int col)
{
auto primaryRow = -1;
QRadioButton* primaryRadio = nullptr;
for (auto i = 0; i < table->rowCount(); i++)
{
if (const auto radio = qobject_cast(table->cellWidget(i, 1)))
{
if (radio->isChecked())
{
primaryRow = i;
primaryRadio = radio;
break;
}
}
}
if (primaryRow == -1) primaryRow = 0;
if (const auto primaryItem = table->item(primaryRow, 0))
if (primaryItem->checkState() == Qt::Unchecked)
primaryItem->setCheckState(Qt::Checked);
}
///
/// Set a row in a table to represent a palette.
/// This will place the palette name as a string value in the first column,
/// and a QPixmap representing the palette in the second column.
///
/// The table write to the row to
/// A pointer to the palette to write to the row
/// The row to write the palette to
static void AddPaletteToTable(QTableWidget* paletteTable, Palette* palette, int row)
{
const auto v = palette->MakeRgbPaletteBlock(PALETTE_CELL_HEIGHT);
auto nameCol = std::make_unique(palette->m_Name.c_str());
nameCol->setToolTip(palette->m_Name.c_str());
nameCol->setFlags(palette->m_SourceColors.empty() ? (Qt::ItemIsEnabled | Qt::ItemIsSelectable)
: (Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable));
paletteTable->setItem(row, 0, nameCol.release());
const QImage image(v.data(), static_cast(palette->Size()), PALETTE_CELL_HEIGHT, QImage::Format_RGB888);
auto paletteItem = std::make_unique(palette);
paletteItem->setData(Qt::DecorationRole, QPixmap::fromImage(image));
paletteItem->setFlags(paletteItem->flags() & ~Qt::ItemIsEditable);
paletteTable->setItem(row, 1, paletteItem.release());
}
///
/// Read a palette Xml file and populate the palette table with the contents.
/// This will clear any previous contents.
/// Called upon initialization, palette combo index change, and controller type change.
///
/// The name of the palette file without the path
/// The table to populate
/// The global PaletteList shared_ptr to retrieve the specified palette file from to populate the table with
/// True if successful, else false.
static bool FillPaletteTable(const string& s, QTableWidget* paletteTable, shared_ptr> paletteList)
{
if (!s.empty())//This occasionally seems to get called with an empty string for reasons unknown.
{
if (const auto palettes = paletteList->GetPaletteListByFilename(s))
{
paletteTable->clear();
paletteTable->blockSignals(true);
paletteTable->setRowCount(static_cast(palettes->size()));
//Headers get removed when clearing, so must re-create here.
auto nameHeader = std::make_unique("Name");
auto paletteHeader = std::make_unique("Palette");
nameHeader->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
paletteHeader->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
paletteTable->setHorizontalHeaderItem(0, nameHeader.release());
paletteTable->setHorizontalHeaderItem(1, paletteHeader.release());
//Palette list table.
for (auto i = 0; i < palettes->size(); i++)
if (const auto palette = &(*palettes)[i])
AddPaletteToTable(paletteTable, palette, i);
paletteTable->blockSignals(false);
return true;
}
}
return false;
}
///
/// Get the default search paths for config and palette files.
///
/// vector of paths
static vector GetDefaultPaths()
{
static vector paths =
{
(QDir::homePath() + "/AppData/Roaming/Fractorium").toLocal8Bit().data(),
#ifndef _WIN32
QString("/usr/share/fractorium").toLocal8Bit().data(),
QString("/usr/local/share/fractorium").toLocal8Bit().data(),
(QDir::homePath() + "/.config/fractorium").toLocal8Bit().data(),
#endif
QDir::currentPath().toLocal8Bit().data(),
QCoreApplication::applicationDirPath().toLocal8Bit().data()
};
return paths;
}
///
/// Get the default user path for config and palette files.
///
/// vector of paths
static QString GetDefaultUserPath()
{
#ifdef _WIN32
return (QDir::homePath() + "/AppData/Roaming/Fractorium").toLocal8Bit().data();
#else
return (QDir::homePath() + "/.config/fractorium").toLocal8Bit().data();
#endif
}
///
/// Get the first flam3-palettes.xml file in the default search paths.
///
/// The full path and filename if found, else empty string.
static QString FindFirstDefaultPalette()
{
const auto paths = GetDefaultPaths();
for (auto& path : paths)
{
const auto full = path + "/flam3-palettes.xml";
if (QFile::exists(full))
return full;
}
return "";
}
///
/// The basic style that is needed for things to look right, this varies by OS.
///
/// The base style
static QString BaseStyle()
{
return "/*---Base Style---\n"
"This may be needed to deal with the large tabs.\n"
"You should keep this at the top of whatever custom style you make to ensure the tabs aren't unusually large.*/\n"
"/*\n"
#ifndef _WIN32
"QTabBar::tab { height: 4ex; }\n"
#else
"QTabBar::tab { height: 4ex; }\n"
#endif
"*/\n"
"*\n"
"{\n"
"\tfont: 8.5pt \"MS Shell Dlg 2\";/*For some reason the font changes if you set any style. Set this to whatever font is the default on your system*/\n"
"}\n"
"\n"
"/*This is needed to give the labels on the status bar some padding.*/\n"
"QStatusBar QLabel { padding-left: 2px; padding-right: 2px; }\n"
"\n"
"Fractorium\n"
"{\n"
"\tqproperty-VariationTreeColorNonZero:black;\n"
"\tqproperty-VariationTreeColorZero:black;\n"
"\tqproperty-VariationTreeBgColorNonZero:lightgray;\n"
"\tqproperty-VariationTreeBgColorZero:white;\n"
"}\n"
"\n"
"/*For some reason, Qt does not draw table headers correctly, so the style must always be manually specified.*/\n"
"QHeaderView::section::vertical:enabled\n"
"{\n"
"\tcolor: black;\n"
"\tbackground-color: lightgray;\n"
"\tborder: none;\n"
"\tborder-bottom: 1px solid gray;\n"
"\tpadding-left: 5px;\n"
"\tpadding-right: 0px;\n"
"\tpadding-top: 0px;\n"
"\tpadding-bottom: 0px;\n"
"}\n"
"\n"
"QHeaderView::section::horizontal:enabled\n"
"{\n"
"\tcolor: black;\n"
"\tbackground-color: lightgray;\n"
"\tborder: 1px solid darkgray;\n"
"\tborder-right: 1px solid gray;\n"
"\tpadding-top: 0px;\n"
"\tpadding-bottom: 0px;\n"
"\tpadding-left: 3px;\n"
"}\n"
"\n"
"QHeaderView::section::vertical:disabled\n"
"{\n"
"\tcolor: rgb(35, 35, 35);\n"
"\tbackground-color: rgb(53, 53, 53);\n"
"\tborder: 0px none darkgray;\n"
"\tborder-bottom: 1px solid rgb(53, 53, 53);\n"
"\tpadding-left: 5px;\n"
"\tpadding-right: 0px;\n"
"\tpadding-top: 0px;\n"
"\tpadding-bottom: 0px;\n"
"}\n"
"\n"
"QHeaderView::section::horizontal:disabled\n"
"{\n"
"\tcolor:rgb(35, 35, 35);\n"
"\tbackground-color: rgb(53, 53, 53);\n"
"\tborder: 0px none darkgray;\n"
"\tborder-right: 0px solid rgb(53, 53, 53);\n"
"\tpadding-top: 0px;\n"
"\tpadding-bottom: 0px;\n"
"\tpadding-left: 3px;\n"
"}\n"
;
}
///
/// Get all parent objects of the passed in widget.
///
/// The widget whose parents will be retrieved
/// The entire parent object chain in a QList
template
static QList GetAllParents(QWidget* widget)
{
QList parents;
if (widget != nullptr)
{
while (const auto parent = qobject_cast(widget->parent()))
{
if (const auto parentT = qobject_cast(parent))
parents.push_back(parentT);
widget = parent;
}
}
return parents;
}
///
/// Constrain the value in low to be less than or equal to the value in high.
/// Template expected to be any control which has member functions value() and setValue().
/// Most likely QSpinBox or QDoubleSpinbox.
///
/// The control which must contain the lower value
/// The control which must contain the higher value
/// True if the value of low had to be changed, else false.
template
bool ConstrainLow(T* low, T* high)
{
if (low->value() > high->value())
{
low->blockSignals(true);
low->setValue(high->value());
low->blockSignals(false);
return true;
}
return false;
}
///
/// Constrain the value in high to be greater than or equal to the value in low.
/// Template expected to be any control which has member functions value() and setValue().
/// Most likely QSpinBox or QDoubleSpinbox.
///
/// The control which must contain the lower value
/// The control which must contain the higher value
/// True if the value of high had to be changed, else false.
template
bool ConstrainHigh(T* low, T* high)
{
if (high->value() < low->value())
{
high->blockSignals(true);
high->setValue(low->value());
high->blockSignals(false);
return true;
}
return false;
}
///
/// Move all, possibly disjointly, selected items in a range to
/// a new location and update all existing locations.
/// Atribution: Sean Parent, Going Native 2013.
///
/// The location of the first selected item, or the start of the collection.
/// The location of the last selected item, or the end of the collection.
/// A pair of iterators representing the start and end locations of the list of newly moved items
template
pair Gather(I f, I l, I p, S s)
{
return
{
stable_partition(f, p, [&](const typename iterator_traits::value_type & x) { return !s(x); }),
stable_partition(p, l, s)
};
}