#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) }; }