Files
create/src/Gui/PropertyView.cpp
2025-11-11 13:49:01 +01:00

634 lines
22 KiB
C++

/***************************************************************************
* Copyright (c) 2002 Jürgen Riegel <juergen.riegel@web.de> *
* *
* This file is part of the FreeCAD CAx development system. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library 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 Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#include <QEvent>
#include <QGridLayout>
#include <QTimer>
#include <App/Document.h>
#include <App/DocumentObject.h>
#include <Base/Console.h>
#include <Base/Parameter.h>
#include <Base/Tools.h>
#include "PropertyView.h"
#include "Application.h"
#include "Document.h"
#include "MainWindow.h"
#include "SelectionObject.h"
#include "Tree.h"
#include "ViewParams.h"
#include "ViewProvider.h"
#include "ViewProviderDocumentObject.h"
#include "propertyeditor/PropertyEditor.h"
using namespace std;
using namespace Gui;
using namespace Gui::DockWnd;
using namespace Gui::PropertyEditor;
namespace sp = std::placeholders;
static ParameterGrp::handle _GetParam()
{
static ParameterGrp::handle hGrp;
if (!hGrp) {
hGrp = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/PropertyView"
);
}
return hGrp;
}
/* TRANSLATOR Gui::PropertyView */
/*! Property Editor Widget
*
* Provides two Gui::PropertyEditor::PropertyEditor widgets, for "View" and "Data",
* in two tabs.
*/
PropertyView::PropertyView(QWidget* parent)
: QWidget(parent)
, SelectionObserver(false, ResolveMode::NoResolve)
{
auto pLayout = new QGridLayout(this);
pLayout->setSpacing(0);
pLayout->setContentsMargins(0, 0, 0, 0);
timer = new QTimer(this);
timer->setSingleShot(true);
connect(timer, &QTimer::timeout, this, &PropertyView::onTimer);
tabs = new QTabWidget(this);
tabs->setObjectName(QStringLiteral("propertyTab"));
tabs->setTabPosition(QTabWidget::South);
pLayout->addWidget(tabs, 0, 0);
propertyEditorView = new Gui::PropertyEditor::PropertyEditor();
propertyEditorView->setObjectName(QStringLiteral("propertyEditorView"));
propertyEditorView->setAutomaticDocumentUpdate(_GetParam()->GetBool("AutoTransactionView", false));
tabs->addTab(propertyEditorView, tr("View"));
propertyEditorData = new Gui::PropertyEditor::PropertyEditor();
propertyEditorData->setObjectName(QStringLiteral("propertyEditorData"));
propertyEditorData->setAutomaticDocumentUpdate(_GetParam()->GetBool("AutoTransactionData", true));
tabs->addTab(propertyEditorData, tr("Data"));
int preferredTab = _GetParam()->GetInt("LastTabIndex", 1);
if (preferredTab > 0 && preferredTab < tabs->count()) {
tabs->setCurrentIndex(preferredTab);
}
// connect after adding all tabs, so adding doesn't thrash the parameter
connect(tabs, &QTabWidget::currentChanged, this, &PropertyView::tabChanged);
// NOLINTBEGIN
this->connectPropData = App::GetApplication().signalChangedObject.connect(
std::bind(&PropertyView::slotChangePropertyData, this, sp::_2)
);
this->connectPropView = Gui::Application::Instance->signalChangedObject.connect(
std::bind(&PropertyView::slotChangePropertyView, this, sp::_1, sp::_2)
);
this->connectPropAppend = App::GetApplication().signalAppendDynamicProperty.connect(
std::bind(&PropertyView::slotAppendDynamicProperty, this, sp::_1)
);
this->connectPropRemove = App::GetApplication().signalRemoveDynamicProperty.connect(
std::bind(&PropertyView::slotRemoveDynamicProperty, this, sp::_1)
);
this->connectPropRename = App::GetApplication().signalRenameDynamicProperty.connect(
std::bind(&PropertyView::slotRenameDynamicProperty, this, sp::_1, sp::_2)
);
this->connectPropChange = App::GetApplication().signalChangePropertyEditor.connect(
std::bind(&PropertyView::slotChangePropertyEditor, this, sp::_1, sp::_2)
);
this->connectUndoDocument = App::GetApplication().signalUndoDocument.connect(
std::bind(&PropertyView::slotRollback, this)
);
this->connectRedoDocument = App::GetApplication().signalRedoDocument.connect(
std::bind(&PropertyView::slotRollback, this)
);
this->connectActiveDoc = Application::Instance->signalActiveDocument.connect(
std::bind(&PropertyView::slotActiveDocument, this, sp::_1)
);
this->connectDelDocument = Application::Instance->signalDeleteDocument.connect(
std::bind(&PropertyView::slotDeleteDocument, this, sp::_1)
);
this->connectDelViewObject = Application::Instance->signalDeletedObject.connect(
std::bind(&PropertyView::slotDeletedViewObject, this, sp::_1)
);
this->connectDelObject = App::GetApplication().signalDeletedObject.connect(
std::bind(&PropertyView::slotDeletedObject, this, sp::_1)
);
this->connectChangedDocument = App::GetApplication().signalChangedDocument.connect(
std::bind(&PropertyView::slotChangePropertyData, this, sp::_2)
);
// NOLINTEND
}
PropertyView::~PropertyView()
{
this->connectPropData.disconnect();
this->connectPropView.disconnect();
this->connectPropAppend.disconnect();
this->connectPropRemove.disconnect();
this->connectPropChange.disconnect();
this->connectUndoDocument.disconnect();
this->connectRedoDocument.disconnect();
this->connectActiveDoc.disconnect();
this->connectDelDocument.disconnect();
this->connectDelObject.disconnect();
this->connectDelViewObject.disconnect();
this->connectChangedDocument.disconnect();
}
static bool _ShowAll;
bool PropertyView::showAll()
{
return _ShowAll;
}
void PropertyView::setShowAll(bool enable)
{
if (_ShowAll != enable) {
_ShowAll = enable;
const auto views = getMainWindow()->findChildren<PropertyView*>();
for (auto view : views) {
if (view->isVisible()) {
view->propertyEditorData->buildUp();
view->propertyEditorView->buildUp();
view->onTimer();
}
}
}
}
void PropertyView::hideEvent(QHideEvent* ev)
{
this->timer->stop();
this->detachSelection();
// clear the properties before hiding.
propertyEditorData->buildUp();
propertyEditorView->buildUp();
clearPropertyItemSelection();
QWidget::hideEvent(ev);
}
void PropertyView::showEvent(QShowEvent* ev)
{
this->attachSelection();
this->timer->start(ViewParams::instance()->getPropertyViewTimer());
QWidget::showEvent(ev);
}
void PropertyView::clearPropertyItemSelection()
{
QModelIndex index;
propertyEditorData->clearSelection();
propertyEditorData->setCurrentIndex(index);
propertyEditorView->clearSelection();
propertyEditorView->setCurrentIndex(index);
}
void PropertyView::slotRollback()
{
// PropertyItemDelegate will setup application active transaction on
// entering edit mode, and close active transaction when exit editing. But,
// when the user clicks undo/redo button while editing some property, the
// current active transaction will be closed by design, which cause further
// editing to be not recorded. Hence, we force unselect any property item on
// undo/redo
clearPropertyItemSelection();
}
void PropertyView::slotChangePropertyData(const App::Property& prop)
{
if (propertyEditorData->propOwners.contains(prop.getContainer())) {
propertyEditorData->updateProperty(prop);
timer->start(ViewParams::instance()->getPropertyViewTimer());
}
}
void PropertyView::slotChangePropertyView(const Gui::ViewProvider&, const App::Property& prop)
{
if (propertyEditorView->propOwners.contains(prop.getContainer())) {
propertyEditorView->updateProperty(prop);
timer->start(ViewParams::instance()->getPropertyViewTimer());
}
}
bool PropertyView::isPropertyHidden(const App::Property* prop)
{
return prop && !showAll()
&& ((prop->getType() & App::Prop_Hidden) || prop->testStatus(App::Property::Hidden));
}
void PropertyView::slotAppendDynamicProperty(const App::Property& prop)
{
if (isPropertyHidden(&prop)) {
return;
}
App::PropertyContainer* parent = prop.getContainer();
if (propertyEditorData->propOwners.contains(parent)) {
propertyEditorData->blockCollapseAll();
timer->start(ViewParams::instance()->getPropertyViewTimer());
}
if (propertyEditorView->propOwners.contains(parent)) {
propertyEditorView->blockCollapseAll();
timer->start(ViewParams::instance()->getPropertyViewTimer());
}
}
void PropertyView::slotRemoveDynamicProperty(const App::Property& prop)
{
App::PropertyContainer* parent = prop.getContainer();
if (propertyEditorData->propOwners.contains(parent)) {
propertyEditorData->removeProperty(prop);
}
else if (propertyEditorView->propOwners.contains(parent)) {
propertyEditorView->removeProperty(prop);
}
else {
return;
}
timer->start(ViewParams::instance()->getPropertyViewTimer());
}
void PropertyView::slotRenameDynamicProperty(const App::Property& prop, const char* /*oldName*/)
{
App::PropertyContainer* parent = prop.getContainer();
if (propertyEditorData->propOwners.contains(parent)) {
propertyEditorData->renameProperty(prop);
}
else if (propertyEditorView->propOwners.contains(parent)) {
propertyEditorView->renameProperty(prop);
}
else {
return;
}
clearPropertyItemSelection();
timer->start(ViewParams::instance()->getPropertyViewTimer());
}
void PropertyView::slotChangePropertyEditor(const App::Document&, const App::Property& prop)
{
App::PropertyContainer* parent = prop.getContainer();
if (propertyEditorData->propOwners.contains(parent)
|| propertyEditorView->propOwners.contains(parent)) {
timer->start(ViewParams::instance()->getPropertyViewTimer());
}
}
void PropertyView::slotDeleteDocument(const Gui::Document& doc)
{
if (propertyEditorData->propOwners.contains(doc.getDocument())) {
propertyEditorView->buildUp();
propertyEditorData->buildUp();
clearPropertyItemSelection();
timer->start(ViewParams::instance()->getPropertyViewTimer());
}
}
void PropertyView::slotDeletedViewObject(const Gui::ViewProvider& vp)
{
if (propertyEditorView->propOwners.contains(&vp)) {
propertyEditorView->buildUp();
propertyEditorData->buildUp();
clearPropertyItemSelection();
timer->start(ViewParams::instance()->getPropertyViewTimer());
}
}
void PropertyView::slotDeletedObject(const App::DocumentObject& obj)
{
if (propertyEditorData->propOwners.contains(&obj)) {
propertyEditorView->buildUp();
propertyEditorData->buildUp();
clearPropertyItemSelection();
timer->start(ViewParams::instance()->getPropertyViewTimer());
}
}
void PropertyView::slotActiveDocument(const Gui::Document& doc)
{
checkEnable(doc.getDocument()->getName());
}
void PropertyView::checkEnable(const char* doc)
{
if (ViewParams::instance()->getEnablePropertyViewForInactiveDocument()) {
setEnabled(true);
return;
}
// check if at least one selected object is part of the active document
setEnabled(!Selection().hasSelection() || Selection().hasSelection(doc, ResolveMode::NoResolve));
}
struct PropertyView::PropInfo
{
std::string propName;
int propId;
std::vector<App::Property*> propList;
};
struct PropertyView::PropFind
{
const PropInfo& item;
explicit PropFind(const PropInfo& item)
: item(item)
{}
bool operator()(const PropInfo& elem) const
{
return (elem.propId == item.propId) && (elem.propName == item.propName);
}
};
void PropertyView::onSelectionChanged(const SelectionChanges& msg)
{
if (msg.Type != SelectionChanges::AddSelection && msg.Type != SelectionChanges::RmvSelection
&& msg.Type != SelectionChanges::SetSelection && msg.Type != SelectionChanges::ClrSelection) {
return;
}
// clear the properties.
timer->start(ViewParams::instance()->getPropertyViewTimer());
}
void PropertyView::onTimer()
{
// See https://forum.freecad.org/viewtopic.php?f=8&t=72526
if (this->updating) {
Base::Console().log("Ignore recursive call of PropertyView::onTimer()\n");
return;
}
Base::StateLocker guard(this->updating);
timer->stop();
if (!this->isSelectionAttached()) {
propertyEditorData->buildUp();
propertyEditorView->buildUp();
clearPropertyItemSelection();
return;
}
if (!Gui::Selection().hasSelection()) {
auto gdoc = TreeWidget::selectedDocument();
if (!gdoc || !gdoc->getDocument()) {
propertyEditorData->buildUp();
propertyEditorView->buildUp();
clearPropertyItemSelection();
return;
}
PropertyModel::PropertyList docProps;
auto doc = gdoc->getDocument();
std::map<std::string, App::Property*> props;
doc->getPropertyMap(props);
for (auto& v : props) {
docProps.emplace_back(v.first, std::vector<App::Property*>(1, v.second));
}
propertyEditorData->buildUp(std::move(docProps));
tabs->setCurrentIndex(1);
return;
}
std::set<App::DocumentObject*> objSet;
// group the properties by <name,id>
std::vector<PropInfo> propDataMap;
std::vector<PropInfo> propViewMap;
bool checkLink = true;
ViewProviderDocumentObject* vpLast = nullptr;
auto sels = Gui::Selection().getSelectionEx("*");
for (auto& sel : sels) {
App::DocumentObject* ob = sel.getObject();
if (!ob) {
continue;
}
// Do not process an object more than once
if (!objSet.insert(ob).second) {
continue;
}
std::vector<App::Property*> dataList;
std::map<std::string, App::Property*> viewList;
auto vp = Application::Instance->getViewProvider(ob);
if (!vp) {
checkLink = false;
ob->getPropertyList(dataList);
continue;
}
if (vp->isDerivedFrom<ViewProviderDocumentObject>()) {
auto cvp = static_cast<ViewProviderDocumentObject*>(vp);
if (vpLast && cvp != vpLast) {
checkLink = false;
}
vpLast = cvp;
}
ob->getPropertyList(dataList);
// get the properties as map here because it doesn't matter to have them sorted alphabetically
vp->getPropertyMap(viewList);
// store the properties with <name,id> as key in a map
{
for (auto prop : dataList) {
if (isPropertyHidden(prop)) {
continue;
}
PropInfo nameType;
nameType.propName = prop->getName();
nameType.propId = prop->getTypeId().getKey();
auto pi = std::find_if(propDataMap.begin(), propDataMap.end(), PropFind(nameType));
if (pi != propDataMap.end()) {
pi->propList.push_back(prop);
}
else {
nameType.propList.push_back(prop);
propDataMap.push_back(nameType);
}
}
}
// the same for the view properties
{
std::map<std::string, App::Property*>::iterator pt;
for (pt = viewList.begin(); pt != viewList.end(); ++pt) {
if (isPropertyHidden(pt->second)) {
continue;
}
PropInfo nameType;
nameType.propName = pt->first;
nameType.propId = pt->second->getTypeId().getKey();
auto pi = std::find_if(propViewMap.begin(), propViewMap.end(), PropFind(nameType));
if (pi != propViewMap.end()) {
pi->propList.push_back(pt->second);
}
else {
nameType.propList.push_back(pt->second);
propViewMap.push_back(nameType);
}
}
}
}
// the property must be part of each selected object, i.e. the number
// of selected objects is equal to the number of properties with same
// name and id
std::vector<PropInfo>::const_iterator it;
PropertyModel::PropertyList dataProps;
std::map<std::string, std::vector<App::Property*>> dataPropsMap;
PropertyModel::PropertyList viewProps;
if (checkLink && vpLast) {
// In case the only selected object is a link, insert the link's own
// property before the linked object
App::DocumentObject* obj = vpLast->getObject();
auto linked = obj;
if (obj && obj->canLinkProperties() && (linked = obj->getLinkedObject(true)) != obj
&& linked) {
std::vector<App::Property*> dataList;
std::map<std::string, App::Property*> propMap;
obj->getPropertyMap(propMap);
linked->getPropertyList(dataList);
for (auto prop : dataList) {
if (isPropertyHidden(prop)) {
continue;
}
std::string name(prop->getName());
auto it = propMap.find(name);
if (it != propMap.end() && !isPropertyHidden(it->second)) {
continue;
}
std::vector<App::Property*> items(1, prop);
if (prop->testStatus(App::Property::PropDynamic)) {
dataPropsMap.emplace(name + "*", std::move(items));
}
else {
dataProps.emplace_back(name + "*", std::move(items));
}
}
auto vpLinked = Application::Instance->getViewProvider(linked);
if (vpLinked) {
propMap.clear();
vpLast->getPropertyMap(propMap);
dataList.clear();
vpLinked->getPropertyList(dataList);
for (auto prop : dataList) {
if (isPropertyHidden(prop)) {
continue;
}
std::string name(prop->getName());
auto it = propMap.find(name);
if (it != propMap.end() && !isPropertyHidden(it->second)) {
continue;
}
std::vector<App::Property*> items(1, prop);
viewProps.emplace_back(name + "*", std::move(items));
}
}
}
}
for (auto& v : dataPropsMap) {
dataProps.emplace_back(v.first, std::move(v.second));
}
dataPropsMap.clear();
for (it = propDataMap.begin(); it != propDataMap.end(); ++it) {
if (it->propList.size() == sels.size()) {
if (it->propList[0]->testStatus(App::Property::PropDynamic)) {
dataPropsMap.emplace(it->propName, std::move(it->propList));
}
else {
dataProps.emplace_back(it->propName, std::move(it->propList));
}
}
}
for (auto& v : dataPropsMap) {
dataProps.emplace_back(v.first, std::move(v.second));
}
propertyEditorData->buildUp(std::move(dataProps), true);
for (it = propViewMap.begin(); it != propViewMap.end(); ++it) {
if (it->propList.size() == sels.size()) {
viewProps.emplace_back(it->propName, std::move(it->propList));
}
}
propertyEditorView->buildUp(std::move(viewProps));
// make sure the editors are enabled/disabled properly
checkEnable();
}
void PropertyView::tabChanged(int index)
{
_GetParam()->SetInt("LastTabIndex", index);
}
void PropertyView::changeEvent(QEvent* e)
{
if (e->type() == QEvent::LanguageChange) {
tabs->setTabText(0, tr("View"));
tabs->setTabText(1, tr("Data"));
}
QWidget::changeEvent(e);
}
/* TRANSLATOR Gui::DockWnd::PropertyDockView */
PropertyDockView::PropertyDockView(Gui::Document* pcDocument, QWidget* parent)
: DockWindow(pcDocument, parent)
{
setWindowTitle(tr("Property View"));
auto view = new PropertyView(this);
auto pLayout = new QGridLayout(this);
pLayout->setSpacing(0);
pLayout->setContentsMargins(0, 0, 0, 0);
pLayout->addWidget(view, 0, 0);
resize(200, 400);
}
PropertyDockView::~PropertyDockView() = default;
#include "moc_PropertyView.cpp"