diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 325dcf1..11a772e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,10 +5,6 @@ on: pull_request: workflow_dispatch: -env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - BUILD_TYPE: Release - # for matrix check https://docs.github.com/en/actions/reference/specifications-for-github-hosted-runners jobs: prepare_matrix: @@ -35,14 +31,14 @@ jobs: steps: - name: Checkout the source if: github.event_name != 'pull_request' - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: recursive fetch-depth: 0 - name: Checkout the source - pull request if: github.event_name == 'pull_request' - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: recursive fetch-depth: 0 @@ -59,7 +55,7 @@ jobs: python-version: "3.12" - name: Start ssh key agent - uses: webfactory/ssh-agent@v0.9.0 + uses: webfactory/ssh-agent@v0.10.0 with: ssh-private-key: ${{ secrets.RULESSUPPORT_DEPLOY_KEY }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 37b66ea..d29e075 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,12 +13,10 @@ PROJECT(${PROJECT} # ----------------------------------------------------------------------------- # CMake Options # ----------------------------------------------------------------------------- -set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_AUTOMOC ON) if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DWIN32 /EHsc /MP /permissive- /Zc:__cplusplus") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DWIN32 /EHsc /MP /Zc:__cplusplus") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MDd") set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /MD") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MD") @@ -31,12 +29,16 @@ find_package(Qt6 COMPONENTS Widgets WebEngineWidgets REQUIRED) find_package(ManiVault COMPONENTS Core PointData CONFIG QUIET) +find_package(OpenMP) + # ----------------------------------------------------------------------------- # Source files # ----------------------------------------------------------------------------- set(SOURCES src/PointDataConversionPlugin.h src/PointDataConversionPlugin.cpp + src/SlidersAction.h + src/SlidersAction.cpp ) set(AUX @@ -60,6 +62,10 @@ target_include_directories(${PROJECT} PRIVATE "${ManiVault_INCLUDE_DIR}") # ----------------------------------------------------------------------------- target_compile_features(${PROJECT} PRIVATE cxx_std_20) +set_target_properties(${PROJECT} PROPERTIES + AUTOMOC ON +) + # ----------------------------------------------------------------------------- # Target library linking # ----------------------------------------------------------------------------- @@ -68,6 +74,7 @@ target_link_libraries(${PROJECT} PRIVATE Qt6::WebEngineWidgets) target_link_libraries(${PROJECT} PRIVATE ManiVault::Core) target_link_libraries(${PROJECT} PRIVATE ManiVault::PointData) +target_link_libraries(${PROJECT} PRIVATE OpenMP::OpenMP_CXX) # ----------------------------------------------------------------------------- # Target installation diff --git a/conanfile.py b/conanfile.py index f1b7ff6..d118966 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,6 +1,7 @@ from conans import ConanFile from conan.tools.cmake import CMakeDeps, CMake, CMakeToolchain from conans.tools import save, load +from conans.tools import os_info, SystemPackageTool import os import shutil import pathlib @@ -9,7 +10,7 @@ class PointDataConversionPluginConan(ConanFile): - """Class to package the PointDataConversionPlugin using conan + """Class to package the PointDataConversionPlugin using conan Packages both RELEASE and RELWITHDEBINFO. Uses rules_support (github.com/ManiVaultStudio/rulessupport) to derive @@ -68,8 +69,13 @@ def configure(self): pass def system_requirements(self): - # May be needed for macOS or Linux - pass + if os_info.is_macos: + installer = SystemPackageTool() + installer.install("libomp") + proc = subprocess.run("brew --prefix libomp", shell=True, capture_output=True) + subprocess.run(f"ln {proc.stdout.decode('UTF-8').strip()}/lib/libomp.dylib /usr/local/lib/libomp.dylib", shell=True) + if os_info.is_linux: + self.run("sudo apt update && sudo apt install -y libtbb-dev") def config_options(self): if self.settings.os == "Windows": @@ -100,8 +106,13 @@ def generate(self): tc.variables["ManiVault_DIR"] = manivault_dir # Set some build options - tc.variables["MV_UNITY_BUILD"] = "ON" + tc.cache_variables["MV_UNITY_BUILD"] = "ON" + if os_info.is_macos: + proc = subprocess.run("brew --prefix libomp", shell=True, capture_output=True) + prefix_path = f"{proc.stdout.decode('UTF-8').strip()}" + tc.variables["OpenMP_ROOT"] = prefix_path + tc.generate() def _configure_cmake(self): diff --git a/src/PointDataConversionPlugin.cpp b/src/PointDataConversionPlugin.cpp index bfa1dd2..b255ceb 100644 --- a/src/PointDataConversionPlugin.cpp +++ b/src/PointDataConversionPlugin.cpp @@ -4,6 +4,8 @@ #include +#include +#include #include #include @@ -11,15 +13,19 @@ Q_PLUGIN_METADATA(IID "studio.manivault.PointDataConversionPlugin") using namespace mv; +using namespace mv::gui; -const QMap PointDataConversionPlugin::types = QMap({ - { Type::Log2, "Log2" }, - { Type::ArcSin, "Arcsin" } +// ============================================================================= +// PointDataConversionPlugin +// ============================================================================= + +const QMap PointDataConversionPlugin::CONVERSIONS = QMap({ + { Conversion::Log2, "Log2" }, + { Conversion::ArcSin, "Arcsin" } }); -PointDataConversionPlugin::PointDataConversionPlugin(const PluginFactory* factory) : - TransformationPlugin(factory), - _type(Type::ArcSin) +PointDataConversionPlugin::PointDataConversionPlugin(const mv::plugin::PluginFactory* factory) : + TransformationPlugin(factory) { } @@ -36,33 +42,52 @@ void PointDataConversionPlugin::transform() task.setName("Converting"); task.setRunning(); - task.setProgressDescription(QString("%1 conversion").arg(getTypeName(_type))); + task.setProgressDescription(QString("%1 conversion").arg(getConversionName(_conversion))); points->visitData([this, &points, &task](auto pointData) { - std::uint32_t noPointsProcessed = 0; - - for (auto point : pointData) { - for (std::int32_t dimensionIndex = 0; dimensionIndex < points->getNumDimensions(); dimensionIndex++) { - switch (_type) + std::atomic_uint64_t noPointsProcessed = 0; + + const auto numPointsF = static_cast(points->getNumPoints()); + const auto numPointsI = static_cast(points->getNumPoints()); + const auto numDims = points->getNumDimensions(); + + assert(!_cofactors.empty()); + assert(_cofactors.size() == 1 || _cofactors.size() == numDims); + + if (_cofactors.size() == 1) + qDebug() << "PointDataConversionPlugin::transform: cofactor of" << _cofactors[0]; + else + qDebug() << "PointDataConversionPlugin::transform: cofactors of" << _cofactors; + +#pragma omp parallel for + for (std::int64_t pointIndex = 0; pointIndex < numPointsI; pointIndex++) { + + for (std::uint64_t dimensionIndex = 0; dimensionIndex < numDims; dimensionIndex++) { + switch (_conversion) { - case Type::Log2: - point[dimensionIndex] = std::log2f(point[dimensionIndex] + 1.0f); - break; - - case Type::ArcSin: - point[dimensionIndex] = std::asinhf(point[dimensionIndex] / 5.0f); - break; + case Conversion::Log2: + pointData[pointIndex][dimensionIndex] = std::log2f(pointData[pointIndex][dimensionIndex] + 1.0f); + break; + + case Conversion::ArcSin: + + const float cofactor = (_cofactors.size() == 1) ? _cofactors[0] : _cofactors[dimensionIndex]; + + pointData[pointIndex][dimensionIndex] = std::asinhf(pointData[pointIndex][dimensionIndex] / cofactor); + break; } } - - ++noPointsProcessed; - - if (noPointsProcessed % 1000 == 0) { - task.setProgress(static_cast(noPointsProcessed) / static_cast(points->getNumPoints())); - - QApplication::processEvents(); + + if (const auto processed = ++noPointsProcessed; + processed % 1000 == 0) { +#pragma omp critical + { + task.setProgress(static_cast(noPointsProcessed) / numPointsF); + QApplication::processEvents(); + } } } + }); task.setProgress(1.0f); @@ -71,27 +96,59 @@ void PointDataConversionPlugin::transform() events().notifyDatasetDataChanged(points); } -PointDataConversionPlugin::Type PointDataConversionPlugin::getType() const +PointDataConversionPlugin::Conversion PointDataConversionPlugin::getConversion() const { - return _type; + return _conversion; } -void PointDataConversionPlugin::setType(const Type& type) +void PointDataConversionPlugin::setConversion(const Conversion& conversion) { - if (type == _type) + if (conversion == _conversion) return; - _type = type; + _conversion = conversion; } -QString PointDataConversionPlugin::getTypeName(const Type& type) +QString PointDataConversionPlugin::getConversionName(const Conversion& conversion) { - return types[type]; + return CONVERSIONS[conversion]; } +// ============================================================================= +// PointDataConversionPluginFactory +// ============================================================================= + PointDataConversionPluginFactory::PointDataConversionPluginFactory() : - _arcSinFactorAction(this, "Factor", 1.0f, 100.0f, 5.0f, 5.0f) + _sameFactorAction(this, "Same factor", true), + _arcSinFactorAction(this, "Factor"), + _arcSinFactorsAction(this, "Factors") { + _arcSinFactorAction.setToolTip("Apply the same cofactors to each channel."); + _arcSinFactorsAction.setToolTip("Apply different cofactors to each channel."); + + _arcSinFactorAction.initialize(SlidersAction::EntryData::DEFAULT_MIN, SlidersAction::EntryData::DEFAULT_MAX, + SlidersAction::EntryData::DEFAULT_VALUE, SlidersAction::EntryData::DEFAULT_DECIMALS); + + connect(&_sameFactorAction, &ToggleAction::toggled, this, [&](bool toggled) + { + _arcSinFactorAction.setEnabled(_sameFactorAction.isChecked()); + _arcSinFactorsAction.setAllSlidersEnabled(!_sameFactorAction.isChecked()); + }); + + connect(&_arcSinFactorAction, &DecimalAction::valueChanged, this, [&](float value) + { + const auto sliderValues = _arcSinFactorsAction.getValues(); + + const bool allEqual = !sliderValues.empty() && + std::all_of(sliderValues.cbegin() + 1, sliderValues.cend(), + [&](const float v) { return std::abs(v - sliderValues.front()) < 0.0001f; }); + + if (!allEqual) + return; + + _arcSinFactorsAction.setValueForAllEntries(value); + }); + } PointDataConversionPlugin* PointDataConversionPluginFactory::produce() @@ -99,53 +156,50 @@ PointDataConversionPlugin* PointDataConversionPluginFactory::produce() return new PointDataConversionPlugin(this); } -PluginTriggerActions PointDataConversionPluginFactory::getPluginTriggerActions(const mv::Datasets& datasets) const +std::vector PointDataConversionPluginFactory::getArcSinCoFactor() const { - PluginTriggerActions pluginTriggerActions; + if (_sameFactorAction.isChecked()) + return { _arcSinFactorAction.getValue() }; - const auto numberOfDatasets = datasets.count(); + return _arcSinFactorsAction.getValues(); +} - if (PluginFactory::areAllDatasetsOfTheSameType(datasets, PointType)) { - if (numberOfDatasets >= 1 && datasets.first()->getDataType() == PointType) { - const auto addPluginTriggerAction = [this, &pluginTriggerActions, datasets](const PointDataConversionPlugin::Type& type) -> void { - const auto typeName = PointDataConversionPlugin::getTypeName(type); +PluginTriggerActions PointDataConversionPluginFactory::getPluginTriggerActions(const mv::Datasets& datasets) const +{ + PluginTriggerActions pluginTriggerActions; - auto pluginTriggerAction = new PluginTriggerAction(const_cast(this), this, QString("Conversion/%1").arg(typeName), QString("Perform %1 data conversion").arg(typeName), icon(), [this, datasets, type](PluginTriggerAction& pluginTriggerAction) -> void { - for (const auto& dataset : datasets) { - auto pluginInstance = dynamic_cast(plugins().requestPlugin(getKind())); + if (datasets.count() >= 1 && PluginFactory::areAllDatasetsOfTheSameType(datasets, PointType)) { + const auto addPluginTriggerAction = [this, &pluginTriggerActions, datasets](const PointDataConversionPlugin::Conversion& type) -> void { + const auto typeName = PointDataConversionPlugin::getConversionName(type); - pluginInstance->setInputDataset(dataset); - pluginInstance->setType(type); - pluginInstance->transform(); - } + auto pluginTriggerAction = new PluginTriggerAction(const_cast(this), this, QString("Conversion/%1").arg(typeName), QString("Perform %1 data conversion").arg(typeName), icon(), [this, datasets, type](PluginTriggerAction& pluginTriggerAction) -> void { + for (const auto& dataset : datasets) { + const_cast(this)->openConfigDialog(type, dataset); + } }); - pluginTriggerActions << pluginTriggerAction; + pluginTriggerActions << pluginTriggerAction; }; - addPluginTriggerAction(PointDataConversionPlugin::Type::Log2); - addPluginTriggerAction(PointDataConversionPlugin::Type::ArcSin); - } + addPluginTriggerAction(PointDataConversionPlugin::Conversion::Log2); + addPluginTriggerAction(PointDataConversionPlugin::Conversion::ArcSin); } return pluginTriggerActions; } +// This is used in e.g. the image viewer PluginTriggerActions PointDataConversionPluginFactory::getPluginTriggerActions(const mv::DataTypes& dataTypes) const { PluginTriggerActions pluginTriggerActions; if (dataTypes.count(PointType) == dataTypes.count()) { - const auto addPluginTriggerAction = [this, &pluginTriggerActions](const PointDataConversionPlugin::Type& type) -> void { - const auto typeName = PointDataConversionPlugin::getTypeName(type); + const auto addPluginTriggerAction = [this, &pluginTriggerActions](const PointDataConversionPlugin::Conversion& type) -> void { + const auto typeName = PointDataConversionPlugin::getConversionName(type); auto pluginTriggerAction = new PluginTriggerAction(const_cast(this), this, QString("Conversion/%1").arg(typeName), QString("Perform %1 data conversion").arg(typeName), icon(), [this, type](PluginTriggerAction& pluginTriggerAction) -> void { for (const auto& dataset : pluginTriggerAction.getDatasets()) { - auto pluginInstance = dynamic_cast(plugins().requestPlugin(getKind())); - - pluginInstance->setInputDataset(dataset); - pluginInstance->setType(type); - pluginInstance->transform(); + createPluginAndTransform(type, dataset); } }); @@ -154,34 +208,103 @@ PluginTriggerActions PointDataConversionPluginFactory::getPluginTriggerActions(c pluginTriggerActions << pluginTriggerAction; }; - addPluginTriggerAction(PointDataConversionPlugin::Type::Log2); - addPluginTriggerAction(PointDataConversionPlugin::Type::ArcSin); + addPluginTriggerAction(PointDataConversionPlugin::Conversion::Log2); + addPluginTriggerAction(PointDataConversionPlugin::Conversion::ArcSin); } return pluginTriggerActions; } -WidgetAction* PointDataConversionPluginFactory::getConfigurationAction(const PointDataConversionPlugin::Type& type) +WidgetAction* PointDataConversionPluginFactory::getConfigurationAction(const PointDataConversionPlugin::Conversion& type) { - const auto createGroupAction = [this](WidgetAction& widgetAction) -> GroupAction* { + const auto createGroupAction = [this]() -> GroupAction* { + + _arcSinFactorsAction.initialize({}); + _sameFactorAction.setChecked(true); + auto groupAction = new GroupAction(this, "PointDataConversionGroupAction"); groupAction->setText("Settings"); groupAction->setToolTip("Data conversion settings"); groupAction->setLabelSizingType(GroupAction::LabelSizingType::Auto); - groupAction->addAction(&widgetAction); + groupAction->addAction(&_arcSinFactorAction); return groupAction; }; switch (type) { - case PointDataConversionPlugin::Type::Log2: + case PointDataConversionPlugin::Conversion::Log2: return nullptr; - case PointDataConversionPlugin::Type::ArcSin: - return createGroupAction(_arcSinFactorAction); + case PointDataConversionPlugin::Conversion::ArcSin: + return createGroupAction(); } return nullptr; } + + +void PointDataConversionPluginFactory::openConfigDialog(const PointDataConversionPlugin::Conversion& type, const mv::Dataset& inputDataset) +{ + _sameFactorAction.setChecked(true); + const std::vector dimNamesVec = mv::Dataset(inputDataset)->getDimensionNames(); + const QStringList dimNamesList(dimNamesVec.begin(), dimNamesVec.end()); + _arcSinFactorsAction.initialize(dimNamesList); + + switch (type) + { + case PointDataConversionPlugin::Conversion::Log2: + createPluginAndTransform(type, inputDataset); + break; + + case PointDataConversionPlugin::Conversion::ArcSin: + { + ConversionDialog inputDialog(nullptr, &_sameFactorAction, &_arcSinFactorAction, &_arcSinFactorsAction); + inputDialog.setModal(true); + if (inputDialog.exec() == QDialog::Accepted) + createPluginAndTransform(type, inputDataset); + + break; + } + } + +} + +void PointDataConversionPluginFactory::createPluginAndTransform(const PointDataConversionPlugin::Conversion& type, const mv::Dataset& inputDataset) const +{ + auto pluginInstance = dynamic_cast(plugins().requestPlugin(getKind())); + + pluginInstance->setInputDataset(inputDataset); + pluginInstance->setConversion(type); + pluginInstance->setCofactor(getArcSinCoFactor()); + pluginInstance->transform(); + +} + +// ============================================================================= +// Helper +// ============================================================================= + +ConversionDialog::ConversionDialog(QWidget* parent, ToggleAction* sameFactorAction, DecimalAction* arcSinFactorAction, SlidersAction* arcSinFactorsAction) : + QDialog(parent), _conversionButton(this, "Convert") +{ + setWindowTitle(tr("Data conversion settings")); + + connect(&_conversionButton, &TriggerAction::triggered, this, &ConversionDialog::closeDialogAction); + + auto* layout = new QHBoxLayout(); + + auto groupAction = new GroupAction(this, "PointDataConversionGroupAction"); + + groupAction->setText("Settings"); + groupAction->setToolTip("Data conversion settings"); + groupAction->setLabelSizingType(GroupAction::LabelSizingType::Auto); + groupAction->addAction(sameFactorAction); + groupAction->addAction(arcSinFactorAction); + groupAction->addAction(arcSinFactorsAction); + groupAction->addAction(&_conversionButton); + + layout->addWidget(groupAction->createWidget(this)); + setLayout(layout); +} diff --git a/src/PointDataConversionPlugin.h b/src/PointDataConversionPlugin.h index 6745b59..6d15b31 100644 --- a/src/PointDataConversionPlugin.h +++ b/src/PointDataConversionPlugin.h @@ -1,31 +1,35 @@ #pragma once -#include #include +#include + +#include #include -using namespace mv::plugin; -using namespace mv::gui; -using namespace mv::util; +#include "SlidersAction.h" + +#include +#include +#include /** * Point data conversion plugin class * * @author Thomas Kroes */ -class PointDataConversionPlugin : public TransformationPlugin +class PointDataConversionPlugin : public mv::plugin::TransformationPlugin { Q_OBJECT public: /** Point data conversion type */ - enum class Type { + enum class Conversion { Log2, /** log2(value+1) */ ArcSin /** asinh(value/factor) */ }; - static const QMap types; + static const QMap CONVERSIONS; public: @@ -33,39 +37,42 @@ class PointDataConversionPlugin : public TransformationPlugin * Constructor * @param factory Pointer to the plugin factory */ - PointDataConversionPlugin(const PluginFactory* factory); + PointDataConversionPlugin(const mv::plugin::PluginFactory* factory); /** Destructor */ ~PointDataConversionPlugin() override = default; /** Initialization is called when the plugin is first instantiated. */ - void init() override {}; + void init() override {} /** Performs the data transformation */ void transform() override; - /** + /** Set the sinh cofactors */ + void setCofactor(std::vector cofactors) { _cofactors = std::move(cofactors); } + /** * Get point data conversion type * @return Point data conversion type */ - Type getType() const; + Conversion getConversion() const; /** * Set point data conversion type - * @param type Point data conversion type + * @param conversion Point data conversion type */ - void setType(const Type& type); + void setConversion(const Conversion& conversion); /** * Get string representation of type enum - * @param type Point data conversion type - * @return Type name + * @param conversion Point data conversion type + * @return conversion name */ - static QString getTypeName(const Type& type); + static QString getConversionName(const Conversion& conversion); private: - Type _type; /** Data conversion type */ + Conversion _conversion = Conversion::ArcSin; + std::vector _cofactors = { 5.f }; }; /** @@ -73,7 +80,7 @@ class PointDataConversionPlugin : public TransformationPlugin * * @author Thomas Kroes */ -class PointDataConversionPluginFactory : public TransformationPluginFactory +class PointDataConversionPluginFactory : public mv::plugin::TransformationPluginFactory { Q_INTERFACES(mv::plugin::TransformationPluginFactory mv::plugin::PluginFactory) Q_OBJECT @@ -96,21 +103,59 @@ class PointDataConversionPluginFactory : public TransformationPluginFactory * @param datasets Vector of input datasets * @return Vector of plugin trigger actions */ - PluginTriggerActions getPluginTriggerActions(const mv::Datasets& datasets) const override; + mv::gui::PluginTriggerActions getPluginTriggerActions(const mv::Datasets& datasets) const override; /** * Get plugin trigger actions given \p dataTypes - * @param datasetTypes Vector of input data types + * @param dataTypes Vector of input data types * @return Vector of plugin trigger actions */ - PluginTriggerActions getPluginTriggerActions(const mv::DataTypes& dataTypes) const override; + mv::gui::PluginTriggerActions getPluginTriggerActions(const mv::DataTypes& dataTypes) const override; /** * Get configuration action for \p type * @return Pointer to configuration action (may be null) */ - WidgetAction* getConfigurationAction(const PointDataConversionPlugin::Type& type); + WidgetAction* getConfigurationAction(const PointDataConversionPlugin::Conversion& type); + + /** + * Show option dialog and run transform + */ + void openConfigDialog(const PointDataConversionPlugin::Conversion& type, const mv::Dataset& inputDataset); + + /** + * Create a transformation plugin and apply transformation + */ + void createPluginAndTransform(const PointDataConversionPlugin::Conversion& type, const mv::Dataset& inputDataset) const; + +private: + std::vector getArcSinCoFactor() const; + +private: + mv::gui::ToggleAction _sameFactorAction; + mv::gui::DecimalAction _arcSinFactorAction; + mv::gui::SlidersAction _arcSinFactorsAction; +}; + +/** + * Helper dialog to set conversion options + * + * @author Alex Vieth + */ +class ConversionDialog : public QDialog +{ + Q_OBJECT +public: + explicit ConversionDialog(QWidget* parent, mv::gui::ToggleAction* sameFactorAction, mv::gui::DecimalAction* arcSinFactorAction, mv::gui::SlidersAction* arcSinFactorsAction); + +signals: + void closeDialog(bool onlyIndices); + +private slots: + void closeDialogAction() { + emit QDialog::accept(); + } private: - DecimalAction _arcSinFactorAction; /** Factor for arcsin(value/factor) conversion */ + mv::gui::TriggerAction _conversionButton; }; diff --git a/src/SlidersAction.cpp b/src/SlidersAction.cpp new file mode 100644 index 0000000..12a96ce --- /dev/null +++ b/src/SlidersAction.cpp @@ -0,0 +1,166 @@ +#include "SlidersAction.h" + +#include + +#include +#include + +using namespace mv::gui; + +SlidersAction::SlidersAction(QObject* parent, const QString& title) : + WidgetAction(parent, title) +{ + setText(title); + setDefaultWidgetFlags(SlidersAction::DisableOnFirstOpen); +} + +void SlidersAction::initialize(const QStringList& entries) { + setEntries(entries); + setAllEntriesToDefault(); +} + +void SlidersAction::setEntries(const QStringList& entries) { + _entries = entries; + // populate default EntryData if new + for (const QString& opt : entries) { + if (!_entryData.contains(opt)) { + _entryData.insert({ opt, EntryData{} }); + } + } + // remove data for removed entries + for (auto it = _entryData.begin(); it != _entryData.end(); ) { + if (!entries.contains(it->first)) + it = _entryData.erase(it); + else ++it; + } +} + +void SlidersAction::setAllEntriesToDefault() +{ + for (const auto& [name, data] : _entryData) + setDataForEntry(name, 5.f, 0.f, 100.f); + +} + +void SlidersAction::setRangeForEntry(const QString& entry, float minimum, float maximum) +{ + if (!_entryData.contains(entry)) return; + auto& d = _entryData[entry]; + d.min = minimum; + d.max = maximum; + d.value = std::clamp(d.value, d.min, d.max); +} + +void SlidersAction::setValueForAllEntries(float value) +{ + for (const auto& [name, data] : _entryData) + setValueForEntry(name, value); + + setAllSlidersValues(value); +} + +void SlidersAction::setValueForEntry(const QString& entry, float value) +{ + if (!_entryData.contains(entry)) return; + auto& d = _entryData[entry]; + value = std::clamp(value, d.min, d.max); + if (std::abs(d.value - value) < 0.0001f) return; + d.value = value; + emit entryValueChanged(entry, value); +} + +void SlidersAction::setDataForEntry(const QString& entry, float value, float minimum, float maximum) +{ + setRangeForEntry(entry, minimum, maximum); + setValueForEntry(entry, value); +} + +float SlidersAction::getValueForEntry(const QString& entry) const +{ + if (!_entryData.contains(entry)) + return 0.0f; + return _entryData.at(entry).value; +} + +std::vector SlidersAction::getValues() const +{ + std::vector values; + values.reserve(_entries.size()); + + for (const auto& opt : _entries) + values.push_back(getValueForEntry(opt)); + + return values; +} + +void SlidersAction::setAllSlidersEnabled(bool enabled) +{ + setEnabled(enabled); + + if (!_sliderList) + return; + + for (int i = 0; i < _sliderList->count(); ++i) { + QListWidgetItem* item = _sliderList->item(i); + if (QWidget* row = _sliderList->itemWidget(item)) { + row->setEnabled(enabled); + } + } + +} + +void SlidersAction::setAllSlidersValues(float value) +{ + if (!_sliderList) + return; + + for (int i = 0; i < _sliderList->count(); ++i) { + QListWidgetItem* item = _sliderList->item(i); + if (QWidget* row = _sliderList->itemWidget(item)) { + if (auto* slider = row->findChild()) { + slider->setValue(value); + } + } + } + +} + +QWidget* SlidersAction::getWidget(QWidget* parent, const std::int32_t& widgetFlags) +{ + auto* container = new QWidget(parent); + auto* layout = new QVBoxLayout(container); + layout->setContentsMargins(4, 4, 4, 4); + + const bool disableAll = _sliderList == nullptr && widgetFlags == WidgetFlag::DisableOnFirstOpen; + + _sliderList = new QListWidget(container); + _sliderList->setSelectionMode(QAbstractItemView::NoSelection); + layout->addWidget(_sliderList); + + for (const QString& opt : _entries) { + auto* item = new QListWidgetItem(_sliderList); + + QWidget* row = new QWidget(_sliderList); + QHBoxLayout* rowLayout = new QHBoxLayout(row); + rowLayout->setContentsMargins(2, 2, 2, 2); + + const EntryData& d = _entryData.at(opt); + + DecimalAction* slider = new DecimalAction(row, opt, d.min, d.max, d.value, EntryData::DEFAULT_DECIMALS); + rowLayout->addWidget(slider->createLabelWidget(container)); + rowLayout->addWidget(slider->createWidget(container)); + + _sliderList->addItem(item); + _sliderList->setItemWidget(item, row); + item->setSizeHint(row->sizeHint()); + + connect(slider, &DecimalAction::valueChanged, this, [this, opt](float value) { + setValueForEntry(opt, value); + }); + } + + if (disableAll) + setAllSlidersEnabled(false); + + return container; +} diff --git a/src/SlidersAction.h b/src/SlidersAction.h new file mode 100644 index 0000000..5f9ea4a --- /dev/null +++ b/src/SlidersAction.h @@ -0,0 +1,67 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace mv::gui { + + class SlidersAction : public WidgetAction { + Q_OBJECT + + public: + /** Describes the widget flags */ + enum WidgetFlag { + Default = 0x00001, + DisableOnFirstOpen = 0x00002, + }; + + public: + struct EntryData { + static constexpr float DEFAULT_MIN = 1.0f; + static constexpr float DEFAULT_MAX = 100.0f; + static constexpr float DEFAULT_VALUE = 5.0f; + static constexpr std::int32_t DEFAULT_DECIMALS = 2; + + float min = DEFAULT_MIN; + float max = DEFAULT_MAX; + float value = DEFAULT_VALUE; + }; + + public: + explicit SlidersAction(QObject* parent, const QString& title); + + void initialize(const QStringList& entries = QStringList()); + void setEntries(const QStringList& entries); + void setAllEntriesToDefault(); + + void setRangeForEntry(const QString& entry, float minimum, float maximum); + void setValueForEntry(const QString& entry, float value); + void setValueForAllEntries(float value); // also updates sliders + void setDataForEntry(const QString& entry, float value, float minimum, float maximum); + [[nodiscard]] float getValueForEntry(const QString& entry) const; + [[nodiscard]] std::vector getValues() const; + + void setAllSlidersEnabled(bool enabled); + void setAllSlidersValues(float value); + + signals: + void entryValueChanged(const QString& entry, float value); + + protected: + QWidget* getWidget(QWidget* parent, const std::int32_t& widgetFlags) override; + + private: + QStringList _entries = {}; + std::unordered_map _entryData = {}; + QListWidget* _sliderList = nullptr; + + }; +}