Files
create/src/Mod/Material/Gui/MaterialsEditor.cpp
David Carter 00c57a9d08 Materials: External Modules Part 1
Refactored code to support local and external material sources

This is the first PR in a series to support external modules. External
modules allow materials to be stored in external data sources such as
databases or web services. No new functionality is introduced in this
PR, rather it is a refactoring of code that will allow for changes to
be introduced in future PRs. Minor performance improvements have also
been made in the model and material managers.

The Python API has been enhanced for many data types to allow for
modification within Python.
2025-03-18 20:33:10 -05:00

1359 lines
43 KiB
C++

/***************************************************************************
* Copyright (c) 2023 David Carter <dcarter@david.carter.ca> *
* *
* This file is part of FreeCAD. *
* *
* FreeCAD is free software: you can redistribute it and/or modify it *
* under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation, either version 2.1 of the *
* License, or (at your option) any later version. *
* *
* FreeCAD is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with FreeCAD. If not, see *
* <https://www.gnu.org/licenses/>. *
* *
**************************************************************************/
#include "PreCompiled.h"
#ifndef _PreComp_
#include <QColorDialog>
#include <QDesktopServices>
#include <QIODevice>
#include <QItemSelectionModel>
#include <QMenu>
#include <QMessageBox>
#include <QString>
#include <QStringList>
#include <QTextStream>
#include <QVariant>
#include <limits>
#endif
#include <App/Application.h>
#include <App/License.h>
#include <Base/Interpreter.h>
#include <Base/Quantity.h>
#include <Gui/Application.h>
#include <Gui/Command.h>
#include <Gui/InputField.h>
#include <Gui/PrefWidgets.h>
#include <Gui/SpinBox.h>
#include <Gui/WaitCursor.h>
#include <Mod/Material/App/Exceptions.h>
#include <Mod/Material/App/MaterialLibrary.h>
#include <Mod/Material/App/ModelManager.h>
#include <Mod/Material/App/ModelUuids.h>
#include "MaterialDelegate.h"
#include "MaterialSave.h"
#include "MaterialsEditor.h"
#include "ModelSelect.h"
#include "ui_MaterialsEditor.h"
using namespace MatGui;
/* TRANSLATOR MatGui::MaterialsEditor */
MaterialsEditor::MaterialsEditor(std::shared_ptr<Materials::MaterialFilter> filter, QWidget* parent)
: QDialog(parent)
, ui(new Ui_MaterialsEditor)
, _material(std::make_shared<Materials::Material>())
, _rendered(nullptr)
, _materialSelected(false)
, _recentMax(0)
, _filter(filter)
{
setup();
}
MaterialsEditor::MaterialsEditor(QWidget* parent)
: QDialog(parent)
, ui(new Ui_MaterialsEditor)
, _material(std::make_shared<Materials::Material>())
, _rendered(nullptr)
, _materialSelected(false)
, _recentMax(0)
, _filter(nullptr)
{
setup();
}
void MaterialsEditor::setup()
{
Gui::WaitCursor wc;
ui->setupUi(this);
_warningIcon = QIcon(QStringLiteral(":/icons/Warning.svg"));
getFavorites();
getRecents();
createMaterialTree();
createPhysicalTree();
createAppearanceTree();
createPreviews();
setMaterialDefaults();
// Reset to previous size
auto param = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Material/Editor");
auto width = param->GetInt("EditorWidth", 835);
auto height = param->GetInt("EditorHeight", 542);
resize(width, height);
ui->buttonURL->setIcon(QIcon(QStringLiteral(":/icons/internet-web-browser.svg")));
connect(ui->standardButtons->button(QDialogButtonBox::Ok),
&QPushButton::clicked,
this,
&MaterialsEditor::onOk);
connect(ui->standardButtons->button(QDialogButtonBox::Cancel),
&QPushButton::clicked,
this,
&MaterialsEditor::onCancel);
connect(ui->standardButtons->button(QDialogButtonBox::Save),
&QPushButton::clicked,
this,
&MaterialsEditor::onSave);
connect(ui->editName, &QLineEdit::textEdited, this, &MaterialsEditor::onName);
connect(ui->editAuthor, &QLineEdit::textEdited, this, &MaterialsEditor::onAuthor);
connect(ui->editLicense, &QLineEdit::textEdited, this, &MaterialsEditor::onLicense);
connect(ui->editSourceURL, &QLineEdit::textEdited, this, &MaterialsEditor::onSourceURL);
connect(ui->editSourceReference,
&QLineEdit::textEdited,
this,
&MaterialsEditor::onSourceReference);
connect(ui->editDescription, &QTextEdit::textChanged, this, &MaterialsEditor::onDescription);
connect(ui->buttonURL, &QPushButton::clicked, this, &MaterialsEditor::onURL);
connect(ui->buttonPhysicalAdd, &QPushButton::clicked, this, &MaterialsEditor::onPhysicalAdd);
connect(ui->buttonPhysicalRemove,
&QPushButton::clicked,
this,
&MaterialsEditor::onPhysicalRemove);
connect(ui->buttonAppearanceAdd,
&QPushButton::clicked,
this,
&MaterialsEditor::onAppearanceAdd);
connect(ui->buttonAppearanceRemove,
&QPushButton::clicked,
this,
&MaterialsEditor::onAppearanceRemove);
connect(ui->buttonInheritNew,
&QPushButton::clicked,
this,
&MaterialsEditor::onInheritNewMaterial);
connect(ui->buttonNew, &QPushButton::clicked, this, &MaterialsEditor::onNewMaterial);
connect(ui->buttonFavorite, &QPushButton::clicked, this, &MaterialsEditor::onFavourite);
QItemSelectionModel* selectionModel = ui->treeMaterials->selectionModel();
connect(selectionModel,
&QItemSelectionModel::selectionChanged,
this,
&MaterialsEditor::onSelectMaterial);
connect(ui->treeMaterials, &QTreeView::doubleClicked, this, &MaterialsEditor::onDoubleClick);
// Disabled for now. This will be revisited post 1.0
#if 0
ui->treeMaterials->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->treeMaterials,
&QWidget::customContextMenuRequested,
this,
&MaterialsEditor::onContextMenu);
#endif
}
void MaterialsEditor::getFavorites()
{
_favorites.clear();
auto param = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Material/Favorites");
int count = param->GetInt("Favorites", 0);
for (int i = 0; static_cast<long>(i) < count; i++) {
QString key = QStringLiteral("FAV%1").arg(i);
QString uuid = QString::fromStdString(param->GetASCII(key.toStdString().c_str(), ""));
if (!_filter || _filter->modelIncluded(uuid)) {
_favorites.push_back(uuid);
}
}
}
void MaterialsEditor::saveFavorites()
{
auto param = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Material/Favorites");
// Clear out the existing favorites
int count = param->GetInt("Favorites", 0);
for (int i = 0; static_cast<long>(i) < count; i++) {
QString key = QStringLiteral("FAV%1").arg(i);
param->RemoveASCII(key.toStdString().c_str());
}
// Add the current values
param->SetInt("Favorites", _favorites.size());
int j = 0;
for (auto& favorite : _favorites) {
QString key = QStringLiteral("FAV%1").arg(j);
param->SetASCII(key.toStdString().c_str(), favorite.toStdString());
j++;
}
}
void MaterialsEditor::addFavorite(const QString& uuid)
{
// Ensure it is a material. New, unsaved materials will not be
try {
auto material = Materials::MaterialManager::getManager().getMaterial(uuid);
Q_UNUSED(material)
}
catch (const Materials::MaterialNotFound&) {
return;
}
if (!isFavorite(uuid)) {
_favorites.push_back(uuid);
saveFavorites();
refreshMaterialTree();
}
}
void MaterialsEditor::removeFavorite(const QString& uuid)
{
if (isFavorite(uuid)) {
_favorites.remove(uuid);
saveFavorites();
refreshMaterialTree();
}
}
bool MaterialsEditor::isFavorite(const QString& uuid) const
{
for (auto& it : _favorites) {
if (it == uuid) {
return true;
}
}
return false;
}
void MaterialsEditor::getRecents()
{
_recents.clear();
auto param = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Material/Recent");
_recentMax = param->GetInt("RecentMax", 5);
int count = param->GetInt("Recent", 0);
for (int i = 0; static_cast<long>(i) < count; i++) {
QString key = QStringLiteral("MRU%1").arg(i);
QString uuid = QString::fromStdString(param->GetASCII(key.toStdString().c_str(), ""));
if (!_filter || _filter->modelIncluded(uuid)) {
_recents.push_back(uuid);
}
}
}
void MaterialsEditor::saveRecents()
{
auto param = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Material/Recent");
// Clear out the existing favorites
int count = param->GetInt("Recent", 0);
for (int i = 0; static_cast<long>(i) < count; i++) {
QString key = QStringLiteral("MRU%1").arg(i);
param->RemoveASCII(key.toStdString().c_str());
}
// Add the current values
int size = _recents.size();
if (size > _recentMax) {
size = _recentMax;
}
param->SetInt("Recent", size);
int j = 0;
for (auto& recent : _recents) {
QString key = QStringLiteral("MRU%1").arg(j);
param->SetASCII(key.toStdString().c_str(), recent.toStdString());
j++;
if (j >= size) {
break;
}
}
}
void MaterialsEditor::addRecent(const QString& uuid)
{
// Ensure it is a material. New, unsaved materials will not be
try {
auto material = Materials::MaterialManager::getManager().getMaterial(uuid);
Q_UNUSED(material)
}
catch (const Materials::MaterialNotFound&) {
return;
}
// Ensure no duplicates
if (isRecent(uuid)) {
_recents.remove(uuid);
}
_recents.push_front(uuid);
while (_recents.size() > static_cast<std::size_t>(_recentMax)) {
_recents.pop_back();
}
saveRecents();
}
bool MaterialsEditor::isRecent(const QString& uuid) const
{
for (auto& it : _recents) {
if (it == uuid) {
return true;
}
}
return false;
}
void MaterialsEditor::onName(const QString& text)
{
_material->setName(text);
}
void MaterialsEditor::onAuthor(const QString& text)
{
_material->setAuthor(text);
}
void MaterialsEditor::onLicense(const QString& text)
{
_material->setLicense(text);
}
void MaterialsEditor::onSourceURL(const QString& text)
{
_material->setURL(text);
}
void MaterialsEditor::onSourceReference(const QString& text)
{
_material->setReference(text);
}
void MaterialsEditor::onDescription()
{
_material->setDescription(ui->editDescription->toPlainText());
}
void MaterialsEditor::propertyChange(const QString& property, const QVariant& value)
{
if (_material->hasPhysicalProperty(property)) {
_material->setPhysicalValue(property, value);
}
else if (_material->hasAppearanceProperty(property)) {
_material->setAppearanceValue(property, value);
updatePreview();
}
update();
}
void MaterialsEditor::onURL(bool checked)
{
Q_UNUSED(checked)
QString url = ui->editSourceURL->text();
if (url.length() > 0) {
QDesktopServices::openUrl(QUrl(url, QUrl::TolerantMode));
}
}
void MaterialsEditor::onPhysicalAdd(bool checked)
{
Q_UNUSED(checked)
ModelSelect dialog(this, Materials::ModelFilter_Physical);
dialog.setModal(true);
if (dialog.exec() == QDialog::Accepted) {
QString selected = dialog.selectedModel();
_material->addPhysical(selected);
updateMaterial();
}
else {
Base::Console().Log("No model selected\n");
}
}
void MaterialsEditor::onPhysicalRemove(bool checked)
{
Q_UNUSED(checked)
QItemSelectionModel* selectionModel = ui->treePhysicalProperties->selectionModel();
if (selectionModel->hasSelection()) {
auto index = selectionModel->currentIndex().siblingAtColumn(0);
auto treeModel = dynamic_cast<const QStandardItemModel*>(index.model());
// Check we're the material model root.
auto item = treeModel->itemFromIndex(index);
auto group = item->parent();
if (!group) {
QString propertyName = index.data().toString();
QString uuid = _material->getModelByName(propertyName);
_material->removePhysical(uuid);
updateMaterial();
}
}
}
void MaterialsEditor::onAppearanceAdd(bool checked)
{
Q_UNUSED(checked)
ModelSelect dialog(this, Materials::ModelFilter_Appearance);
dialog.setModal(true);
if (dialog.exec() == QDialog::Accepted) {
QString selected = dialog.selectedModel();
_material->addAppearance(selected);
auto model = Materials::ModelManager::getManager().getModel(selected);
if (selected == Materials::ModelUUIDs::ModelUUID_Rendering_Basic
|| model->inherits(Materials::ModelUUIDs::ModelUUID_Rendering_Basic)) {
// Add default appearance properties
*_material = *(getMaterialManager().defaultAppearance());
}
updateMaterial();
}
else {
Base::Console().Log("No model selected\n");
}
}
void MaterialsEditor::onAppearanceRemove(bool checked)
{
Q_UNUSED(checked)
QItemSelectionModel* selectionModel = ui->treeAppearance->selectionModel();
if (selectionModel->hasSelection()) {
auto index = selectionModel->currentIndex().siblingAtColumn(0);
auto treeModel = dynamic_cast<const QStandardItemModel*>(index.model());
// Check we're the material model root.
auto item = treeModel->itemFromIndex(index);
auto group = item->parent();
if (!group) {
QString propertyName = index.data().toString();
QString uuid = _material->getModelByName(propertyName);
_material->removeAppearance(uuid);
updateMaterial();
}
}
}
void MaterialsEditor::onFavourite(bool checked)
{
Q_UNUSED(checked)
auto selected = _material->getUUID();
if (isFavorite(selected)) {
removeFavorite(selected);
}
else {
addFavorite(selected);
}
}
void MaterialsEditor::setMaterialDefaults()
{
_material->setName(tr("Unnamed"));
std::string Author = App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
->GetASCII("prefAuthor", "");
_material->setAuthor(QString::fromStdString(Author));
// license stuff
auto paramGrp {App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Document")};
auto index = static_cast<int>(paramGrp->GetInt("prefLicenseType", 0));
const char* name = App::licenseItems.at(index).at(App::posnOfFullName);
// const char* url = App::licenseItems.at(index).at(App::posnOfUrl);
// std::string licenseUrl = (paramGrp->GetASCII("prefLicenseUrl", url));
_material->setLicense(QLatin1String(name));
// Empty materials will have no parent
Materials::MaterialManager::getManager().dereference(_material);
updateMaterial();
_material->resetEditState();
}
void MaterialsEditor::onNewMaterial(bool checked)
{
Q_UNUSED(checked)
// Ensure data is saved (or discarded) before changing materials
if (_material->getEditState() != Materials::Material::ModelEdit_None) {
// Prompt the user to save or discard changes
int res = confirmSave(this);
if (res == QMessageBox::Cancel) {
return;
}
}
// Create a new material
_material = std::make_shared<Materials::Material>();
setMaterialDefaults();
_materialSelected = false;
}
void MaterialsEditor::onInheritNewMaterial(bool checked)
{
Q_UNUSED(checked)
// Save the current UUID to use as out parent
auto parent = _material->getUUID();
// Ensure data is saved (or discarded) before changing materials
if (_material->getEditState() != Materials::Material::ModelEdit_None) {
// Prompt the user to save or discard changes
int res = confirmSave(this);
if (res == QMessageBox::Cancel) {
return;
}
}
// Create a new material
_material = std::make_shared<Materials::Material>();
_material->setParentUUID(parent);
setMaterialDefaults();
}
void MaterialsEditor::onOk(bool checked)
{
Q_UNUSED(checked)
// Ensure data is saved (or discarded) before exiting
if (_material->getEditState() != Materials::Material::ModelEdit_None) {
// Prompt the user to save or discard changes
int res = confirmSave(this);
if (res == QMessageBox::Cancel) {
return;
}
}
accept();
}
void MaterialsEditor::onCancel(bool checked)
{
Q_UNUSED(checked)
reject();
}
void MaterialsEditor::onSave(bool checked)
{
Q_UNUSED(checked)
saveMaterial();
}
void MaterialsEditor::saveMaterial()
{
MaterialSave dialog(_material, this);
dialog.setModal(true);
if (dialog.exec() == QDialog::Accepted) {
updateMaterialGeneral();
_material->resetEditState();
refreshMaterialTree();
_materialSelected = true;
}
}
void MaterialsEditor::accept()
{
if (_material->isOldFormat()) {
Base::Console().Log("*** Old Format File ***\n");
oldFormatError();
return;
}
addRecent(_material->getUUID());
saveWindow();
QDialog::accept();
}
void MaterialsEditor::oldFormatError()
{
QMessageBox box(this);
box.setIcon(QMessageBox::Warning);
box.setWindowTitle(tr("Old Format Material"));
box.setText(tr("This file is in the old material card format."));
box.setInformativeText(QObject::tr("You must save the material before using it."));
box.adjustSize(); // Silence warnings from Qt on Windows
box.exec();
}
void MaterialsEditor::reject()
{
saveWindow();
QDialog::reject();
}
void MaterialsEditor::saveWindow()
{
auto param = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Material/Editor");
param->SetInt("EditorWidth", width());
param->SetInt("EditorHeight", height());
saveMaterialTree(param);
}
void MaterialsEditor::saveMaterialTreeChildren(const Base::Reference<ParameterGrp>& param,
QTreeView* tree,
QStandardItemModel* model,
QStandardItem* item)
{
if (item->hasChildren()) {
param->SetBool(item->text().toStdString().c_str(), tree->isExpanded(item->index()));
auto treeParam = param->GetGroup(item->text().toStdString().c_str());
for (int i = 0; i < item->rowCount(); i++) {
auto child = item->child(i);
saveMaterialTreeChildren(treeParam, tree, model, child);
}
}
}
void MaterialsEditor::saveMaterialTree(const Base::Reference<ParameterGrp>& param)
{
auto treeParam = param->GetGroup("MaterialTree");
treeParam->Clear();
auto tree = ui->treeMaterials;
auto model = dynamic_cast<QStandardItemModel*>(tree->model());
auto root = model->invisibleRootItem();
for (int i = 0; i < root->rowCount(); i++) {
auto child = root->child(i);
// treeParam->SetBool(child->text().toStdString().c_str(),
// tree->isExpanded(child->index()));
saveMaterialTreeChildren(treeParam, tree, model, child);
}
}
void MaterialsEditor::addMaterials(
QStandardItem& parent,
const std::shared_ptr<std::map<QString, std::shared_ptr<Materials::MaterialTreeNode>>>
materialTree,
const QIcon& folderIcon,
const QIcon& icon,
const Base::Reference<ParameterGrp>& param)
{
auto childParam = param->GetGroup(parent.text().toStdString().c_str());
auto tree = ui->treeMaterials;
for (auto& mat : *materialTree) {
std::shared_ptr<Materials::MaterialTreeNode> nodePtr = mat.second;
if (nodePtr->getType() == Materials::MaterialTreeNode::NodeType::DataNode) {
QString uuid = nodePtr->getUUID();
auto material = nodePtr->getData();
if (!material) {
material = Materials::MaterialManager::getManager().getMaterial(uuid);
nodePtr->setData(material);
}
QIcon matIcon = icon;
if (material->isOldFormat()) {
matIcon = _warningIcon;
}
auto card = new QStandardItem(matIcon, mat.first);
card->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled
| Qt::ItemIsDropEnabled);
card->setData(QVariant(uuid), Qt::UserRole);
if (material->isOldFormat()) {
card->setToolTip(tr("This card uses the old format and must be saved before use"));
}
addExpanded(tree, &parent, card);
}
else {
auto node = new QStandardItem(folderIcon, mat.first);
addExpanded(tree, &parent, node, childParam);
node->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled);
auto treeMap = nodePtr->getFolder();
// if (treeMap) {
addMaterials(*node, treeMap, folderIcon, icon, childParam);
// }
}
}
}
void MaterialsEditor::addExpanded(QTreeView* tree, QStandardItem* parent, QStandardItem* child)
{
parent->appendRow(child);
tree->setExpanded(child->index(), true);
}
void MaterialsEditor::addExpanded(QTreeView* tree,
QStandardItem* parent,
QStandardItem* child,
const Base::Reference<ParameterGrp>& param)
{
parent->appendRow(child);
// Restore to any previous expansion state
auto expand = param->GetBool(child->text().toStdString().c_str(), true);
tree->setExpanded(child->index(), expand);
}
void MaterialsEditor::addExpanded(QTreeView* tree, QStandardItemModel* parent, QStandardItem* child)
{
parent->appendRow(child);
tree->setExpanded(child->index(), true);
}
void MaterialsEditor::addExpanded(QTreeView* tree,
QStandardItemModel* parent,
QStandardItem* child,
const Base::Reference<ParameterGrp>& param)
{
parent->appendRow(child);
// Restore to any previous expansion state
auto expand = param->GetBool(child->text().toStdString().c_str(), true);
tree->setExpanded(child->index(), expand);
}
void MaterialsEditor::createPhysicalTree()
{
auto tree = ui->treePhysicalProperties;
auto model = new QStandardItemModel();
tree->setModel(model);
QStringList headers;
headers.append(tr("Property"));
headers.append(tr("Value"));
headers.append(tr("Type"));
model->setHorizontalHeaderLabels(headers);
tree->setColumnWidth(0, 250);
tree->setColumnWidth(1, 250);
tree->setColumnHidden(2, true);
tree->setHeaderHidden(false);
tree->setUniformRowHeights(false);
auto delegate = new MaterialDelegate(this);
tree->setItemDelegateForColumn(1, delegate);
connect(delegate, &MaterialDelegate::propertyChange, this, &MaterialsEditor::propertyChange);
}
void MaterialsEditor::createPreviews()
{
_rendered = new AppearancePreview();
ui->layoutAppearance->addWidget(_rendered);
updatePreview();
}
void MaterialsEditor::createAppearanceTree()
{
auto tree = ui->treeAppearance;
auto model = new QStandardItemModel();
tree->setModel(model);
QStringList headers;
headers.append(tr("Property"));
headers.append(tr("Value"));
headers.append(tr("Type"));
model->setHorizontalHeaderLabels(headers);
tree->setColumnWidth(0, 250);
tree->setColumnWidth(1, 250);
tree->setColumnHidden(2, true);
tree->setHeaderHidden(false);
tree->setUniformRowHeights(false);
auto delegate = new MaterialDelegate(this);
tree->setItemDelegateForColumn(1, delegate);
connect(delegate, &MaterialDelegate::propertyChange, this, &MaterialsEditor::propertyChange);
}
void MaterialsEditor::addRecents(QStandardItem* parent)
{
auto tree = ui->treeMaterials;
for (auto& uuid : _recents) {
try {
auto material = getMaterialManager().getMaterial(uuid);
// if (material->getLibrary()->isLocal()) {
QIcon icon = QIcon(material->getLibrary()->getIconPath());
auto card = new QStandardItem(icon, libraryPath(material));
card->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled
| Qt::ItemIsDropEnabled);
card->setData(QVariant(uuid), Qt::UserRole);
addExpanded(tree, parent, card);
// }
}
catch (const Materials::MaterialNotFound&) {
}
}
}
void MaterialsEditor::addFavorites(QStandardItem* parent)
{
auto tree = ui->treeMaterials;
for (auto& uuid : _favorites) {
try {
auto material = getMaterialManager().getMaterial(uuid);
QIcon icon = QIcon(material->getLibrary()->getIconPath());
auto card = new QStandardItem(icon, libraryPath(material));
card->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled
| Qt::ItemIsDropEnabled);
card->setData(QVariant(uuid), Qt::UserRole);
addExpanded(tree, parent, card);
}
catch (const Materials::MaterialNotFound&) {
}
}
}
void MaterialsEditor::fillMaterialTree()
{
auto param = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Material/Editor/MaterialTree");
auto tree = ui->treeMaterials;
auto model = dynamic_cast<QStandardItemModel*>(tree->model());
if (_filterOptions.includeFavorites()) {
auto lib = new QStandardItem(tr("Favorites"));
lib->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled);
addExpanded(tree, model, lib, param);
addFavorites(lib);
}
if (_filterOptions.includeRecent()) {
auto lib = new QStandardItem(tr("Recent"));
lib->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled);
addExpanded(tree, model, lib, param);
addRecents(lib);
}
auto libraries = getMaterialManager().getLibraries();
for (const auto& library : *libraries) {
auto materialTree = getMaterialManager().getMaterialTree(library);
bool showLibraries = _filterOptions.includeEmptyLibraries();
if (!_filterOptions.includeEmptyLibraries() && materialTree->size() > 0) {
showLibraries = true;
}
if (showLibraries) {
auto lib = new QStandardItem(library->getName());
lib->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled);
addExpanded(tree, model, lib, param);
QIcon icon(library->getIconPath());
QIcon folderIcon(QStringLiteral(":/icons/folder.svg"));
addMaterials(*lib, materialTree, folderIcon, icon, param);
}
}
}
void MaterialsEditor::createMaterialTree()
{
auto tree = ui->treeMaterials;
auto model = new QStandardItemModel();
tree->setModel(model);
tree->setHeaderHidden(true);
fillMaterialTree();
}
void MaterialsEditor::refreshMaterialTree()
{
auto tree = ui->treeMaterials;
auto model = dynamic_cast<QStandardItemModel*>(tree->model());
model->clear();
fillMaterialTree();
}
bool MaterialsEditor::updateTexturePreview() const
{
bool hasImage = false;
QImage image;
// double scaling = 99.0;
if (_material->hasModel(Materials::ModelUUIDs::ModelUUID_Rendering_Texture)) {
// First try loading an embedded image
try {
auto property = _material->getAppearanceProperty(QStringLiteral("TextureImage"));
if (!property->isNull()) {
// Base::Console().Log("Has 'TextureImage'\n");
auto propertyValue = property->getString();
if (!propertyValue.isEmpty()) {
QByteArray by = QByteArray::fromBase64(propertyValue.toUtf8());
image = QImage::fromData(by, "PNG"); //.scaled(64, 64, Qt::KeepAspectRatio);
hasImage = true;
}
}
}
catch (const Materials::PropertyNotFound&) {
}
// If no embedded image, load from a path
if (!hasImage) {
try {
auto property = _material->getAppearanceProperty(QStringLiteral("TexturePath"));
if (!property->isNull()) {
// Base::Console().Log("Has 'TexturePath'\n");
auto filePath = property->getString();
if (!image.load(filePath)) {
Base::Console().Log("Unable to load image '%s'\n",
filePath.toStdString().c_str());
// return; // ???
}
hasImage = true;
}
}
catch (const Materials::PropertyNotFound&) {
}
}
// Apply any scaling
try {
auto property = _material->getAppearanceProperty(QStringLiteral("TextureScaling"));
if (!property->isNull()) {
// scaling = property->getFloat();
// Base::Console().Log("Has 'TextureScaling' = %g\n", scaling);
}
}
catch (const Materials::PropertyNotFound&) {
}
if (hasImage) {
_rendered->setTexture(image);
}
}
return hasImage;
}
bool MaterialsEditor::updateMaterialPreview() const
{
if (_material->hasAppearanceProperty(QStringLiteral("AmbientColor"))) {
QString color = _material->getAppearanceValueString(QStringLiteral("AmbientColor"));
_rendered->setAmbientColor(getColorHash(color, 255));
}
else {
_rendered->resetAmbientColor();
}
if (_material->hasAppearanceProperty(QStringLiteral("DiffuseColor"))) {
QString color = _material->getAppearanceValueString(QStringLiteral("DiffuseColor"));
_rendered->setDiffuseColor(getColorHash(color, 255));
}
else {
_rendered->resetDiffuseColor();
}
if (_material->hasAppearanceProperty(QStringLiteral("SpecularColor"))) {
QString color = _material->getAppearanceValueString(QStringLiteral("SpecularColor"));
_rendered->setSpecularColor(getColorHash(color, 255));
}
else {
_rendered->resetSpecularColor();
}
if (_material->hasAppearanceProperty(QStringLiteral("EmissiveColor"))) {
QString color = _material->getAppearanceValueString(QStringLiteral("EmissiveColor"));
_rendered->setEmissiveColor(getColorHash(color, 255));
}
else {
_rendered->resetEmissiveColor();
}
if (_material->hasAppearanceProperty(QStringLiteral("Shininess"))) {
double value = _material->getAppearanceValue(QStringLiteral("Shininess")).toDouble();
_rendered->setShininess(value);
}
else {
_rendered->resetShininess();
}
if (_material->hasAppearanceProperty(QStringLiteral("Transparency"))) {
double value = _material->getAppearanceValue(QStringLiteral("Transparency")).toDouble();
_rendered->setTransparency(value);
}
else {
_rendered->resetTransparency();
}
return true;
}
void MaterialsEditor::updatePreview() const
{
if (updateTexturePreview()) {
return;
}
updateMaterialPreview();
}
QString MaterialsEditor::getColorHash(const QString& colorString, int colorRange)
{
/*
returns a '#000000' string from a '(0.1,0.2,0.3)' string. Optionally the string
has a fourth value for alpha (transparency)
*/
std::stringstream stream(colorString.toStdString());
char c;
stream >> c; // read "("
double red;
stream >> red;
stream >> c; // ","
double green;
stream >> green;
stream >> c; // ","
double blue;
stream >> blue;
stream >> c; // ","
double alpha = 1.0;
if (c == ',') {
stream >> alpha;
}
QColor color(static_cast<int>(red * colorRange),
static_cast<int>(green * colorRange),
static_cast<int>(blue * colorRange),
static_cast<int>(alpha * colorRange));
return color.name();
}
void MaterialsEditor::updateMaterialAppearance()
{
QTreeView* tree = ui->treeAppearance;
auto treeModel = dynamic_cast<QStandardItemModel*>(tree->model());
treeModel->clear();
QStringList headers;
headers.append(tr("Property"));
headers.append(tr("Value"));
headers.append(tr("Type"));
treeModel->setHorizontalHeaderLabels(headers);
tree->setColumnWidth(0, 250);
tree->setColumnWidth(1, 250);
tree->setColumnHidden(2, true);
auto models = _material->getAppearanceModels();
if (models) {
for (auto it = models->begin(); it != models->end(); it++) {
QString uuid = *it;
try {
auto model = Materials::ModelManager::getManager().getModel(uuid);
QString name = model->getName();
auto modelRoot = new QStandardItem(name);
modelRoot->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDragEnabled
| Qt::ItemIsDropEnabled);
addExpanded(tree, treeModel, modelRoot);
for (auto itp = model->begin(); itp != model->end(); itp++) {
QList<QStandardItem*> items;
QString key = itp->first;
// auto propertyItem = new QStandardItem(key);
auto propertyItem = new QStandardItem(itp->second.getDisplayName());
propertyItem->setData(key);
propertyItem->setToolTip(itp->second.getDescription());
items.append(propertyItem);
auto valueItem = new QStandardItem(_material->getAppearanceValueString(key));
valueItem->setToolTip(itp->second.getDescription());
QVariant variant;
// variant.setValue(_material->getAppearanceValueString(key));
variant.setValue(_material);
valueItem->setData(variant);
items.append(valueItem);
auto typeItem = new QStandardItem(itp->second.getPropertyType());
items.append(typeItem);
auto unitsItem = new QStandardItem(itp->second.getUnits());
items.append(unitsItem);
modelRoot->appendRow(items);
tree->setExpanded(modelRoot->index(), true);
}
}
catch (Materials::ModelNotFound const&) {
}
}
}
}
void MaterialsEditor::updateMaterialProperties()
{
QTreeView* tree = ui->treePhysicalProperties;
auto treeModel = dynamic_cast<QStandardItemModel*>(tree->model());
treeModel->clear();
QStringList headers;
headers.append(tr("Property"));
headers.append(tr("Value"));
headers.append(tr("Type"));
headers.append(tr("Units"));
treeModel->setHorizontalHeaderLabels(headers);
tree->setColumnWidth(0, 250);
tree->setColumnWidth(1, 250);
tree->setColumnHidden(2, true);
tree->setColumnHidden(3, true);
auto models = _material->getPhysicalModels();
if (models) {
for (auto it = models->begin(); it != models->end(); it++) {
QString uuid = *it;
try {
auto model = Materials::ModelManager::getManager().getModel(uuid);
QString name = model->getName();
auto modelRoot = new QStandardItem(name);
modelRoot->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDragEnabled
| Qt::ItemIsDropEnabled);
addExpanded(tree, treeModel, modelRoot);
for (auto itp = model->begin(); itp != model->end(); itp++) {
QList<QStandardItem*> items;
QString key = itp->first;
Materials::ModelProperty modelProperty =
static_cast<Materials::ModelProperty>(itp->second);
// auto propertyItem = new QStandardItem(key);
auto propertyItem = new QStandardItem(modelProperty.getDisplayName());
propertyItem->setData(key);
propertyItem->setToolTip(modelProperty.getDescription());
items.append(propertyItem);
auto valueItem = new QStandardItem(_material->getPhysicalValueString(key));
valueItem->setToolTip(modelProperty.getDescription());
QVariant variant;
variant.setValue(_material);
valueItem->setData(variant);
items.append(valueItem);
auto typeItem = new QStandardItem(modelProperty.getPropertyType());
items.append(typeItem);
auto unitsItem = new QStandardItem(modelProperty.getUnits());
items.append(unitsItem);
// addExpanded(tree, modelRoot, propertyItem);
modelRoot->appendRow(items);
tree->setExpanded(modelRoot->index(), true);
}
}
catch (Materials::ModelNotFound const&) {
}
}
}
}
QString MaterialsEditor::libraryPath(const std::shared_ptr<Materials::Material>& material)
{
QString path;
auto library = material->getLibrary();
if (library) {
path = QStringLiteral("/%1/%2/%3")
.arg(library->getName())
.arg(material->getDirectory())
.arg(material->getName());
return path;
}
path = QStringLiteral("%1/%2").arg(material->getDirectory()).arg(material->getName());
return path;
}
void MaterialsEditor::updateMaterialGeneral()
{
QString parentString;
try {
auto parent = Materials::MaterialManager::getManager().getParent(_material);
parentString = libraryPath(parent);
}
catch (const Materials::MaterialNotFound&) {
}
// Update the general information
ui->editName->setText(_material->getName());
ui->editAuthor->setText(_material->getAuthor());
ui->editLicense->setText(_material->getLicense());
ui->editParent->setText(parentString);
ui->editParent->setReadOnly(true);
ui->editSourceURL->setText(_material->getURL());
ui->editSourceReference->setText(_material->getReference());
// ui->editTags->setText(_material->getName());
ui->editDescription->setText(_material->getDescription());
}
void MaterialsEditor::updateMaterial()
{
updateMaterialGeneral();
updateMaterialProperties();
updateMaterialAppearance();
updatePreview();
}
void MaterialsEditor::onSelectMaterial(const QItemSelection& selected,
const QItemSelection& deselected)
{
Q_UNUSED(deselected);
// Get the UUID before changing the underlying data model
QString uuid;
auto model = dynamic_cast<QStandardItemModel*>(ui->treeMaterials->model());
QModelIndexList indexes = selected.indexes();
for (auto it = indexes.begin(); it != indexes.end(); it++) {
QStandardItem* item = model->itemFromIndex(*it);
if (item) {
uuid = item->data(Qt::UserRole).toString();
break;
}
}
if (uuid.isEmpty() || uuid == _material->getUUID()) {
return;
}
// Ensure data is saved (or discarded) before changing materials
if (_material->getEditState() != Materials::Material::ModelEdit_None) {
// Prompt the user to save or discard changes
int res = confirmSave(this);
if (res == QMessageBox::Cancel) {
return;
}
}
// Get the selected material
try {
_material = std::make_shared<Materials::Material>(*getMaterialManager().getMaterial(uuid));
}
catch (Materials::ModelNotFound const&) {
Base::Console().Log("*** Unable to load material '%s'\n", uuid.toStdString().c_str());
_material = std::make_shared<Materials::Material>();
}
updateMaterial();
_material->resetEditState();
_materialSelected = true;
}
void MaterialsEditor::onDoubleClick(const QModelIndex& index)
{
Q_UNUSED(index)
// Ensure data is saved (or discarded) before exiting
if (_material->getEditState() != Materials::Material::ModelEdit_None) {
// Prompt the user to save or discard changes
int res = confirmSave(this);
if (res == QMessageBox::Cancel) {
return;
}
}
_materialSelected = true;
accept();
}
void MaterialsEditor::onContextMenu(const QPoint& pos)
{
QMenu contextMenu(tr("Context menu"), this);
QAction action1(tr("Inherit from"), this);
connect(&action1, &QAction::triggered, this, &MaterialsEditor::onInherit);
contextMenu.addAction(&action1);
QAction action2(tr("Inherit new material"), this);
connect(&action2, &QAction::triggered, this, &MaterialsEditor::onInheritNew);
contextMenu.addAction(&action2);
contextMenu.exec(ui->treeMaterials->mapToGlobal(pos));
}
void MaterialsEditor::onInherit(bool checked)
{
Q_UNUSED(checked)
}
void MaterialsEditor::onInheritNew(bool checked)
{
Q_UNUSED(checked)
}
int MaterialsEditor::confirmSave(QWidget* parent)
{
QMessageBox box(parent ? parent : this);
box.setIcon(QMessageBox::Question);
box.setWindowTitle(QObject::tr("Unsaved Material"));
box.setText(QObject::tr("Do you want to save your changes to the material before closing?"));
box.setInformativeText(QObject::tr("If you don't save, your changes will be lost."));
box.setStandardButtons(QMessageBox::Discard | QMessageBox::Cancel | QMessageBox::Save);
box.setDefaultButton(QMessageBox::Save);
box.setEscapeButton(QMessageBox::Cancel);
// add shortcuts
QAbstractButton* saveBtn = box.button(QMessageBox::Save);
if (saveBtn->shortcut().isEmpty()) {
QString text = saveBtn->text();
text.prepend(QLatin1Char('&'));
saveBtn->setShortcut(QKeySequence::mnemonic(text));
}
QAbstractButton* discardBtn = box.button(QMessageBox::Discard);
if (discardBtn->shortcut().isEmpty()) {
QString text = discardBtn->text();
text.prepend(QLatin1Char('&'));
discardBtn->setShortcut(QKeySequence::mnemonic(text));
}
int res = QMessageBox::Cancel;
box.adjustSize(); // Silence warnings from Qt on Windows
switch (box.exec()) {
case QMessageBox::Save:
saveMaterial();
res = QMessageBox::Save;
break;
case QMessageBox::Discard:
res = QMessageBox::Discard;
break;
}
return res;
}
#include "moc_MaterialsEditor.cpp"