* Core: move getPlacementProperty to be more accessible * Update DocumentObject.h * Update DocumentObject.cpp * Update ViewProviderDragger.h * Update ViewProviderDragger.cpp * Update DocumentObject.h * Update DocumentObject.cpp * Update ViewProviderLink.cpp * Update DocumentObject.h * Update DocumentObject.cpp * Update DocumentObject.h
4009 lines
121 KiB
C++
4009 lines
121 KiB
C++
/****************************************************************************
|
|
* Copyright (c) 2017 Zheng Lei (realthunder) <realthunder.dev@gmail.com> *
|
|
* *
|
|
* 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 <atomic>
|
|
#include <cctype>
|
|
#include <unordered_set>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <deque>
|
|
#include <memory>
|
|
#include <algorithm>
|
|
#include <vector>
|
|
#include <set>
|
|
#include <map>
|
|
#include <string>
|
|
#include <boost/algorithm/string/predicate.hpp>
|
|
#include <Inventor/SoPickedPoint.h>
|
|
#include <Inventor/actions/SoGetBoundingBoxAction.h>
|
|
#include <Inventor/details/SoDetail.h>
|
|
#include <Inventor/draggers/SoCenterballDragger.h>
|
|
#include <Inventor/misc/SoChildList.h>
|
|
#include <Inventor/nodes/SoAnnotation.h>
|
|
#include <Inventor/nodes/SoCube.h>
|
|
#include <Inventor/nodes/SoDrawStyle.h>
|
|
#include <Inventor/nodes/SoPickStyle.h>
|
|
#include <Inventor/nodes/SoSeparator.h>
|
|
#include <Inventor/nodes/SoShapeHints.h>
|
|
#include <Inventor/nodes/SoSurroundScale.h>
|
|
#include <Inventor/nodes/SoSwitch.h>
|
|
#include <Inventor/nodes/SoTransform.h>
|
|
#include <Inventor/sensors/SoNodeSensor.h>
|
|
#include <QApplication>
|
|
#include <QMenu>
|
|
#include <QCheckBox>
|
|
|
|
|
|
#include <boost/range.hpp>
|
|
#include <App/ElementNamingUtils.h>
|
|
#include <App/Document.h>
|
|
#include <Base/BoundBoxPy.h>
|
|
#include <Base/MatrixPy.h>
|
|
#include <Base/PlacementPy.h>
|
|
#include <Base/Tools.h>
|
|
|
|
#include "Action.h"
|
|
#include "MainWindow.h"
|
|
#include "ViewProviderLink.h"
|
|
#include "ViewProviderLinkPy.h"
|
|
#include "Application.h"
|
|
#include "BitmapFactory.h"
|
|
#include "Control.h"
|
|
#include "Inventor/Draggers/SoTransformDragger.h"
|
|
#include "LinkViewPy.h"
|
|
#include "Selection.h"
|
|
#include "SoFCUnifiedSelection.h"
|
|
#include "TaskTransform.h"
|
|
#include "TaskElementColors.h"
|
|
#include "View3DInventor.h"
|
|
#include "ViewParams.h"
|
|
#include "ViewProviderGeometryObject.h"
|
|
|
|
#include "ActionFunction.h"
|
|
#include "Command.h"
|
|
#include "Dialogs/DlgObjectSelection.h"
|
|
|
|
FC_LOG_LEVEL_INIT("App::Link", true, true)
|
|
|
|
using namespace Gui;
|
|
using namespace Base;
|
|
|
|
using CharRange = boost::iterator_range<const char*>;
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
static inline bool appendPathSafe(SoPath* path, SoNode* node)
|
|
{
|
|
if (path->getLength()) {
|
|
SoNode* tail = path->getTail();
|
|
const SoChildList* children = tail->getChildren();
|
|
if (!children || children->find((void*)node) < 0) {
|
|
return false;
|
|
}
|
|
}
|
|
path->append(node);
|
|
return true;
|
|
}
|
|
|
|
#ifdef FC_DEBUG
|
|
# define appendPath(_path, _node) \
|
|
do { \
|
|
if (!appendPathSafe(_path, _node)) \
|
|
FC_ERR("LinkView: coin path error"); \
|
|
} while (0)
|
|
#else
|
|
# define appendPath(_path, _node) (_path)->append(_node)
|
|
#endif
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
class Gui::LinkInfo
|
|
{
|
|
|
|
public:
|
|
std::atomic<int> ref;
|
|
|
|
using Connection = boost::signals2::scoped_connection;
|
|
Connection connChangeIcon;
|
|
|
|
ViewProviderDocumentObject* pcLinked;
|
|
std::unordered_set<Gui::LinkOwner*> links;
|
|
|
|
using Pointer = LinkInfoPtr;
|
|
|
|
SoNodeSensor sensor;
|
|
SoNodeSensor switchSensor;
|
|
SoNodeSensor childSensor;
|
|
SoNodeSensor transformSensor;
|
|
|
|
std::array<CoinPtr<SoSeparator>, LinkView::SnapshotMax> pcSnapshots;
|
|
std::array<CoinPtr<SoSwitch>, LinkView::SnapshotMax> pcSwitches;
|
|
CoinPtr<SoSwitch> pcLinkedSwitch;
|
|
|
|
// for group type view providers
|
|
CoinPtr<SoGroup> pcChildGroup;
|
|
using NodeMap = std::unordered_map<SoNode*, Pointer>;
|
|
NodeMap nodeMap;
|
|
|
|
std::map<qint64, QIcon> iconMap;
|
|
|
|
static ViewProviderDocumentObject* getView(App::DocumentObject* obj)
|
|
{
|
|
if (obj && obj->isAttachedToDocument()) {
|
|
Document* pDoc = Application::Instance->getDocument(obj->getDocument());
|
|
if (pDoc) {
|
|
ViewProvider* vp = pDoc->getViewProvider(obj);
|
|
if (vp && vp->isDerivedFrom<ViewProviderDocumentObject>()) {
|
|
return static_cast<ViewProviderDocumentObject*>(vp);
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static Pointer get(App::DocumentObject* obj, Gui::LinkOwner* owner)
|
|
{
|
|
return get(getView(obj), owner);
|
|
}
|
|
|
|
static Pointer get(ViewProviderDocumentObject* vp, LinkOwner* owner)
|
|
{
|
|
if (!vp) {
|
|
return Pointer();
|
|
}
|
|
|
|
auto ext = vp->getExtensionByType<ViewProviderLinkObserver>(true);
|
|
if (!ext) {
|
|
ext = new ViewProviderLinkObserver();
|
|
ext->initExtension(vp);
|
|
}
|
|
if (!ext->linkInfo) {
|
|
// extension can be created automatically when restored from document,
|
|
// with an empty linkInfo. So we need to check here.
|
|
ext->linkInfo = Pointer(new LinkInfo(vp));
|
|
ext->linkInfo->update();
|
|
}
|
|
if (owner) {
|
|
ext->linkInfo->links.insert(owner);
|
|
}
|
|
return ext->linkInfo;
|
|
}
|
|
|
|
static void sensorCB(void* data, SoSensor*)
|
|
{
|
|
static_cast<LinkInfo*>(data)->update();
|
|
}
|
|
|
|
static void switchSensorCB(void* data, SoSensor*)
|
|
{
|
|
static_cast<LinkInfo*>(data)->updateSwitch();
|
|
}
|
|
|
|
static void childSensorCB(void* data, SoSensor*)
|
|
{
|
|
static_cast<LinkInfo*>(data)->updateChildren();
|
|
}
|
|
|
|
static void transformSensorCB(void* data, SoSensor*)
|
|
{
|
|
auto self = static_cast<LinkInfo*>(data);
|
|
for (size_t i = 0; i < self->pcSnapshots.size(); ++i) {
|
|
if (self->pcSnapshots[i] && i != LinkView::SnapshotTransform) {
|
|
self->getSnapshot(i, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
explicit LinkInfo(ViewProviderDocumentObject* vp)
|
|
: ref(0)
|
|
, pcLinked(vp)
|
|
{
|
|
FC_LOG("new link to " << pcLinked->getObject()->getFullName());
|
|
// NOLINTBEGIN
|
|
connChangeIcon = vp->signalChangeIcon.connect(std::bind(&LinkInfo::slotChangeIcon, this));
|
|
// NOLINTEND
|
|
vp->forceUpdate(true);
|
|
sensor.setFunction(sensorCB);
|
|
sensor.setData(this);
|
|
switchSensor.setFunction(switchSensorCB);
|
|
switchSensor.setData(this);
|
|
childSensor.setFunction(childSensorCB);
|
|
childSensor.setData(this);
|
|
transformSensor.setFunction(transformSensorCB);
|
|
transformSensor.setData(this);
|
|
}
|
|
|
|
~LinkInfo() = default;
|
|
|
|
bool checkName(const char* name) const
|
|
{
|
|
return isLinked() && strcmp(name, getLinkedName()) == 0;
|
|
}
|
|
|
|
void remove(LinkOwner* owner)
|
|
{
|
|
links.erase(owner);
|
|
}
|
|
|
|
bool isLinked() const
|
|
{
|
|
return pcLinked && pcLinked->getObject() && pcLinked->getObject()->isAttachedToDocument();
|
|
}
|
|
|
|
const char* getLinkedName() const
|
|
{
|
|
return pcLinked->getObject()->getDagKey();
|
|
}
|
|
|
|
const char* getLinkedLabel() const
|
|
{
|
|
return pcLinked->getObject()->Label.getValue();
|
|
}
|
|
|
|
const char* getLinkedNameSafe() const
|
|
{
|
|
if (isLinked()) {
|
|
return getLinkedName();
|
|
}
|
|
return "<nil>";
|
|
}
|
|
|
|
const char* getDocName() const
|
|
{
|
|
return pcLinked->getDocument()->getDocument()->getName();
|
|
}
|
|
|
|
void detach(bool unlink)
|
|
{
|
|
FC_LOG("link detach " << getLinkedNameSafe());
|
|
auto me = LinkInfoPtr(this);
|
|
if (unlink) {
|
|
while (!links.empty()) {
|
|
auto link = *links.begin();
|
|
links.erase(links.begin());
|
|
link->unlink(me);
|
|
}
|
|
}
|
|
sensor.detach();
|
|
switchSensor.detach();
|
|
childSensor.detach();
|
|
transformSensor.detach();
|
|
for (const auto& node : pcSnapshots) {
|
|
if (node) {
|
|
coinRemoveAllChildren(node);
|
|
}
|
|
}
|
|
for (const auto& node : pcSwitches) {
|
|
if (node) {
|
|
coinRemoveAllChildren(node);
|
|
}
|
|
}
|
|
pcLinkedSwitch.reset();
|
|
if (pcChildGroup) {
|
|
coinRemoveAllChildren(pcChildGroup);
|
|
pcChildGroup.reset();
|
|
}
|
|
pcLinked = nullptr;
|
|
connChangeIcon.disconnect();
|
|
}
|
|
|
|
void updateSwitch(SoSwitch* node = nullptr)
|
|
{
|
|
if (!isLinked() || !pcLinkedSwitch) {
|
|
return;
|
|
}
|
|
int index = pcLinkedSwitch->whichChild.getValue();
|
|
for (size_t i = 0; i < pcSwitches.size(); ++i) {
|
|
if (!pcSwitches[i] || (node && node != pcSwitches[i])) {
|
|
continue;
|
|
}
|
|
int count = pcSwitches[i]->getNumChildren();
|
|
if ((index < 0 && i == LinkView::SnapshotChild) || !count) {
|
|
pcSwitches[i]->whichChild = SO_SWITCH_NONE;
|
|
}
|
|
else if (count > pcLinked->getDefaultMode()) {
|
|
pcSwitches[i]->whichChild = pcLinked->getDefaultMode();
|
|
}
|
|
else {
|
|
pcSwitches[i]->whichChild = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
inline void addref()
|
|
{
|
|
++ref;
|
|
}
|
|
|
|
inline void release()
|
|
{
|
|
int r = --ref;
|
|
assert(r >= 0);
|
|
if (r == 0) {
|
|
delete this;
|
|
}
|
|
else if (r == 1) {
|
|
if (pcLinked) {
|
|
FC_LOG("link release " << getLinkedNameSafe());
|
|
auto ext = pcLinked->getExtensionByType<ViewProviderLinkObserver>(true);
|
|
if (ext && ext->linkInfo == this) {
|
|
pcLinked->forceUpdate(false);
|
|
detach(true);
|
|
ext->linkInfo.reset();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MSVC has trouble with template argument dependent lookup in
|
|
// namespace. Have to put the below functions in global namespace.
|
|
#if defined(_MSC_VER)
|
|
friend void Gui::intrusive_ptr_add_ref(LinkInfo* px);
|
|
friend void Gui::intrusive_ptr_release(LinkInfo* px);
|
|
#else
|
|
friend inline void intrusive_ptr_add_ref(LinkInfo* px)
|
|
{
|
|
px->addref();
|
|
}
|
|
friend inline void intrusive_ptr_release(LinkInfo* px)
|
|
{
|
|
px->release();
|
|
}
|
|
#endif
|
|
|
|
bool isVisible() const
|
|
{
|
|
if (!isLinked()) {
|
|
return true;
|
|
}
|
|
int indices[] = {LinkView::SnapshotTransform, LinkView::SnapshotVisible};
|
|
for (int idx : indices) {
|
|
if (!pcSwitches[idx]) {
|
|
continue;
|
|
}
|
|
if (pcSwitches[idx]->whichChild.getValue() == SO_SWITCH_NONE) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void setVisible(bool visible)
|
|
{
|
|
if (!isLinked()) {
|
|
return;
|
|
}
|
|
int indices[] = {LinkView::SnapshotTransform, LinkView::SnapshotVisible};
|
|
for (int idx : indices) {
|
|
if (!pcSwitches[idx]) {
|
|
continue;
|
|
}
|
|
if (!visible) {
|
|
pcSwitches[idx]->whichChild = SO_SWITCH_NONE;
|
|
}
|
|
else if (pcSwitches[idx]->getNumChildren() > pcLinked->getDefaultMode()) {
|
|
pcSwitches[idx]->whichChild = pcLinked->getDefaultMode();
|
|
}
|
|
}
|
|
}
|
|
|
|
SoSeparator* getSnapshot(int type, bool update = false)
|
|
{
|
|
if (type < 0 || type >= LinkView::SnapshotMax) {
|
|
return nullptr;
|
|
}
|
|
|
|
SoSeparator* root;
|
|
if (!isLinked() || !(root = pcLinked->getRoot())) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (sensor.getAttachedNode() != root) {
|
|
sensor.detach();
|
|
sensor.attach(root);
|
|
}
|
|
|
|
auto& pcSnapshot = pcSnapshots[type];
|
|
auto& pcModeSwitch = pcSwitches[type];
|
|
if (pcSnapshot) {
|
|
if (!update) {
|
|
return pcSnapshot;
|
|
}
|
|
}
|
|
else {
|
|
if (ViewParams::instance()->getUseSelectionRoot()) {
|
|
pcSnapshot = new SoFCSelectionRoot;
|
|
}
|
|
else {
|
|
pcSnapshot = new SoSeparator;
|
|
}
|
|
pcSnapshot->boundingBoxCaching = SoSeparator::OFF;
|
|
pcSnapshot->renderCaching = SoSeparator::OFF;
|
|
std::ostringstream ss;
|
|
ss << pcLinked->getObject()->getNameInDocument() << "(" << type << ')';
|
|
pcSnapshot->setName(ss.str().c_str());
|
|
pcModeSwitch = new SoSwitch;
|
|
}
|
|
|
|
pcLinkedSwitch.reset();
|
|
|
|
coinRemoveAllChildren(pcSnapshot);
|
|
pcModeSwitch->whichChild = SO_SWITCH_NONE;
|
|
coinRemoveAllChildren(pcModeSwitch);
|
|
|
|
SoSwitch* pcUpdateSwitch = pcModeSwitch;
|
|
|
|
auto childRoot = pcLinked->getChildRoot();
|
|
|
|
for (int i = 0, count = root->getNumChildren(); i < count; ++i) {
|
|
SoNode* node = root->getChild(i);
|
|
|
|
// Exclude the linked object's pick style from the snapshot,
|
|
// so that the Link's own pick style is the only one in effect.
|
|
if (node->isOfType(SoPickStyle::getClassTypeId())) {
|
|
continue;
|
|
}
|
|
|
|
if (node == pcLinked->getTransformNode()) {
|
|
if (type != LinkView::SnapshotTransform) {
|
|
pcSnapshot->addChild(node);
|
|
}
|
|
else {
|
|
auto transform = pcLinked->getTransformNode();
|
|
const auto& scale = transform->scaleFactor.getValue();
|
|
if (scale[0] != 1.0 || scale[1] != 1.0 || scale[2] != 1.0) {
|
|
auto trans = new SoTransform;
|
|
pcSnapshot->addChild(trans);
|
|
trans->scaleFactor.setValue(scale);
|
|
trans->scaleOrientation = transform->scaleOrientation;
|
|
if (transformSensor.getAttachedNode() != transform) {
|
|
transformSensor.detach();
|
|
transformSensor.attach(transform);
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
else if (node != pcLinked->getModeSwitch()) {
|
|
pcSnapshot->addChild(node);
|
|
continue;
|
|
}
|
|
|
|
pcLinkedSwitch = static_cast<SoSwitch*>(node);
|
|
if (switchSensor.getAttachedNode() != pcLinkedSwitch) {
|
|
switchSensor.detach();
|
|
switchSensor.attach(pcLinkedSwitch);
|
|
pcUpdateSwitch = nullptr;
|
|
}
|
|
|
|
pcSnapshot->addChild(pcModeSwitch);
|
|
for (int i = 0, count = pcLinkedSwitch->getNumChildren(); i < count; ++i) {
|
|
auto child = pcLinkedSwitch->getChild(i);
|
|
if (pcChildGroup && child == childRoot) {
|
|
pcModeSwitch->addChild(pcChildGroup);
|
|
}
|
|
else {
|
|
pcModeSwitch->addChild(child);
|
|
}
|
|
}
|
|
}
|
|
updateSwitch(pcUpdateSwitch);
|
|
return pcSnapshot;
|
|
}
|
|
|
|
void updateData(const App::Property* prop)
|
|
{
|
|
LinkInfoPtr me(this);
|
|
for (auto link : links) {
|
|
link->onLinkedUpdateData(me, prop);
|
|
}
|
|
// update();
|
|
}
|
|
|
|
void update()
|
|
{
|
|
if (!isLinked() || pcLinked->isRestoring()) {
|
|
return;
|
|
}
|
|
|
|
updateChildren();
|
|
|
|
for (size_t i = 0; i < pcSnapshots.size(); ++i) {
|
|
if (pcSnapshots[i]) {
|
|
getSnapshot(i, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void updateChildren()
|
|
{
|
|
if (!isLinked()) {
|
|
return;
|
|
}
|
|
|
|
if (!pcLinked->getChildRoot()) {
|
|
childSensor.detach();
|
|
if (pcChildGroup) {
|
|
coinRemoveAllChildren(pcChildGroup);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (childSensor.getAttachedNode() != pcLinked->getChildRoot()) {
|
|
childSensor.detach();
|
|
childSensor.attach(pcLinked->getChildRoot());
|
|
}
|
|
if (!pcChildGroup) {
|
|
pcChildGroup = new SoGroup;
|
|
}
|
|
else {
|
|
coinRemoveAllChildren(pcChildGroup);
|
|
}
|
|
|
|
NodeMap nodeMap;
|
|
|
|
for (auto child : pcLinked->claimChildren3D()) {
|
|
Pointer info = get(child, nullptr);
|
|
if (!info) {
|
|
continue;
|
|
}
|
|
SoNode* node = info->getSnapshot(LinkView::SnapshotChild);
|
|
if (!node) {
|
|
continue;
|
|
}
|
|
nodeMap[node] = info;
|
|
pcChildGroup->addChild(node);
|
|
}
|
|
|
|
// Use swap instead of clear() here to avoid potential link
|
|
// destruction
|
|
this->nodeMap.swap(nodeMap);
|
|
}
|
|
|
|
bool getElementPicked(bool addname, int type, const SoPickedPoint* pp, std::ostream& str) const
|
|
{
|
|
if (!pp || !isLinked()) {
|
|
return false;
|
|
}
|
|
|
|
if (addname) {
|
|
str << getLinkedName() << '.';
|
|
}
|
|
|
|
auto pcSwitch = pcSwitches[type];
|
|
if (pcChildGroup && pcSwitch && pcSwitch->whichChild.getValue() >= 0
|
|
&& pcSwitch->getChild(pcSwitch->whichChild.getValue()) == pcChildGroup) {
|
|
SoPath* path = pp->getPath();
|
|
int index = path->findNode(pcChildGroup);
|
|
if (index <= 0) {
|
|
return false;
|
|
}
|
|
auto it = nodeMap.find(path->getNode(index + 1));
|
|
if (it == nodeMap.end()) {
|
|
return false;
|
|
}
|
|
return it->second->getElementPicked(true, LinkView::SnapshotChild, pp, str);
|
|
}
|
|
else {
|
|
std::string subname;
|
|
if (!pcLinked->getElementPicked(pp, subname)) {
|
|
return false;
|
|
}
|
|
str << subname;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static const char* checkSubname(App::DocumentObject* obj, const char* subname)
|
|
{
|
|
#define CHECK_NAME(_name, _end) \
|
|
do { \
|
|
if (!_name) \
|
|
return 0; \
|
|
const char* _n = _name; \
|
|
for (; *subname && *_n; ++subname, ++_n) \
|
|
if (*subname != *_n) \
|
|
break; \
|
|
if (*_n || (*subname != 0 && *subname != _end)) \
|
|
return 0; \
|
|
if (*subname == _end) \
|
|
++subname; \
|
|
} while (0)
|
|
|
|
// if(subname[0] == '*') {
|
|
// ++subname;
|
|
// CHECK_NAME(obj->getDocument()->getName(),'*');
|
|
// }
|
|
CHECK_NAME(obj->getNameInDocument(), '.');
|
|
return subname;
|
|
}
|
|
|
|
bool getDetail(bool checkname, int type, const char* subname, SoDetail*& det, SoFullPath* path) const
|
|
{
|
|
if (!isLinked()) {
|
|
return false;
|
|
}
|
|
|
|
if (checkname) {
|
|
subname = checkSubname(pcLinked->getObject(), subname);
|
|
if (!subname) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (pcSnapshots[type]->findChild(pcSwitches[type]) < 0) {
|
|
if (path) {
|
|
if (!appendPathSafe(path, pcSnapshots[type])) {
|
|
return false;
|
|
}
|
|
}
|
|
// this is possible in case of editing, where the switch node
|
|
// of the linked view object is temparaly removed from its root
|
|
return true;
|
|
}
|
|
int len = 0;
|
|
if (path) {
|
|
len = path->getLength();
|
|
if (!appendPathSafe(path, pcSnapshots[type])) {
|
|
return false;
|
|
}
|
|
appendPath(path, pcSwitches[type]);
|
|
}
|
|
if (*subname == 0) {
|
|
return true;
|
|
}
|
|
|
|
auto pcSwitch = pcSwitches[type];
|
|
if (!pcChildGroup || !pcSwitch || pcSwitch->whichChild.getValue() < 0
|
|
|| pcSwitch->getChild(pcSwitch->whichChild.getValue()) != pcChildGroup) {
|
|
return pcLinked->getDetailPath(subname, path, false, det);
|
|
}
|
|
if (path) {
|
|
appendPath(path, pcChildGroup);
|
|
if (pcLinked->getChildRoot()) {
|
|
type = LinkView::SnapshotChild;
|
|
}
|
|
else {
|
|
type = LinkView::SnapshotVisible;
|
|
}
|
|
}
|
|
|
|
// Special handling of nodes with childRoot, especially geo feature
|
|
// group. It's object hierarchy in the tree view (i.e. in subname) is
|
|
// difference from its coin hierarchy. All objects under a geo feature
|
|
// group is visually grouped directly under the group's childRoot,
|
|
// even though some object has secondary hierarchy in subname. E.g.
|
|
//
|
|
// Body
|
|
// |--Pad
|
|
// |--Sketch
|
|
//
|
|
// Both Sketch and Pad's coin nodes are grouped directly under Body as,
|
|
//
|
|
// Body
|
|
// |--Pad
|
|
// |--Sketch
|
|
|
|
const char* dot = strchr(subname, '.');
|
|
const char* nextsub = subname;
|
|
if (!dot) {
|
|
return false;
|
|
}
|
|
auto geoGroup = pcLinked->getObject();
|
|
auto sobj = geoGroup;
|
|
while (true) {
|
|
std::string objname = std::string(nextsub, dot - nextsub + 1);
|
|
if (!geoGroup->getSubObject(objname.c_str())) {
|
|
// this object is not found under the geo group, abort.
|
|
break;
|
|
}
|
|
// Object found under geo group, remember this subname
|
|
subname = nextsub;
|
|
|
|
auto ssobj = sobj->getSubObject(objname.c_str());
|
|
if (!ssobj) {
|
|
FC_ERR("invalid sub name " << nextsub << " of object " << sobj->getFullName());
|
|
return false;
|
|
}
|
|
sobj = ssobj;
|
|
auto vp = Application::Instance->getViewProvider(sobj);
|
|
if (!vp) {
|
|
FC_ERR("cannot find view provider of " << sobj->getFullName());
|
|
return false;
|
|
}
|
|
if (vp->getChildRoot()) {
|
|
// In case the children is also a geo group, it will visually
|
|
// hold all of its own children, so stop going further down.
|
|
break;
|
|
}
|
|
// new style mapped sub-element
|
|
if (Data::isMappedElement(dot + 1)) {
|
|
break;
|
|
}
|
|
auto next = strchr(dot + 1, '.');
|
|
if (!next) {
|
|
// no dot any more, the following must be a sub-element
|
|
break;
|
|
}
|
|
nextsub = dot + 1;
|
|
dot = next;
|
|
}
|
|
|
|
for (const auto& v : nodeMap) {
|
|
if (v.second->getDetail(true, type, subname, det, path)) {
|
|
return true;
|
|
}
|
|
}
|
|
if (path) {
|
|
path->truncate(len);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void slotChangeIcon()
|
|
{
|
|
iconMap.clear();
|
|
if (!isLinked()) {
|
|
return;
|
|
}
|
|
LinkInfoPtr me(this);
|
|
for (auto link : links) {
|
|
link->onLinkedIconChange(me);
|
|
}
|
|
}
|
|
|
|
QIcon getIcon(QPixmap px)
|
|
{
|
|
static int iconSize = -1;
|
|
if (iconSize < 0) {
|
|
auto sampleIcon = QApplication::style()->standardPixmap(QStyle::SP_DirClosedIcon);
|
|
double pixelRatio = sampleIcon.devicePixelRatio();
|
|
iconSize = static_cast<int>(sampleIcon.width() / pixelRatio);
|
|
}
|
|
|
|
if (!isLinked()) {
|
|
return QIcon();
|
|
}
|
|
|
|
if (px.isNull()) {
|
|
return pcLinked->getIcon();
|
|
}
|
|
QIcon& iconLink = iconMap[px.cacheKey()];
|
|
if (iconLink.isNull()) {
|
|
QIcon icon = pcLinked->getIcon();
|
|
iconLink = QIcon();
|
|
iconLink.addPixmap(
|
|
BitmapFactory().merge(
|
|
icon.pixmap(iconSize, iconSize, QIcon::Normal, QIcon::Off),
|
|
px,
|
|
BitmapFactoryInst::BottomLeft
|
|
),
|
|
QIcon::Normal,
|
|
QIcon::Off
|
|
);
|
|
iconLink.addPixmap(
|
|
BitmapFactory().merge(
|
|
icon.pixmap(iconSize, iconSize, QIcon::Normal, QIcon::On),
|
|
px,
|
|
BitmapFactoryInst::BottomLeft
|
|
),
|
|
QIcon::Normal,
|
|
QIcon::On
|
|
);
|
|
}
|
|
return iconLink;
|
|
}
|
|
};
|
|
|
|
#if defined(_MSC_VER)
|
|
namespace Gui
|
|
{
|
|
void intrusive_ptr_add_ref(Gui::LinkInfo* px)
|
|
{
|
|
px->addref();
|
|
}
|
|
|
|
void intrusive_ptr_release(Gui::LinkInfo* px)
|
|
{
|
|
px->release();
|
|
}
|
|
} // namespace Gui
|
|
#endif
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
EXTENSION_TYPESYSTEM_SOURCE(Gui::ViewProviderLinkObserver, Gui::ViewProviderExtension)
|
|
|
|
ViewProviderLinkObserver::ViewProviderLinkObserver()
|
|
{
|
|
// TODO: any better way to get deleted automatically?
|
|
m_isPythonExtension = true;
|
|
initExtensionType(ViewProviderLinkObserver::getExtensionClassTypeId());
|
|
}
|
|
|
|
ViewProviderLinkObserver::~ViewProviderLinkObserver()
|
|
{
|
|
if (linkInfo) {
|
|
linkInfo->detach(true);
|
|
linkInfo.reset();
|
|
}
|
|
}
|
|
|
|
bool ViewProviderLinkObserver::isLinkVisible() const
|
|
{
|
|
if (linkInfo) {
|
|
return linkInfo->isVisible();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ViewProviderLinkObserver::setLinkVisible(bool visible)
|
|
{
|
|
if (linkInfo) {
|
|
linkInfo->setVisible(visible);
|
|
}
|
|
}
|
|
|
|
void ViewProviderLinkObserver::extensionBeforeDelete()
|
|
{
|
|
if (linkInfo) {
|
|
linkInfo->detach(false);
|
|
}
|
|
}
|
|
|
|
void ViewProviderLinkObserver::extensionReattach(App::DocumentObject*)
|
|
{
|
|
if (linkInfo) {
|
|
linkInfo->pcLinked = freecad_cast<ViewProviderDocumentObject*>(getExtendedContainer());
|
|
linkInfo->update();
|
|
}
|
|
}
|
|
|
|
void ViewProviderLinkObserver::extensionOnChanged(const App::Property* prop)
|
|
{
|
|
(void)prop;
|
|
}
|
|
|
|
void ViewProviderLinkObserver::extensionModeSwitchChange()
|
|
{
|
|
auto owner = freecad_cast<ViewProviderDocumentObject*>(getExtendedContainer());
|
|
if (owner && linkInfo) {
|
|
linkInfo->updateSwitch();
|
|
}
|
|
}
|
|
|
|
void ViewProviderLinkObserver::extensionUpdateData(const App::Property* prop)
|
|
{
|
|
if (linkInfo && linkInfo->pcLinked && linkInfo->pcLinked->getObject()
|
|
&& prop != &linkInfo->pcLinked->getObject()->Visibility) {
|
|
linkInfo->updateData(prop);
|
|
}
|
|
}
|
|
|
|
void ViewProviderLinkObserver::extensionFinishRestoring()
|
|
{
|
|
if (linkInfo) {
|
|
FC_TRACE("linked finish restoing");
|
|
linkInfo->update();
|
|
}
|
|
}
|
|
|
|
class LinkView::SubInfo: public LinkOwner
|
|
{
|
|
public:
|
|
LinkInfoPtr linkInfo;
|
|
LinkView& handle;
|
|
CoinPtr<SoSeparator> pcNode;
|
|
CoinPtr<SoTransform> pcTransform;
|
|
std::set<std::string> subElements;
|
|
|
|
friend LinkView;
|
|
|
|
SubInfo(LinkView& handle)
|
|
: handle(handle)
|
|
{
|
|
pcNode = new SoFCSelectionRoot(true);
|
|
pcTransform = new SoTransform;
|
|
pcNode->addChild(pcTransform);
|
|
}
|
|
|
|
~SubInfo() override
|
|
{
|
|
unlink();
|
|
auto root = handle.getLinkRoot();
|
|
if (root) {
|
|
int idx = root->findChild(pcNode);
|
|
if (idx >= 0) {
|
|
root->removeChild(idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
void onLinkedIconChange(LinkInfoPtr) override
|
|
{
|
|
if (handle.autoSubLink && handle.subInfo.size() == 1) {
|
|
handle.onLinkedIconChange(handle.linkInfo);
|
|
}
|
|
}
|
|
|
|
void unlink(LinkInfoPtr info = LinkInfoPtr()) override
|
|
{
|
|
(void)info;
|
|
if (linkInfo) {
|
|
linkInfo->remove(this);
|
|
linkInfo.reset();
|
|
}
|
|
coinRemoveAllChildren(pcNode);
|
|
pcNode->addChild(pcTransform);
|
|
}
|
|
|
|
void link(App::DocumentObject* obj)
|
|
{
|
|
if (isLinked() && linkInfo->pcLinked->getObject() == obj) {
|
|
return;
|
|
}
|
|
unlink();
|
|
linkInfo = LinkInfo::get(obj, this);
|
|
if (linkInfo) {
|
|
pcNode->addChild(linkInfo->getSnapshot(LinkView::SnapshotTransform));
|
|
}
|
|
}
|
|
|
|
bool isLinked() const
|
|
{
|
|
return linkInfo && linkInfo->isLinked();
|
|
}
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////
|
|
|
|
class LinkView::Element: public LinkOwner
|
|
{
|
|
public:
|
|
LinkInfoPtr linkInfo;
|
|
LinkView& handle;
|
|
CoinPtr<SoSwitch> pcSwitch;
|
|
CoinPtr<SoFCSelectionRoot> pcRoot;
|
|
CoinPtr<SoTransform> pcTransform;
|
|
int groupIndex = -1;
|
|
bool isGroup = false;
|
|
|
|
friend LinkView;
|
|
|
|
Element(LinkView& handle)
|
|
: handle(handle)
|
|
{
|
|
pcTransform = new SoTransform;
|
|
pcRoot = new SoFCSelectionRoot(true);
|
|
pcSwitch = new SoSwitch;
|
|
pcSwitch->addChild(pcRoot);
|
|
pcSwitch->whichChild = 0;
|
|
}
|
|
|
|
~Element() override
|
|
{
|
|
unlink();
|
|
auto root = handle.getLinkRoot();
|
|
if (root) {
|
|
int idx = root->findChild(pcRoot);
|
|
if (idx >= 0) {
|
|
root->removeChild(idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
void unlink(LinkInfoPtr info = LinkInfoPtr()) override
|
|
{
|
|
(void)info;
|
|
if (linkInfo) {
|
|
linkInfo->remove(this);
|
|
linkInfo.reset();
|
|
}
|
|
coinRemoveAllChildren(pcRoot);
|
|
}
|
|
|
|
void link(App::DocumentObject* obj)
|
|
{
|
|
if (isLinked() && linkInfo->pcLinked->getObject() == obj) {
|
|
return;
|
|
}
|
|
unlink();
|
|
linkInfo = LinkInfo::get(obj, this);
|
|
if (isLinked()) {
|
|
pcRoot->addChild(linkInfo->getSnapshot(handle.childType));
|
|
}
|
|
}
|
|
|
|
bool isLinked() const
|
|
{
|
|
return linkInfo && linkInfo->isLinked();
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
TYPESYSTEM_SOURCE(Gui::LinkView, Base::BaseClass)
|
|
|
|
LinkView::LinkView()
|
|
: nodeType(SnapshotTransform)
|
|
, childType((SnapshotType)-1)
|
|
, autoSubLink(true)
|
|
{
|
|
pcLinkRoot = new SoFCSelectionRoot;
|
|
}
|
|
|
|
LinkView::~LinkView()
|
|
{
|
|
unlink(linkInfo);
|
|
unlink(linkOwner);
|
|
}
|
|
|
|
PyObject* LinkView::getPyObject()
|
|
{
|
|
if (PythonObject.is(Py::_None())) {
|
|
PythonObject = Py::Object(new LinkViewPy(this), true);
|
|
}
|
|
return Py::new_reference_to(PythonObject);
|
|
}
|
|
|
|
void LinkView::setInvalid()
|
|
{
|
|
if (!PythonObject.is(Py::_None())) {
|
|
auto obj = static_cast<Base::PyObjectBase*>(PythonObject.ptr());
|
|
obj->setInvalid();
|
|
obj->DecRef();
|
|
}
|
|
else {
|
|
delete this;
|
|
}
|
|
}
|
|
|
|
Base::BoundBox3d _getBoundBox(ViewProviderDocumentObject* vpd, SoNode* rootNode)
|
|
{
|
|
auto doc = vpd->getDocument();
|
|
if (!doc) {
|
|
LINK_THROW(Base::RuntimeError, "no document");
|
|
}
|
|
Gui::MDIView* view = doc->getViewOfViewProvider(vpd);
|
|
if (!view) {
|
|
LINK_THROW(Base::RuntimeError, "no view");
|
|
}
|
|
|
|
Gui::View3DInventorViewer* viewer = static_cast<Gui::View3DInventor*>(view)->getViewer();
|
|
SoGetBoundingBoxAction bboxAction(viewer->getSoRenderManager()->getViewportRegion());
|
|
bboxAction.apply(rootNode);
|
|
auto bbox = bboxAction.getBoundingBox();
|
|
float minX, minY, minZ, maxX, maxY, maxZ;
|
|
bbox.getMax().getValue(maxX, maxY, maxZ);
|
|
bbox.getMin().getValue(minX, minY, minZ);
|
|
return Base::BoundBox3d(minX, minY, minZ, maxX, maxY, maxZ);
|
|
}
|
|
|
|
Base::BoundBox3d LinkView::getBoundBox(ViewProviderDocumentObject* vpd) const
|
|
{
|
|
if (!vpd) {
|
|
if (!linkOwner || !linkOwner->isLinked()) {
|
|
LINK_THROW(Base::ValueError, "no ViewProvider");
|
|
}
|
|
vpd = linkOwner->pcLinked;
|
|
}
|
|
return _getBoundBox(vpd, pcLinkRoot);
|
|
}
|
|
|
|
ViewProviderDocumentObject* LinkView::getOwner() const
|
|
{
|
|
if (linkOwner && linkOwner->isLinked()) {
|
|
return linkOwner->pcLinked;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void LinkView::setOwner(ViewProviderDocumentObject* vpd)
|
|
{
|
|
unlink(linkOwner);
|
|
linkOwner = LinkInfo::get(vpd, this);
|
|
}
|
|
|
|
bool LinkView::isLinked() const
|
|
{
|
|
return linkInfo && linkInfo->isLinked();
|
|
}
|
|
|
|
void LinkView::setDrawStyle(int style, double lineWidth, double pointSize)
|
|
{
|
|
if (!pcDrawStyle) {
|
|
if (!style) {
|
|
return;
|
|
}
|
|
pcDrawStyle = new SoDrawStyle;
|
|
pcDrawStyle->style = SoDrawStyle::FILLED;
|
|
pcLinkRoot->insertChild(pcDrawStyle, 0);
|
|
}
|
|
if (!style) {
|
|
pcDrawStyle->setOverride(false);
|
|
return;
|
|
}
|
|
pcDrawStyle->lineWidth = lineWidth;
|
|
pcDrawStyle->pointSize = pointSize;
|
|
switch (style) {
|
|
case 2:
|
|
pcDrawStyle->linePattern = 0xf00f;
|
|
break;
|
|
case 3:
|
|
pcDrawStyle->linePattern = 0x0f0f;
|
|
break;
|
|
case 4:
|
|
pcDrawStyle->linePattern = 0xff88;
|
|
break;
|
|
default:
|
|
pcDrawStyle->linePattern = 0xffff;
|
|
}
|
|
pcDrawStyle->setOverride(true);
|
|
}
|
|
|
|
void LinkView::renderDoubleSide(bool enable)
|
|
{
|
|
if (enable) {
|
|
if (!pcShapeHints) {
|
|
pcShapeHints = new SoShapeHints;
|
|
pcShapeHints->vertexOrdering = SoShapeHints::CLOCKWISE;
|
|
pcShapeHints->shapeType = SoShapeHints::UNKNOWN_SHAPE_TYPE;
|
|
pcLinkRoot->insertChild(pcShapeHints, 0);
|
|
}
|
|
pcShapeHints->setOverride(true);
|
|
}
|
|
else if (pcShapeHints) {
|
|
pcShapeHints->setOverride(false);
|
|
}
|
|
}
|
|
|
|
void LinkView::setMaterial(int index, const App::Material* material)
|
|
{
|
|
if (index < 0) {
|
|
if (!material) {
|
|
pcLinkRoot->removeColorOverride();
|
|
return;
|
|
}
|
|
Base::Color c = material->diffuseColor;
|
|
c.setTransparency(material->transparency);
|
|
pcLinkRoot->setColorOverride(c);
|
|
for (int i = 0; i < getSize(); ++i) {
|
|
setMaterial(i, nullptr);
|
|
}
|
|
}
|
|
else if (index >= (int)nodeArray.size()) {
|
|
LINK_THROW(Base::ValueError, "LinkView: material index out of range");
|
|
}
|
|
else {
|
|
auto& info = *nodeArray[index];
|
|
if (!material) {
|
|
info.pcRoot->removeColorOverride();
|
|
return;
|
|
}
|
|
Base::Color c = material->diffuseColor;
|
|
c.setTransparency(material->transparency);
|
|
info.pcRoot->setColorOverride(c);
|
|
}
|
|
}
|
|
|
|
void LinkView::setLink(App::DocumentObject* obj, const std::vector<std::string>& subs)
|
|
{
|
|
setLinkViewObject(
|
|
freecad_cast<ViewProviderDocumentObject*>(Application::Instance->getViewProvider(obj)),
|
|
subs
|
|
);
|
|
}
|
|
|
|
void LinkView::setLinkViewObject(ViewProviderDocumentObject* vpd, const std::vector<std::string>& subs)
|
|
{
|
|
if (!isLinked() || linkInfo->pcLinked != vpd) {
|
|
unlink(linkInfo);
|
|
linkInfo = LinkInfo::get(vpd, this);
|
|
if (!linkInfo) {
|
|
return;
|
|
}
|
|
}
|
|
subInfo.clear();
|
|
for (const auto& sub : subs) {
|
|
if (sub.empty()) {
|
|
continue;
|
|
}
|
|
const char* subelement = Data::findElementName(sub.c_str());
|
|
std::string subname = sub.substr(0, subelement - sub.c_str());
|
|
auto it = subInfo.find(subname);
|
|
if (it == subInfo.end()) {
|
|
it = subInfo.insert(std::make_pair(subname, std::unique_ptr<SubInfo>())).first;
|
|
it->second = std::make_unique<SubInfo>(*this);
|
|
}
|
|
if (subelement[0]) {
|
|
it->second->subElements.insert(subelement);
|
|
}
|
|
}
|
|
updateLink();
|
|
}
|
|
|
|
void LinkView::setTransform(SoTransform* pcTransform, const Base::Matrix4D& mat)
|
|
{
|
|
double dMtrx[16];
|
|
mat.getGLMatrix(dMtrx);
|
|
pcTransform->setMatrix(SbMatrix(
|
|
dMtrx[0],
|
|
dMtrx[1],
|
|
dMtrx[2],
|
|
dMtrx[3],
|
|
dMtrx[4],
|
|
dMtrx[5],
|
|
dMtrx[6],
|
|
dMtrx[7],
|
|
dMtrx[8],
|
|
dMtrx[9],
|
|
dMtrx[10],
|
|
dMtrx[11],
|
|
dMtrx[12],
|
|
dMtrx[13],
|
|
dMtrx[14],
|
|
dMtrx[15]
|
|
));
|
|
}
|
|
|
|
void LinkView::setSize(int _size)
|
|
{
|
|
size_t size = _size < 0 ? 0 : (size_t)_size;
|
|
if (childType < 0 && size == nodeArray.size()) {
|
|
return;
|
|
}
|
|
resetRoot();
|
|
if (!size || childType >= 0) {
|
|
nodeArray.clear();
|
|
nodeMap.clear();
|
|
if (!size && childType < 0) {
|
|
if (pcLinkedRoot) {
|
|
pcLinkRoot->addChild(pcLinkedRoot);
|
|
}
|
|
return;
|
|
}
|
|
childType = SnapshotContainer;
|
|
}
|
|
if (size < nodeArray.size()) {
|
|
for (size_t i = size; i < nodeArray.size(); ++i) {
|
|
nodeMap.erase(nodeArray[i]->pcSwitch);
|
|
}
|
|
nodeArray.resize(size);
|
|
}
|
|
for (const auto& info : nodeArray) {
|
|
pcLinkRoot->addChild(info->pcSwitch);
|
|
}
|
|
|
|
while (nodeArray.size() < size) {
|
|
nodeArray.push_back(std::make_unique<Element>(*this));
|
|
auto& info = *nodeArray.back();
|
|
info.pcRoot->addChild(info.pcTransform);
|
|
if (pcLinkedRoot) {
|
|
info.pcRoot->addChild(pcLinkedRoot);
|
|
}
|
|
pcLinkRoot->addChild(info.pcSwitch);
|
|
nodeMap.emplace(info.pcSwitch, (int)nodeArray.size() - 1);
|
|
}
|
|
}
|
|
|
|
void LinkView::resetRoot()
|
|
{
|
|
coinRemoveAllChildren(pcLinkRoot);
|
|
if (pcTransform) {
|
|
pcLinkRoot->addChild(pcTransform);
|
|
}
|
|
if (pcShapeHints) {
|
|
pcLinkRoot->addChild(pcShapeHints);
|
|
}
|
|
if (pcDrawStyle) {
|
|
pcLinkRoot->addChild(pcDrawStyle);
|
|
}
|
|
}
|
|
|
|
void LinkView::setChildren(
|
|
const std::vector<App::DocumentObject*>& children,
|
|
const boost::dynamic_bitset<>& vis,
|
|
SnapshotType type
|
|
)
|
|
{
|
|
if (children.empty()) {
|
|
if (!nodeArray.empty()) {
|
|
nodeArray.clear();
|
|
nodeMap.clear();
|
|
childType = SnapshotContainer;
|
|
resetRoot();
|
|
if (pcLinkedRoot) {
|
|
pcLinkRoot->addChild(pcLinkedRoot);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (type < 0 || type >= SnapshotMax) {
|
|
LINK_THROW(Base::ValueError, "invalid children type");
|
|
}
|
|
|
|
resetRoot();
|
|
|
|
if (childType < 0) {
|
|
nodeArray.clear();
|
|
}
|
|
childType = type;
|
|
|
|
if (nodeArray.size() > children.size()) {
|
|
nodeArray.resize(children.size());
|
|
}
|
|
else {
|
|
nodeArray.reserve(children.size());
|
|
}
|
|
|
|
std::map<App::DocumentObject*, size_t> groups;
|
|
for (size_t i = 0; i < children.size(); ++i) {
|
|
auto obj = children[i];
|
|
if (nodeArray.size() <= i) {
|
|
nodeArray.push_back(std::make_unique<Element>(*this));
|
|
}
|
|
auto& info = *nodeArray[i];
|
|
info.isGroup = false;
|
|
info.groupIndex = -1;
|
|
info.pcSwitch->whichChild = (vis.size() <= i || vis[i]) ? 0 : SO_SWITCH_NONE;
|
|
info.link(obj);
|
|
if (obj->hasExtension(App::GroupExtension::getExtensionClassTypeId(), false)) {
|
|
info.isGroup = true;
|
|
coinRemoveAllChildren(info.pcRoot);
|
|
groups.emplace(obj, i);
|
|
}
|
|
}
|
|
nodeMap.clear();
|
|
for (size_t i = 0; i < nodeArray.size(); ++i) {
|
|
auto& info = *nodeArray[i];
|
|
nodeMap.emplace(info.pcSwitch, i);
|
|
if (info.isLinked() && !groups.empty()) {
|
|
auto iter = groups.find(
|
|
App::GroupExtension::getGroupOfObject(info.linkInfo->pcLinked->getObject())
|
|
);
|
|
if (iter != groups.end()) {
|
|
info.groupIndex = iter->second;
|
|
auto& groupInfo = *nodeArray[iter->second];
|
|
groupInfo.pcRoot->addChild(info.pcSwitch);
|
|
continue;
|
|
}
|
|
}
|
|
pcLinkRoot->addChild(info.pcSwitch);
|
|
}
|
|
}
|
|
|
|
std::vector<ViewProviderDocumentObject*> LinkView::getChildren() const
|
|
{
|
|
std::vector<ViewProviderDocumentObject*> ret;
|
|
for (const auto& info : nodeArray) {
|
|
if (info->isLinked()) {
|
|
ret.push_back(info->linkInfo->pcLinked);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void LinkView::setTransform(int index, const Base::Matrix4D& mat)
|
|
{
|
|
if (index < 0) {
|
|
if (!pcTransform) {
|
|
pcTransform = new SoTransform;
|
|
pcLinkRoot->insertChild(pcTransform, 0);
|
|
}
|
|
setTransform(pcTransform, mat);
|
|
return;
|
|
}
|
|
if (index < 0 || index >= (int)nodeArray.size()) {
|
|
LINK_THROW(Base::ValueError, "LinkView: index out of range");
|
|
}
|
|
setTransform(nodeArray[index]->pcTransform, mat);
|
|
}
|
|
|
|
void LinkView::setElementVisible(int idx, bool visible)
|
|
{
|
|
if (idx >= 0 && idx < (int)nodeArray.size()) {
|
|
nodeArray[idx]->pcSwitch->whichChild = visible ? 0 : SO_SWITCH_NONE;
|
|
}
|
|
}
|
|
|
|
bool LinkView::isElementVisible(int idx) const
|
|
{
|
|
if (idx >= 0 && idx < (int)nodeArray.size()) {
|
|
return nodeArray[idx]->pcSwitch->whichChild.getValue() >= 0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
ViewProviderDocumentObject* LinkView::getLinkedView() const
|
|
{
|
|
auto link = linkInfo;
|
|
if (autoSubLink && subInfo.size() == 1) {
|
|
link = subInfo.begin()->second->linkInfo;
|
|
}
|
|
return link ? link->pcLinked : nullptr;
|
|
}
|
|
|
|
std::vector<std::string> LinkView::getSubNames() const
|
|
{
|
|
std::vector<std::string> ret;
|
|
for (const auto& v : subInfo) {
|
|
if (v.second->subElements.empty()) {
|
|
ret.push_back(v.first);
|
|
continue;
|
|
}
|
|
for (const auto& s : v.second->subElements) {
|
|
ret.push_back(v.first + s);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void LinkView::setNodeType(SnapshotType type, bool sublink)
|
|
{
|
|
autoSubLink = sublink;
|
|
if (nodeType == type) {
|
|
return;
|
|
}
|
|
if (type >= SnapshotMax
|
|
|| (type < 0 && type != SnapshotContainer && type != SnapshotContainerTransform)) {
|
|
LINK_THROW(Base::ValueError, "LinkView: invalid node type");
|
|
}
|
|
|
|
if (nodeType >= 0 && type < 0) {
|
|
if (pcLinkedRoot) {
|
|
SoSelectionElementAction action(SoSelectionElementAction::None, true);
|
|
action.apply(pcLinkedRoot);
|
|
}
|
|
replaceLinkedRoot(CoinPtr<SoSeparator>(new SoFCSelectionRoot));
|
|
}
|
|
else if (nodeType < 0 && type >= 0) {
|
|
if (isLinked()) {
|
|
replaceLinkedRoot(linkInfo->getSnapshot(type));
|
|
}
|
|
else {
|
|
replaceLinkedRoot(nullptr);
|
|
}
|
|
}
|
|
nodeType = type;
|
|
updateLink();
|
|
}
|
|
|
|
void LinkView::replaceLinkedRoot(SoSeparator* root)
|
|
{
|
|
if (root == pcLinkedRoot) {
|
|
return;
|
|
}
|
|
if (nodeArray.empty()) {
|
|
if (pcLinkedRoot && root) {
|
|
pcLinkRoot->replaceChild(pcLinkedRoot, root);
|
|
}
|
|
else if (root) {
|
|
pcLinkRoot->addChild(root);
|
|
}
|
|
else {
|
|
resetRoot();
|
|
}
|
|
}
|
|
else if (childType < 0) {
|
|
if (pcLinkedRoot && root) {
|
|
for (const auto& info : nodeArray) {
|
|
info->pcRoot->replaceChild(pcLinkedRoot, root);
|
|
}
|
|
}
|
|
else if (root) {
|
|
for (const auto& info : nodeArray) {
|
|
info->pcRoot->addChild(root);
|
|
}
|
|
}
|
|
else {
|
|
for (const auto& info : nodeArray) {
|
|
info->pcRoot->removeChild(pcLinkedRoot);
|
|
}
|
|
}
|
|
}
|
|
pcLinkedRoot = root;
|
|
}
|
|
|
|
void LinkView::onLinkedIconChange(LinkInfoPtr info)
|
|
{
|
|
if (info == linkInfo && info != linkOwner && linkOwner && linkOwner->isLinked()) {
|
|
linkOwner->pcLinked->signalChangeIcon();
|
|
}
|
|
}
|
|
|
|
void LinkView::onLinkedUpdateData(LinkInfoPtr info, const App::Property* prop)
|
|
{
|
|
if (info != linkInfo || !linkOwner || !linkOwner->isLinked() || info == linkOwner) {
|
|
return;
|
|
}
|
|
auto ext = linkOwner->pcLinked->getObject()->getExtensionByType<App::LinkBaseExtension>(true);
|
|
if (ext && !(prop->getType() & App::Prop_Output) && !prop->testStatus(App::Property::Output)) {
|
|
// propagate the signalChangedObject to potentially multiple levels
|
|
// of links, to inform tree view of children change, and other
|
|
// parent objects about the change. But we need to be careful to not
|
|
// touch the object if the property of change is marked as output.
|
|
ext->_LinkTouched.touch();
|
|
}
|
|
else {
|
|
// In case the owner object does not have link extension, here is a
|
|
// trick to link the signalChangedObject from linked object to the
|
|
// owner
|
|
linkOwner->pcLinked->getDocument()->signalChangedObject(
|
|
*linkOwner->pcLinked,
|
|
linkOwner->pcLinked->getObject()->Label
|
|
);
|
|
}
|
|
}
|
|
|
|
void LinkView::updateLink()
|
|
{
|
|
if (!isLinked()) {
|
|
return;
|
|
}
|
|
|
|
if (linkOwner && linkOwner->isLinked() && linkOwner->pcLinked->isRestoring()) {
|
|
FC_TRACE("restoring '" << linkOwner->pcLinked->getObject()->getFullName() << "'");
|
|
return;
|
|
}
|
|
|
|
// TODO: is it a good idea to clear any selection here?
|
|
pcLinkRoot->resetContext();
|
|
|
|
if (nodeType >= 0) {
|
|
replaceLinkedRoot(linkInfo->getSnapshot(nodeType));
|
|
return;
|
|
}
|
|
|
|
// rebuild link sub objects tree
|
|
CoinPtr<SoSeparator> linkedRoot = pcLinkedRoot;
|
|
if (!linkedRoot) {
|
|
linkedRoot = new SoFCSelectionRoot;
|
|
}
|
|
else {
|
|
SoSelectionElementAction action(SoSelectionElementAction::None, true);
|
|
action.apply(linkedRoot);
|
|
coinRemoveAllChildren(linkedRoot);
|
|
}
|
|
|
|
SoTempPath path(10);
|
|
path.ref();
|
|
appendPath(&path, linkedRoot);
|
|
auto obj = linkInfo->pcLinked->getObject();
|
|
for (const auto& v : subInfo) {
|
|
auto& sub = *v.second;
|
|
Base::Matrix4D mat;
|
|
App::DocumentObject* sobj
|
|
= obj->getSubObject(v.first.c_str(), nullptr, &mat, nodeType == SnapshotContainer);
|
|
if (!sobj) {
|
|
sub.unlink();
|
|
continue;
|
|
}
|
|
sub.link(sobj);
|
|
linkedRoot->addChild(sub.pcNode);
|
|
setTransform(sub.pcTransform, mat);
|
|
|
|
if (!sub.subElements.empty()) {
|
|
path.truncate(1);
|
|
appendPath(&path, sub.pcNode);
|
|
SoSelectionElementAction action(SoSelectionElementAction::Append, true);
|
|
for (const auto& subelement : sub.subElements) {
|
|
path.truncate(2);
|
|
SoDetail* det = nullptr;
|
|
if (!sub.linkInfo->getDetail(false, SnapshotTransform, subelement.c_str(), det, &path)) {
|
|
continue;
|
|
}
|
|
action.setElement(det);
|
|
action.apply(&path);
|
|
delete det;
|
|
}
|
|
}
|
|
}
|
|
path.unrefNoDelete();
|
|
replaceLinkedRoot(linkedRoot);
|
|
}
|
|
|
|
bool LinkView::linkGetElementPicked(const SoPickedPoint* pp, std::string& subname) const
|
|
{
|
|
std::ostringstream ss;
|
|
CoinPtr<SoPath> path = pp->getPath();
|
|
if (!nodeArray.empty()) {
|
|
auto idx = path->findNode(pcLinkRoot);
|
|
if (idx < 0 || idx + 2 >= path->getLength()) {
|
|
return false;
|
|
}
|
|
auto node = path->getNode(idx + 1);
|
|
auto it = nodeMap.find(node);
|
|
if (it == nodeMap.end() || !isElementVisible(it->second)) {
|
|
return false;
|
|
}
|
|
int nodeIdx = it->second;
|
|
++idx;
|
|
while (nodeArray[nodeIdx]->isGroup) {
|
|
auto& info = *nodeArray[nodeIdx];
|
|
if (!info.isLinked()) {
|
|
return false;
|
|
}
|
|
ss << info.linkInfo->getLinkedName() << '.';
|
|
idx += 2;
|
|
if (idx >= path->getLength()) {
|
|
return false;
|
|
}
|
|
auto iter = nodeMap.find(path->getNode(idx));
|
|
if (iter == nodeMap.end() || !isElementVisible(iter->second)) {
|
|
return false;
|
|
}
|
|
nodeIdx = iter->second;
|
|
}
|
|
|
|
auto& info = *nodeArray[nodeIdx];
|
|
if (!info.linkInfo) {
|
|
ss << it->second << '.';
|
|
}
|
|
else {
|
|
ss << info.linkInfo->getLinkedName() << '.';
|
|
}
|
|
|
|
if (info.isLinked()) {
|
|
if (!info.linkInfo->getElementPicked(false, childType, pp, ss)) {
|
|
return false;
|
|
}
|
|
subname = ss.str();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!isLinked()) {
|
|
return false;
|
|
}
|
|
|
|
if (nodeType >= 0) {
|
|
if (linkInfo->getElementPicked(false, nodeType, pp, ss)) {
|
|
subname = ss.str();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
auto idx = path->findNode(pcLinkedRoot);
|
|
if (idx < 0 || idx + 1 >= path->getLength()) {
|
|
return false;
|
|
}
|
|
auto node = path->getNode(idx + 1);
|
|
for (const auto& v : subInfo) {
|
|
auto& sub = *v.second;
|
|
if (node != sub.pcNode) {
|
|
continue;
|
|
}
|
|
|
|
std::ostringstream ss2;
|
|
if (!sub.linkInfo->getElementPicked(false, SnapshotTransform, pp, ss2)) {
|
|
return false;
|
|
}
|
|
const std::string& element = ss2.str();
|
|
if (!sub.subElements.empty()) {
|
|
if (sub.subElements.find(element) == sub.subElements.end()) {
|
|
auto pos = element.find('.');
|
|
if (pos == std::string::npos
|
|
|| sub.subElements.find(element.c_str() + pos + 1) == sub.subElements.end()) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
if (!autoSubLink || subInfo.size() > 1) {
|
|
ss << v.first;
|
|
}
|
|
ss << element;
|
|
subname = ss.str();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool LinkView::getGroupHierarchy(int index, SoFullPath* path) const
|
|
{
|
|
if (index > (int)nodeArray.size()) {
|
|
return false;
|
|
}
|
|
auto& info = *nodeArray[index];
|
|
if (info.groupIndex >= 0 && !getGroupHierarchy(info.groupIndex, path)) {
|
|
return false;
|
|
}
|
|
appendPath(path, info.pcSwitch);
|
|
appendPath(path, info.pcRoot);
|
|
return true;
|
|
}
|
|
|
|
bool LinkView::linkGetDetailPath(const char* subname, SoFullPath* path, SoDetail*& det) const
|
|
{
|
|
if (!subname || *subname == 0) {
|
|
return true;
|
|
}
|
|
auto len = path->getLength();
|
|
if (nodeArray.empty()) {
|
|
if (!appendPathSafe(path, pcLinkRoot)) {
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
int idx = -1;
|
|
if (subname[0] >= '0' && subname[0] <= '9') {
|
|
idx = App::LinkBaseExtension::getArrayIndex(subname, &subname);
|
|
}
|
|
else {
|
|
while (true) {
|
|
const char* dot = strchr(subname, '.');
|
|
if (!dot) {
|
|
break;
|
|
}
|
|
int i = 0;
|
|
if (subname[0] == '$') {
|
|
CharRange name(subname + 1, dot);
|
|
for (const auto& info : nodeArray) {
|
|
if (info->isLinked()
|
|
&& boost::equals(name, info->linkInfo->getLinkedLabel())) {
|
|
idx = i;
|
|
break;
|
|
}
|
|
++i;
|
|
}
|
|
}
|
|
else {
|
|
CharRange name(subname, dot);
|
|
for (const auto& info : nodeArray) {
|
|
if (info->isLinked() && boost::equals(name, info->linkInfo->getLinkedName())) {
|
|
idx = i;
|
|
break;
|
|
}
|
|
++i;
|
|
}
|
|
}
|
|
|
|
if (idx < 0) {
|
|
return false;
|
|
}
|
|
|
|
subname = dot + 1;
|
|
if (!subname[0] || nodeArray[idx]->isGroup == 0) {
|
|
break;
|
|
}
|
|
idx = -1;
|
|
}
|
|
}
|
|
|
|
if (idx < 0 || idx >= (int)nodeArray.size()) {
|
|
return false;
|
|
}
|
|
auto& info = *nodeArray[idx];
|
|
if (!appendPathSafe(path, pcLinkRoot)) {
|
|
return false;
|
|
}
|
|
if (info.groupIndex >= 0 && !getGroupHierarchy(info.groupIndex, path)) {
|
|
return false;
|
|
}
|
|
appendPath(path, info.pcSwitch);
|
|
appendPath(path, info.pcRoot);
|
|
|
|
if (*subname == 0) {
|
|
return true;
|
|
}
|
|
|
|
if (info.isLinked()) {
|
|
if (info.linkInfo->getDetail(false, childType, subname, det, path)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
if (isLinked()) {
|
|
if (nodeType >= 0) {
|
|
if (linkInfo->getDetail(false, nodeType, subname, det, path)) {
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
appendPath(path, pcLinkedRoot);
|
|
for (const auto& v : subInfo) {
|
|
auto& sub = *v.second;
|
|
if (!sub.isLinked()) {
|
|
continue;
|
|
}
|
|
const char* nextsub;
|
|
if (autoSubLink && subInfo.size() == 1) {
|
|
nextsub = subname;
|
|
}
|
|
else {
|
|
if (!boost::algorithm::starts_with(subname, v.first)) {
|
|
continue;
|
|
}
|
|
nextsub = subname + v.first.size();
|
|
if (*nextsub != '.') {
|
|
continue;
|
|
}
|
|
++nextsub;
|
|
}
|
|
if (*nextsub && !sub.subElements.empty()
|
|
&& sub.subElements.find(nextsub) == sub.subElements.end()) {
|
|
break;
|
|
}
|
|
appendPath(path, sub.pcNode);
|
|
len = path->getLength();
|
|
if (sub.linkInfo->getDetail(false, SnapshotTransform, nextsub, det, path)) {
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
path->truncate(len);
|
|
return false;
|
|
}
|
|
|
|
void LinkView::unlink(LinkInfoPtr info)
|
|
{
|
|
if (!info) {
|
|
return;
|
|
}
|
|
if (info == linkOwner) {
|
|
linkOwner->remove(this);
|
|
linkOwner.reset();
|
|
}
|
|
if (info != linkInfo) {
|
|
return;
|
|
}
|
|
if (linkInfo) {
|
|
linkInfo->remove(this);
|
|
linkInfo.reset();
|
|
}
|
|
pcLinkRoot->resetContext();
|
|
if (pcLinkedRoot) {
|
|
if (nodeArray.empty()) {
|
|
resetRoot();
|
|
}
|
|
else {
|
|
for (const auto& info : nodeArray) {
|
|
int idx;
|
|
if (info->isLinked() && (idx = info->pcRoot->findChild(pcLinkedRoot)) >= 0) {
|
|
info->pcRoot->removeChild(idx);
|
|
}
|
|
}
|
|
}
|
|
pcLinkedRoot.reset();
|
|
}
|
|
subInfo.clear();
|
|
return;
|
|
}
|
|
|
|
QIcon LinkView::getLinkedIcon(QPixmap px) const
|
|
{
|
|
auto link = linkInfo;
|
|
if (autoSubLink && subInfo.size() == 1) {
|
|
link = subInfo.begin()->second->linkInfo;
|
|
}
|
|
if (!link || !link->isLinked()) {
|
|
return QIcon();
|
|
}
|
|
return link->getIcon(px);
|
|
}
|
|
|
|
bool LinkView::hasSubs() const
|
|
{
|
|
return isLinked() && !subInfo.empty();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
PROPERTY_SOURCE(Gui::ViewProviderLink, Gui::ViewProviderDocumentObject)
|
|
|
|
static const char* _LinkIcon = "Link";
|
|
// static const char *_LinkArrayIcon = "LinkArray";
|
|
static const char* _LinkGroupIcon = "LinkGroup";
|
|
static const char* _LinkElementIcon = "LinkElement";
|
|
|
|
ViewProviderLink::ViewProviderLink()
|
|
: linkType(LinkTypeNone)
|
|
, hasSubName(false)
|
|
, hasSubElement(false)
|
|
, childVp(nullptr)
|
|
, overlayCacheKey(0)
|
|
{
|
|
sPixmap = _LinkIcon;
|
|
|
|
ADD_PROPERTY_TYPE(Selectable, (true), " Link", App::Prop_None, 0);
|
|
|
|
ADD_PROPERTY_TYPE(
|
|
OverrideMaterial,
|
|
(false),
|
|
" Link",
|
|
App::Prop_None,
|
|
"Override linked object's material"
|
|
);
|
|
|
|
App::Material mat(App::Material::DEFAULT);
|
|
mat.diffuseColor.setPackedValue(ViewParams::instance()->getDefaultLinkColor());
|
|
ADD_PROPERTY_TYPE(ShapeMaterial, (mat), " Link", App::Prop_None, 0);
|
|
ShapeMaterial.setStatus(App::Property::MaterialEdit, true);
|
|
|
|
ADD_PROPERTY_TYPE(DrawStyle, ((long int)0), " Link", App::Prop_None, "");
|
|
static const char* DrawStyleEnums[] = {"None", "Solid", "Dashed", "Dotted", "Dashdot", nullptr};
|
|
DrawStyle.setEnums(DrawStyleEnums);
|
|
|
|
int lwidth = ViewParams::instance()->getDefaultShapeLineWidth();
|
|
ADD_PROPERTY_TYPE(LineWidth, (lwidth), " Link", App::Prop_None, "");
|
|
|
|
static App::PropertyFloatConstraint::Constraints sizeRange = {1.0, 64.0, 1.0};
|
|
LineWidth.setConstraints(&sizeRange);
|
|
|
|
ADD_PROPERTY_TYPE(PointSize, (lwidth), " Link", App::Prop_None, "");
|
|
PointSize.setConstraints(&sizeRange);
|
|
|
|
ADD_PROPERTY(MaterialList, ());
|
|
MaterialList.setStatus(App::Property::NoMaterialListEdit, true);
|
|
|
|
ADD_PROPERTY(OverrideMaterialList, ());
|
|
ADD_PROPERTY(OverrideColorList, ());
|
|
|
|
ADD_PROPERTY(ChildViewProvider, (""));
|
|
ChildViewProvider.setStatus(App::Property::Hidden, true);
|
|
|
|
DisplayMode.setStatus(App::Property::Status::Hidden, true);
|
|
|
|
linkView = new LinkView;
|
|
|
|
pcPickStyle = new SoPickStyle;
|
|
pcPickStyle->ref();
|
|
}
|
|
|
|
ViewProviderLink::~ViewProviderLink()
|
|
{
|
|
pcPickStyle->unref();
|
|
linkView->setInvalid();
|
|
}
|
|
|
|
bool ViewProviderLink::isSelectable() const
|
|
{
|
|
return Selectable.getValue();
|
|
}
|
|
|
|
void ViewProviderLink::attach(App::DocumentObject* pcObj)
|
|
{
|
|
if (pcRoot->findChild(pcPickStyle) < 0) {
|
|
pcRoot->insertChild(pcPickStyle, 0);
|
|
}
|
|
|
|
SoNode* node = linkView->getLinkRoot();
|
|
node->setName(pcObj->getFullName().c_str());
|
|
addDisplayMaskMode(node, "Link");
|
|
if (childVp) {
|
|
childVpLink = LinkInfo::get(childVp, nullptr);
|
|
node = childVpLink->getSnapshot(LinkView::SnapshotTransform);
|
|
}
|
|
addDisplayMaskMode(node, "ChildView");
|
|
setDisplayMaskMode("Link");
|
|
inherited::attach(pcObj);
|
|
checkIcon();
|
|
if (pcObj->isDerivedFrom<App::LinkElement>()) {
|
|
hide();
|
|
}
|
|
linkView->setOwner(this);
|
|
}
|
|
|
|
void ViewProviderLink::reattach(App::DocumentObject* obj)
|
|
{
|
|
linkView->setOwner(this);
|
|
if (childVp) {
|
|
childVp->reattach(obj);
|
|
}
|
|
ViewProviderDocumentObject::reattach(obj);
|
|
}
|
|
|
|
std::vector<std::string> ViewProviderLink::getDisplayModes() const
|
|
{
|
|
std::vector<std::string> StrList = inherited::getDisplayModes();
|
|
StrList.emplace_back("Link");
|
|
StrList.emplace_back("ChildView");
|
|
return StrList;
|
|
}
|
|
|
|
QIcon ViewProviderLink::getIcon() const
|
|
{
|
|
auto ext = getLinkExtension();
|
|
if (ext) {
|
|
auto link = ext->getLinkedObjectValue();
|
|
if (link && link != getObject()) {
|
|
QPixmap overlay = getOverlayPixmap();
|
|
overlayCacheKey = overlay.cacheKey();
|
|
QIcon icon = linkView->getLinkedIcon(overlay);
|
|
if (!icon.isNull()) {
|
|
return icon;
|
|
}
|
|
}
|
|
}
|
|
overlayCacheKey = 0;
|
|
return Gui::BitmapFactory().pixmap(sPixmap);
|
|
}
|
|
|
|
QPixmap ViewProviderLink::getOverlayPixmap() const
|
|
{
|
|
auto ext = getLinkExtension();
|
|
constexpr int px = 12;
|
|
if (ext && ext->getLinkedObjectProperty() && ext->_getElementCountValue()) {
|
|
return BitmapFactory().pixmapFromSvg("LinkArrayOverlay", QSizeF(px, px));
|
|
}
|
|
else if (hasSubElement) {
|
|
return BitmapFactory().pixmapFromSvg("LinkSubElement", QSizeF(px, px));
|
|
}
|
|
else if (hasSubName) {
|
|
return BitmapFactory().pixmapFromSvg("LinkSubOverlay", QSizeF(px, px));
|
|
}
|
|
else {
|
|
return BitmapFactory().pixmapFromSvg("LinkOverlay", QSizeF(px, px));
|
|
}
|
|
}
|
|
|
|
void ViewProviderLink::onChanged(const App::Property* prop)
|
|
{
|
|
if (prop == &Selectable) {
|
|
pcPickStyle->style = Selectable.getValue() ? SoPickStyle::SHAPE : SoPickStyle::UNPICKABLE;
|
|
}
|
|
|
|
if (prop == &ChildViewProvider) {
|
|
childVp = freecad_cast<ViewProviderDocumentObject*>(ChildViewProvider.getObject().get());
|
|
if (childVp && getObject()) {
|
|
if (strcmp(childVp->getTypeId().getName(), getObject()->getViewProviderName()) != 0
|
|
&& !childVp->allowOverride(*getObject())) {
|
|
FC_ERR(
|
|
"Child view provider type '" << childVp->getTypeId().getName()
|
|
<< "' does not support "
|
|
<< getObject()->getFullName()
|
|
);
|
|
}
|
|
else {
|
|
childVp->setPropertyPrefix("ChildViewProvider.");
|
|
childVp->Visibility.setValue(getObject()->Visibility.getValue());
|
|
childVp->attach(getObject());
|
|
childVp->updateView();
|
|
childVp->setActiveMode();
|
|
if (pcModeSwitch->getNumChildren() > 1) {
|
|
childVpLink = LinkInfo::get(childVp, nullptr);
|
|
pcModeSwitch->replaceChild(1, childVpLink->getSnapshot(LinkView::SnapshotTransform));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (!isRestoring()) {
|
|
if (prop == &OverrideMaterial || prop == &ShapeMaterial || prop == &MaterialList
|
|
|| prop == &OverrideMaterialList) {
|
|
applyMaterial();
|
|
}
|
|
else if (prop == &OverrideColorList) {
|
|
applyColors();
|
|
}
|
|
else if (prop == &DrawStyle || prop == &PointSize || prop == &LineWidth) {
|
|
if (!DrawStyle.getValue()) {
|
|
linkView->setDrawStyle(0);
|
|
}
|
|
else {
|
|
linkView->setDrawStyle(DrawStyle.getValue(), LineWidth.getValue(), PointSize.getValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
inherited::onChanged(prop);
|
|
}
|
|
|
|
bool ViewProviderLink::setLinkType(App::LinkBaseExtension* ext)
|
|
{
|
|
auto propLink = ext->getLinkedObjectProperty();
|
|
if (!propLink) {
|
|
return false;
|
|
}
|
|
LinkType type;
|
|
if (hasSubName) {
|
|
type = LinkTypeSubs;
|
|
}
|
|
else {
|
|
type = LinkTypeNormal;
|
|
}
|
|
if (linkType != type) {
|
|
linkType = type;
|
|
}
|
|
switch (type) {
|
|
case LinkTypeSubs:
|
|
linkView->setNodeType(
|
|
ext->linkTransform() ? LinkView::SnapshotContainer : LinkView::SnapshotContainerTransform
|
|
);
|
|
break;
|
|
case LinkTypeNormal:
|
|
linkView->setNodeType(
|
|
ext->linkTransform() ? LinkView::SnapshotVisible : LinkView::SnapshotTransform
|
|
);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
App::LinkBaseExtension* ViewProviderLink::getLinkExtension()
|
|
{
|
|
if (!pcObject || !pcObject->isAttachedToDocument()) {
|
|
return nullptr;
|
|
}
|
|
return pcObject->getExtensionByType<App::LinkBaseExtension>(true);
|
|
}
|
|
|
|
const App::LinkBaseExtension* ViewProviderLink::getLinkExtension() const
|
|
{
|
|
if (!pcObject || !pcObject->isAttachedToDocument()) {
|
|
return nullptr;
|
|
}
|
|
return const_cast<App::DocumentObject*>(pcObject)->getExtensionByType<App::LinkBaseExtension>(true);
|
|
}
|
|
|
|
void ViewProviderLink::updateData(const App::Property* prop)
|
|
{
|
|
if (childVp) {
|
|
childVp->updateData(prop);
|
|
}
|
|
|
|
inherited::updateData(prop);
|
|
|
|
if (!isRestoring() && !pcObject->isRestoring()) {
|
|
auto ext = getLinkExtension();
|
|
if (ext) {
|
|
updateDataPrivate(getLinkExtension(), prop);
|
|
}
|
|
}
|
|
}
|
|
|
|
static inline bool canScale(const Base::Vector3d& v)
|
|
{
|
|
return fabs(v.x) > 1e-7 && fabs(v.y) > 1e-7 && fabs(v.z) > 1e-7;
|
|
}
|
|
|
|
void ViewProviderLink::updateDataPrivate(App::LinkBaseExtension* ext, const App::Property* prop)
|
|
{
|
|
if (!prop) {
|
|
return;
|
|
}
|
|
if (prop == &ext->_ChildCache) {
|
|
updateElementList(ext);
|
|
}
|
|
else if (prop == &ext->_LinkTouched) {
|
|
if (linkView->hasSubs()) {
|
|
linkView->updateLink();
|
|
}
|
|
applyColors();
|
|
checkIcon(ext);
|
|
}
|
|
else if (prop == ext->getColoredElementsProperty()) {
|
|
if (!prop->testStatus(App::Property::User3)) {
|
|
applyColors();
|
|
}
|
|
}
|
|
else if (prop == ext->getScaleProperty() || prop == ext->getScaleVectorProperty()) {
|
|
if (!prop->testStatus(App::Property::User3)) {
|
|
const auto& v = ext->getScaleVector();
|
|
if (canScale(v)) {
|
|
pcTransform->scaleFactor.setValue(v.x, v.y, v.z);
|
|
}
|
|
SbMatrix matrix = convert(ext->getTransform(false));
|
|
linkView->renderDoubleSide(matrix.det3() < 1e-7);
|
|
}
|
|
}
|
|
else if (prop == ext->getPlacementProperty() || prop == ext->getLinkPlacementProperty()) {
|
|
auto propLinkPlacement = ext->getLinkPlacementProperty();
|
|
if (!propLinkPlacement || propLinkPlacement == prop || prop == ext->getPlacementProperty()) {
|
|
const auto& v = ext->getScaleVector();
|
|
if (canScale(v)) {
|
|
pcTransform->scaleFactor.setValue(v.x, v.y, v.z);
|
|
}
|
|
SbMatrix matrix = convert(ext->getTransform(false));
|
|
linkView->renderDoubleSide(matrix.det3() < 1e-7);
|
|
}
|
|
}
|
|
else if (prop == ext->getLinkCopyOnChangeGroupProperty()) {
|
|
if (auto group = ext->getLinkCopyOnChangeGroupValue()) {
|
|
auto vp = freecad_cast<ViewProviderDocumentObject*>(
|
|
Application::Instance->getViewProvider(group)
|
|
);
|
|
if (vp) {
|
|
vp->hide();
|
|
vp->ShowInTree.setValue(false);
|
|
}
|
|
}
|
|
}
|
|
else if (prop == ext->getLinkedObjectProperty()) {
|
|
|
|
if (!prop->testStatus(App::Property::User3)) {
|
|
std::vector<std::string> subs;
|
|
const char* subname = ext->getSubName();
|
|
std::string sub;
|
|
if (subname) {
|
|
sub = subname;
|
|
}
|
|
hasSubElement = false;
|
|
for (const auto& s : ext->getSubElements()) {
|
|
if (s.empty()) {
|
|
continue;
|
|
}
|
|
hasSubElement = true;
|
|
subs.push_back(sub + s);
|
|
}
|
|
|
|
if (subs.empty() && !sub.empty()) {
|
|
subs.push_back(sub);
|
|
}
|
|
|
|
hasSubName = !subs.empty();
|
|
setLinkType(ext);
|
|
|
|
auto obj = ext->getLinkedObjectValue();
|
|
linkView->setLink(obj, subs);
|
|
|
|
if (ext->_getShowElementValue()) {
|
|
updateElementList(ext);
|
|
}
|
|
else {
|
|
updateDataPrivate(ext, ext->_getElementCountProperty());
|
|
}
|
|
|
|
// applyColors();
|
|
signalChangeIcon();
|
|
}
|
|
}
|
|
else if (prop == ext->getLinkTransformProperty()) {
|
|
setLinkType(ext);
|
|
applyColors();
|
|
}
|
|
else if (prop == ext->_getElementCountProperty()) {
|
|
if (!ext->_getShowElementValue()) {
|
|
linkView->setSize(ext->_getElementCountValue());
|
|
updateDataPrivate(ext, ext->getVisibilityListProperty());
|
|
updateDataPrivate(ext, ext->getPlacementListProperty());
|
|
}
|
|
}
|
|
else if (prop == ext->_getShowElementProperty()) {
|
|
if (!ext->_getShowElementValue()) {
|
|
|
|
auto linked = freecad_cast<ViewProviderDocumentObject*>(getLinkedView(true, ext));
|
|
if (linked && linked->getDocument() == getDocument()) {
|
|
linked->hide();
|
|
}
|
|
|
|
const auto& elements = ext->_getElementListValue();
|
|
// elements is about to be collapsed, preserve the materials
|
|
if (!elements.empty()) {
|
|
std::vector<App::Material> materials;
|
|
boost::dynamic_bitset<> overrideMaterials;
|
|
overrideMaterials.resize(elements.size(), false);
|
|
bool overrideMaterial = false;
|
|
bool hasMaterial = false;
|
|
materials.reserve(elements.size());
|
|
for (size_t i = 0; i < elements.size(); ++i) {
|
|
auto element = freecad_cast<App::LinkElement*>(elements[i]);
|
|
if (!element) {
|
|
continue;
|
|
}
|
|
auto vp = freecad_cast<ViewProviderLink*>(
|
|
Application::Instance->getViewProvider(element)
|
|
);
|
|
if (!vp) {
|
|
continue;
|
|
}
|
|
overrideMaterial = overrideMaterial || vp->OverrideMaterial.getValue();
|
|
hasMaterial = overrideMaterial || hasMaterial
|
|
|| vp->ShapeMaterial.getValue() != ShapeMaterial.getValue();
|
|
materials.push_back(vp->ShapeMaterial.getValue());
|
|
overrideMaterials[i] = vp->OverrideMaterial.getValue();
|
|
}
|
|
if (!overrideMaterial) {
|
|
overrideMaterials.clear();
|
|
}
|
|
OverrideMaterialList.setStatus(App::Property::User3, true);
|
|
OverrideMaterialList.setValue(overrideMaterials);
|
|
OverrideMaterialList.setStatus(App::Property::User3, false);
|
|
if (!hasMaterial) {
|
|
materials.clear();
|
|
}
|
|
MaterialList.setStatus(App::Property::User3, true);
|
|
MaterialList.setValue(materials);
|
|
MaterialList.setStatus(App::Property::User3, false);
|
|
|
|
linkView->setSize(ext->_getElementCountValue());
|
|
updateDataPrivate(ext, ext->getVisibilityListProperty());
|
|
applyMaterial();
|
|
applyColors();
|
|
}
|
|
}
|
|
}
|
|
else if (prop == ext->getScaleListProperty() || prop == ext->getPlacementListProperty()) {
|
|
if (!prop->testStatus(App::Property::User3) && linkView->getSize()
|
|
&& !ext->_getShowElementValue()) {
|
|
auto propPlacements = ext->getPlacementListProperty();
|
|
auto propScales = ext->getScaleListProperty();
|
|
if (propPlacements && linkView->getSize()) {
|
|
const auto& touched = prop == propScales ? propScales->getTouchList()
|
|
: propPlacements->getTouchList();
|
|
if (touched.empty()) {
|
|
for (int i = 0; i < linkView->getSize(); ++i) {
|
|
Base::Matrix4D mat;
|
|
if (propPlacements && propPlacements->getSize() > i) {
|
|
mat = (*propPlacements)[i].toMatrix();
|
|
}
|
|
if (propScales && propScales->getSize() > i && canScale((*propScales)[i])) {
|
|
Base::Matrix4D s;
|
|
s.scale((*propScales)[i]);
|
|
mat *= s;
|
|
}
|
|
linkView->setTransform(i, mat);
|
|
}
|
|
}
|
|
else {
|
|
for (int i : touched) {
|
|
if (i < 0 || i >= linkView->getSize()) {
|
|
continue;
|
|
}
|
|
Base::Matrix4D mat;
|
|
if (propPlacements && propPlacements->getSize() > i) {
|
|
mat = (*propPlacements)[i].toMatrix();
|
|
}
|
|
if (propScales && propScales->getSize() > i && canScale((*propScales)[i])) {
|
|
Base::Matrix4D s;
|
|
s.scale((*propScales)[i]);
|
|
mat *= s;
|
|
}
|
|
linkView->setTransform(i, mat);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (prop == ext->getVisibilityListProperty()) {
|
|
const auto& vis = ext->getVisibilityListValue();
|
|
for (size_t i = 0; i < (size_t)linkView->getSize(); ++i) {
|
|
if (vis.size() > i) {
|
|
linkView->setElementVisible(i, vis[i]);
|
|
}
|
|
else {
|
|
linkView->setElementVisible(i, true);
|
|
}
|
|
}
|
|
}
|
|
else if (prop == ext->_getElementListProperty()) {
|
|
if (ext->_getShowElementValue()) {
|
|
updateElementList(ext);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ViewProviderLink::updateElementList(App::LinkBaseExtension* ext)
|
|
{
|
|
const auto& elements = ext->_getElementListValue();
|
|
if (OverrideMaterialList.getSize() || MaterialList.getSize()) {
|
|
int i = -1;
|
|
for (auto obj : elements) {
|
|
++i;
|
|
auto vp = freecad_cast<ViewProviderLink*>(Application::Instance->getViewProvider(obj));
|
|
if (!vp) {
|
|
continue;
|
|
}
|
|
if (OverrideMaterialList.getSize() > i) {
|
|
vp->OverrideMaterial.setValue(OverrideMaterialList[i]);
|
|
}
|
|
if (MaterialList.getSize() > i) {
|
|
vp->ShapeMaterial.setValue(MaterialList[i]);
|
|
}
|
|
}
|
|
OverrideMaterialList.setSize(0);
|
|
MaterialList.setSize(0);
|
|
}
|
|
linkView->setChildren(elements, ext->getVisibilityListValue());
|
|
applyColors();
|
|
}
|
|
|
|
void ViewProviderLink::checkIcon(const App::LinkBaseExtension* ext)
|
|
{
|
|
if (!ext) {
|
|
ext = getLinkExtension();
|
|
if (!ext) {
|
|
return;
|
|
}
|
|
}
|
|
const char* icon;
|
|
auto element = getObject<App::LinkElement>();
|
|
if (element) {
|
|
icon = _LinkElementIcon;
|
|
}
|
|
else if (!ext->getLinkedObjectProperty() && ext->getElementListProperty()) {
|
|
icon = _LinkGroupIcon;
|
|
}
|
|
// else if(ext->_getElementCountValue())
|
|
// icon = _LinkArrayIcon;
|
|
else {
|
|
icon = _LinkIcon;
|
|
}
|
|
qint64 cacheKey = 0;
|
|
if (getObject()->getLinkedObject(false) != getObject()) {
|
|
cacheKey = getOverlayPixmap().cacheKey();
|
|
}
|
|
if (icon != sPixmap || cacheKey != overlayCacheKey) {
|
|
sPixmap = icon;
|
|
signalChangeIcon();
|
|
}
|
|
}
|
|
|
|
void ViewProviderLink::applyMaterial()
|
|
{
|
|
if (OverrideMaterial.getValue()) {
|
|
// Dispatch a generic Coin3D action to the linked object's scene graph.
|
|
// If the linked object is a Part shape, its SoBrepFaceSet node will
|
|
// handle this action and apply the color to all its faces.
|
|
// This is decoupled and respects the core/module architecture.
|
|
|
|
// 1. Get the material from our property.
|
|
const auto& material = ShapeMaterial.getValue();
|
|
|
|
// 2. Prepare the color for the rendering action. The action's ultimate
|
|
// consumer (SoBrepFaceSet) expects a Base::Color where .a is transparency,
|
|
// so we must perform the conversion here at the boundary.
|
|
Base::Color renderColor = material.diffuseColor;
|
|
renderColor.a = 1.0f - material.transparency;
|
|
|
|
// 3. Create a map with the "Face" wildcard to signify "all faces".
|
|
std::map<std::string, Base::Color> colorMap;
|
|
colorMap["Face"] = renderColor;
|
|
|
|
// 4. Create and dispatch the action. We use a secondary context action,
|
|
// which is the established mechanism for this kind of override.
|
|
SoSelectionElementAction action(
|
|
SoSelectionElementAction::Color,
|
|
true
|
|
); // true for secondary
|
|
action.swapColors(colorMap);
|
|
linkView->getLinkRoot()->doAction(&action);
|
|
|
|
// 5. Ensure the old global override mechanism is not used.
|
|
linkView->setMaterial(-1, nullptr);
|
|
}
|
|
else {
|
|
// OVERRIDE IS DISABLED:
|
|
// We must clear the per-face override we just applied.
|
|
|
|
// 1. Dispatch an empty Color action to clear the secondary context.
|
|
SoSelectionElementAction action(SoSelectionElementAction::Color, true);
|
|
linkView->getLinkRoot()->doAction(&action);
|
|
|
|
// 2. Re-apply any other material settings (e.g., for array elements,
|
|
// or clear the old global override if it was set).
|
|
linkView->setMaterial(-1, nullptr);
|
|
for (int i = 0; i < linkView->getSize(); ++i) {
|
|
if (MaterialList.getSize() > i && OverrideMaterialList.getSize() > i
|
|
&& OverrideMaterialList[i]) {
|
|
linkView->setMaterial(i, &MaterialList[i]);
|
|
}
|
|
else {
|
|
linkView->setMaterial(i, nullptr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ViewProviderLink::finishRestoring()
|
|
{
|
|
FC_TRACE("finish restoring");
|
|
auto ext = getLinkExtension();
|
|
if (!ext) {
|
|
return;
|
|
}
|
|
linkView->setDrawStyle(DrawStyle.getValue(), LineWidth.getValue(), PointSize.getValue());
|
|
updateDataPrivate(ext, ext->getLinkedObjectProperty());
|
|
if (ext->getLinkPlacementProperty()) {
|
|
updateDataPrivate(ext, ext->getLinkPlacementProperty());
|
|
}
|
|
else {
|
|
updateDataPrivate(ext, ext->getPlacementProperty());
|
|
}
|
|
updateDataPrivate(ext, ext->_getElementCountProperty());
|
|
if (ext->getPlacementListProperty()) {
|
|
updateDataPrivate(ext, ext->getPlacementListProperty());
|
|
}
|
|
else {
|
|
updateDataPrivate(ext, ext->getScaleListProperty());
|
|
}
|
|
updateDataPrivate(ext, ext->_getElementListProperty());
|
|
applyMaterial();
|
|
applyColors();
|
|
|
|
// TODO: notify the tree. This is ugly, any other way?
|
|
getDocument()->signalChangedObject(*this, ext->_LinkTouched);
|
|
|
|
if (childVp) {
|
|
childVp->finishRestoring();
|
|
}
|
|
}
|
|
|
|
bool ViewProviderLink::hasElements(const App::LinkBaseExtension* ext) const
|
|
{
|
|
if (!ext) {
|
|
ext = getLinkExtension();
|
|
if (!ext) {
|
|
return false;
|
|
}
|
|
}
|
|
const auto& elements = ext->getElementListValue();
|
|
return !elements.empty() && (int)elements.size() == ext->_getElementCountValue();
|
|
}
|
|
|
|
bool ViewProviderLink::isGroup(const App::LinkBaseExtension* ext, bool plainGroup) const
|
|
{
|
|
if (!ext) {
|
|
ext = getLinkExtension();
|
|
if (!ext) {
|
|
return false;
|
|
}
|
|
}
|
|
return (plainGroup && ext->linkedPlainGroup())
|
|
|| (ext->getElementListProperty() && !ext->getLinkedObjectProperty());
|
|
}
|
|
|
|
ViewProvider* ViewProviderLink::getLinkedView(bool real, const App::LinkBaseExtension* ext) const
|
|
{
|
|
if (!ext) {
|
|
ext = getLinkExtension();
|
|
}
|
|
auto obj = ext && real ? ext->getTrueLinkedObject(true) : getObject()->getLinkedObject(true);
|
|
if (obj && obj != getObject()) {
|
|
return Application::Instance->getViewProvider(obj);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::vector<App::DocumentObject*> ViewProviderLink::claimChildren() const
|
|
{
|
|
auto ext = getLinkExtension();
|
|
std::vector<App::DocumentObject*> ret;
|
|
|
|
if (ext && !ext->_getShowElementValue() && ext->_getElementCountValue()) {
|
|
// in array mode without element objects, we'd better not show the
|
|
// linked object's children to avoid inconsistent behavior on selection.
|
|
// We claim the linked object instead
|
|
if (ext) {
|
|
auto obj = ext->getLinkedObjectValue();
|
|
if (obj) {
|
|
ret.push_back(obj);
|
|
}
|
|
}
|
|
}
|
|
else if (hasElements(ext) || isGroup(ext)) {
|
|
ret = ext->getElementListValue();
|
|
if (ext->_getElementCountValue() && ext->getLinkClaimChildValue()
|
|
&& ext->getLinkedObjectValue()) {
|
|
ret.insert(ret.begin(), ext->getLinkedObjectValue());
|
|
}
|
|
}
|
|
else if (!hasSubName) {
|
|
auto linked = getLinkedView(true);
|
|
if (linked) {
|
|
ret = linked->claimChildren();
|
|
if (ext->getLinkClaimChildValue() && ext->getLinkedObjectValue()) {
|
|
ret.insert(ret.begin(), ext->getLinkedObjectValue());
|
|
}
|
|
}
|
|
}
|
|
if (ext && ext->getLinkCopyOnChangeGroupValue()) {
|
|
ret.insert(ret.begin(), ext->getLinkCopyOnChangeGroupValue());
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
bool ViewProviderLink::canDragObject(App::DocumentObject* obj) const
|
|
{
|
|
auto ext = getLinkExtension();
|
|
if (isGroup(ext)) {
|
|
return true;
|
|
}
|
|
if (hasElements(ext)) {
|
|
return false;
|
|
}
|
|
auto linked = getLinkedView(false, ext);
|
|
if (linked) {
|
|
return linked->canDragObject(obj);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ViewProviderLink::canDragObjects() const
|
|
{
|
|
auto ext = getLinkExtension();
|
|
if (isGroup(ext)) {
|
|
return true;
|
|
}
|
|
if (hasElements(ext)) {
|
|
return false;
|
|
}
|
|
auto linked = getLinkedView(false, ext);
|
|
if (linked) {
|
|
return linked->canDragObjects();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ViewProviderLink::dragObject(App::DocumentObject* obj)
|
|
{
|
|
auto ext = getLinkExtension();
|
|
if (isGroup(ext)) {
|
|
const auto& objs = ext->getElementListValue();
|
|
for (size_t i = 0; i < objs.size(); ++i) {
|
|
if (obj == objs[i]) {
|
|
ext->setLink(i, nullptr);
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (hasElements(ext)) {
|
|
return;
|
|
}
|
|
auto linked = getLinkedView(false);
|
|
if (linked) {
|
|
linked->dragObject(obj);
|
|
}
|
|
}
|
|
|
|
bool ViewProviderLink::canDropObjects() const
|
|
{
|
|
auto ext = getLinkExtension();
|
|
if (isGroup(ext)) {
|
|
return true;
|
|
}
|
|
if (hasElements(ext)) {
|
|
return false;
|
|
}
|
|
if (hasSubElement) {
|
|
return true;
|
|
}
|
|
else if (hasSubName) {
|
|
return false;
|
|
}
|
|
auto linked = getLinkedView(false, ext);
|
|
if (linked) {
|
|
return linked->canDropObjects();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ViewProviderLink::canDropObjectEx(
|
|
App::DocumentObject* obj,
|
|
App::DocumentObject* owner,
|
|
const char* subname,
|
|
const std::vector<std::string>& subElements
|
|
) const
|
|
{
|
|
if (pcObject == obj || pcObject == owner) {
|
|
return false;
|
|
}
|
|
auto ext = getLinkExtension();
|
|
if (isGroup(ext)) {
|
|
return true;
|
|
}
|
|
if (!ext || !ext->getLinkedObjectProperty() || hasElements(ext)) {
|
|
return false;
|
|
}
|
|
if (!hasSubName && linkView->isLinked()) {
|
|
auto linked = getLinkedView(false, ext);
|
|
if (linked) {
|
|
auto linkedVdp = freecad_cast<ViewProviderDocumentObject*>(linked);
|
|
if (linkedVdp) {
|
|
if (linkedVdp->getObject() == obj || linkedVdp->getObject() == owner) {
|
|
return false;
|
|
}
|
|
}
|
|
return linked->canDropObjectEx(obj, owner, subname, subElements);
|
|
}
|
|
}
|
|
if (obj->getDocument() != getObject()->getDocument()
|
|
&& !freecad_cast<App::PropertyXLink*>(ext->getLinkedObjectProperty())) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string ViewProviderLink::dropObjectEx(
|
|
App::DocumentObject* obj,
|
|
App::DocumentObject* owner,
|
|
const char* subname,
|
|
const std::vector<std::string>& subElements
|
|
)
|
|
{
|
|
auto ext = getLinkExtension();
|
|
if (!ext) {
|
|
return std::string();
|
|
}
|
|
|
|
if (isGroup(ext)) {
|
|
size_t size = ext->getElementListValue().size();
|
|
ext->setLink(size, obj);
|
|
return std::to_string(size) + ".";
|
|
}
|
|
|
|
if (!ext->getLinkedObjectProperty() || hasElements(ext)) {
|
|
return std::string();
|
|
}
|
|
|
|
if (!hasSubName) {
|
|
auto linked = getLinkedView(false, ext);
|
|
if (linked) {
|
|
return linked->dropObjectEx(obj, owner, subname, subElements);
|
|
}
|
|
}
|
|
if (owner) {
|
|
if (!ext->getSubElements().empty()) {
|
|
ext->setLink(-1, owner, subname, subElements);
|
|
}
|
|
else {
|
|
ext->setLink(-1, owner, subname);
|
|
}
|
|
}
|
|
else if (!ext->getSubElements().empty()) {
|
|
ext->setLink(-1, obj, nullptr, subElements);
|
|
}
|
|
else {
|
|
ext->setLink(-1, obj, nullptr);
|
|
}
|
|
return std::string();
|
|
}
|
|
|
|
bool ViewProviderLink::canDragAndDropObject(App::DocumentObject* obj) const
|
|
{
|
|
auto ext = getLinkExtension();
|
|
if (!ext) {
|
|
return true;
|
|
}
|
|
if (isGroup(ext)) {
|
|
return ext->getLinkModeValue() < App::LinkBaseExtension::LinkModeAutoLink
|
|
&& obj->getDocument() == getObject()->getDocument();
|
|
}
|
|
if (!ext->getLinkedObjectProperty() || hasElements(ext)) {
|
|
return false;
|
|
}
|
|
if (!hasSubName) {
|
|
auto linked = getLinkedView(false, ext);
|
|
if (linked) {
|
|
return linked->canDragAndDropObject(obj);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ViewProviderLink::getElementPicked(const SoPickedPoint* pp, std::string& subname) const
|
|
{
|
|
if (!isSelectable()) {
|
|
return false;
|
|
}
|
|
auto ext = getLinkExtension();
|
|
if (!ext) {
|
|
return false;
|
|
}
|
|
if (childVpLink && childVp) {
|
|
auto path = pp->getPath();
|
|
int idx = path->findNode(childVpLink->getSnapshot(LinkView::SnapshotTransform));
|
|
if (idx >= 0) {
|
|
return childVp->getElementPicked(pp, subname);
|
|
}
|
|
}
|
|
bool ret = linkView->linkGetElementPicked(pp, subname);
|
|
if (!ret) {
|
|
return ret;
|
|
}
|
|
if (isGroup(ext, true)) {
|
|
const char* sub = nullptr;
|
|
int idx = App::LinkBaseExtension::getArrayIndex(subname.c_str(), &sub);
|
|
if (idx >= 0) {
|
|
--sub;
|
|
assert(*sub == '.');
|
|
const auto& elements = ext->_getElementListValue();
|
|
subname.replace(0, sub - subname.c_str(), elements[idx]->getNameInDocument());
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
bool ViewProviderLink::getDetailPath(const char* subname, SoFullPath* pPath, bool append, SoDetail*& det) const
|
|
{
|
|
auto ext = getLinkExtension();
|
|
if (!ext) {
|
|
return false;
|
|
}
|
|
|
|
auto len = pPath->getLength();
|
|
if (append) {
|
|
appendPath(pPath, pcRoot);
|
|
appendPath(pPath, pcModeSwitch);
|
|
}
|
|
if (childVpLink && getDefaultMode() == 1) {
|
|
if (childVpLink->getDetail(false, LinkView::SnapshotTransform, subname, det, pPath)) {
|
|
return true;
|
|
}
|
|
pPath->truncate(len);
|
|
return false;
|
|
}
|
|
std::string _subname;
|
|
if (!Base::Tools::isNullOrEmpty(subname)) {
|
|
if (auto linked = ext->getLinkedObjectValue()) {
|
|
if (const char* dot = strchr(subname, '.')) {
|
|
if (subname[0] == '$') {
|
|
CharRange sub(subname + 1, dot);
|
|
if (!boost::equals(sub, linked->Label.getValue())) {
|
|
dot = nullptr;
|
|
}
|
|
}
|
|
else {
|
|
CharRange sub(subname, dot);
|
|
if (!boost::equals(sub, linked->getNameInDocument())) {
|
|
dot = nullptr;
|
|
}
|
|
}
|
|
if (dot && linked->getSubObject(dot + 1)) {
|
|
subname = dot + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isGroup(ext, true) || hasElements(ext) || ext->getElementCountValue()) {
|
|
int index = ext->getElementIndex(subname, &subname);
|
|
if (index >= 0) {
|
|
_subname = std::to_string(index) + '.' + subname;
|
|
subname = _subname.c_str();
|
|
}
|
|
}
|
|
}
|
|
if (linkView->linkGetDetailPath(subname, pPath, det)) {
|
|
return true;
|
|
}
|
|
pPath->truncate(len);
|
|
return false;
|
|
}
|
|
|
|
bool ViewProviderLink::onDelete(const std::vector<std::string>&)
|
|
{
|
|
auto element = getObject<App::LinkElement>();
|
|
if (element && !element->canDelete()) {
|
|
return false;
|
|
}
|
|
|
|
auto link = getObject<App::Link>();
|
|
if (link && link->ElementCount.getValue() != 0) {
|
|
auto doc = link->getDocument();
|
|
auto elements = link->ElementList.getValues();
|
|
for (auto element : elements) {
|
|
doc->removeObject(element->getNameInDocument());
|
|
}
|
|
}
|
|
|
|
auto ext = getLinkExtension();
|
|
if (ext->isLinkMutated()) {
|
|
auto linked = ext->getLinkedObjectValue();
|
|
auto doc = ext->getContainer()->getDocument();
|
|
if (linked->getDocument() == doc) {
|
|
std::deque<std::string> objs;
|
|
for (auto obj : ext->getOnChangeCopyObjects(nullptr, linked)) {
|
|
if (obj->getDocument() == doc) {
|
|
// getOnChangeCopyObjects() returns object in depending
|
|
// order. So we delete it in reverse to avoid error
|
|
// reported by some parent object failing to find child
|
|
objs.emplace_front(obj->getNameInDocument());
|
|
}
|
|
}
|
|
for (const auto& name : objs) {
|
|
doc->removeObject(name.c_str());
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ViewProviderLink::canDelete(App::DocumentObject* obj) const
|
|
{
|
|
auto ext = getLinkExtension();
|
|
if (isGroup(ext) || hasElements(ext) || hasSubElement) {
|
|
return true;
|
|
}
|
|
auto linked = getLinkedView(false, ext);
|
|
if (linked) {
|
|
return linked->canDelete(obj);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ViewProviderLink::linkEdit(const App::LinkBaseExtension* ext) const
|
|
{
|
|
if (!ext) {
|
|
ext = getLinkExtension();
|
|
}
|
|
if (!ext || (!ext->_getShowElementValue() && ext->_getElementCountValue()) || hasElements(ext)
|
|
|| isGroup(ext) || hasSubName) {
|
|
return false;
|
|
}
|
|
return linkView->isLinked();
|
|
}
|
|
|
|
bool ViewProviderLink::doubleClicked()
|
|
{
|
|
if (linkEdit()) {
|
|
return linkView->getLinkedView()->doubleClicked();
|
|
}
|
|
return getDocument()->setEdit(this, ViewProvider::Transform);
|
|
}
|
|
|
|
void ViewProviderLink::setupContextMenu(QMenu* menu, QObject* receiver, const char* member)
|
|
{
|
|
auto ext = getLinkExtension();
|
|
if (!ext) {
|
|
return;
|
|
}
|
|
|
|
_setupContextMenu(ext, menu, receiver, member);
|
|
Gui::ActionFunction* func = nullptr;
|
|
|
|
if (ext->isLinkedToConfigurableObject()) {
|
|
auto src = ext->getLinkCopyOnChangeSourceValue();
|
|
if (!src) {
|
|
src = ext->getLinkedObjectValue();
|
|
}
|
|
if (src && !ext->getOnChangeCopyObjects(nullptr, src).empty()) {
|
|
QAction* act = menu->addAction(QObject::tr("Setup Configurable Object"));
|
|
act->setToolTip(
|
|
QObject::tr(
|
|
"Selects which object to copy or exclude when configuration changes. "
|
|
"All external linked objects are excluded by default."
|
|
)
|
|
);
|
|
act->setData(-1);
|
|
if (!func) {
|
|
func = new Gui::ActionFunction(menu);
|
|
}
|
|
func->trigger(act, [ext]() {
|
|
try {
|
|
std::vector<App::DocumentObject*> excludes;
|
|
auto src = ext->getLinkCopyOnChangeSourceValue();
|
|
if (!src) {
|
|
src = ext->getLinkedObjectValue();
|
|
}
|
|
auto objs = ext->getOnChangeCopyObjects(&excludes, src);
|
|
if (objs.empty()) {
|
|
return;
|
|
}
|
|
DlgObjectSelection dlg({src}, excludes, getMainWindow());
|
|
dlg.setMessage(
|
|
QObject::tr("Select which objects to copy when the configuration is changed")
|
|
);
|
|
auto box = new QCheckBox(QObject::tr("Apply to all"), &dlg);
|
|
box->setToolTip(QObject::tr("Applies the setting to all links"));
|
|
box->setChecked(App::LinkParams::getCopyOnChangeApplyToAll());
|
|
dlg.addCheckBox(box);
|
|
if (dlg.exec() != QDialog::Accepted) {
|
|
return;
|
|
}
|
|
|
|
bool applyAll = box->isChecked();
|
|
App::LinkParams::setCopyOnChangeApplyToAll(applyAll);
|
|
|
|
App::Link::OnChangeCopyOptions options {App::Link::OnChangeCopyOptions::None};
|
|
if (applyAll) {
|
|
options |= App::Link::OnChangeCopyOptions::ApplyAll;
|
|
}
|
|
|
|
App::AutoTransaction guard("Setup configurable object");
|
|
auto sels = dlg.getSelections(DlgObjectSelection::SelectionOptions::InvertSort);
|
|
for (const auto& exclude : excludes) {
|
|
auto iter = std::lower_bound(sels.begin(), sels.end(), exclude);
|
|
if (iter == sels.end() || *iter != exclude) {
|
|
ext->setOnChangeCopyObject(exclude, options);
|
|
}
|
|
else {
|
|
sels.erase(iter);
|
|
}
|
|
}
|
|
options |= App::Link::OnChangeCopyOptions::Exclude;
|
|
for (auto obj : sels) {
|
|
ext->setOnChangeCopyObject(obj, options);
|
|
}
|
|
if (!applyAll) {
|
|
ext->monitorOnChangeCopyObjects(ext->getOnChangeCopyObjects());
|
|
}
|
|
else {
|
|
std::set<App::LinkBaseExtension*> exts;
|
|
for (auto o : App::Document::getDependencyList(objs)) {
|
|
if (auto ext = o->getExtensionByType<App::LinkBaseExtension>(true)) {
|
|
exts.insert(ext);
|
|
}
|
|
}
|
|
for (auto ext : exts) {
|
|
ext->monitorOnChangeCopyObjects(ext->getOnChangeCopyObjects());
|
|
}
|
|
}
|
|
Command::updateActive();
|
|
}
|
|
catch (Base::Exception& e) {
|
|
e.reportException();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (ext->getLinkCopyOnChangeValue() == 0) {
|
|
auto submenu = menu->addMenu(QObject::tr("Copy on Change"));
|
|
auto act = submenu->addAction(QObject::tr("Enable"));
|
|
act->setToolTip(
|
|
QObject::tr("Enable auto copy of linked object when its configuration is changed")
|
|
);
|
|
act->setData(-1);
|
|
if (!func) {
|
|
func = new Gui::ActionFunction(menu);
|
|
}
|
|
func->trigger(act, [ext]() {
|
|
try {
|
|
App::AutoTransaction guard("Enable Link copy on change");
|
|
ext->getLinkCopyOnChangeProperty()->setValue(1);
|
|
Command::updateActive();
|
|
}
|
|
catch (Base::Exception& e) {
|
|
e.reportException();
|
|
}
|
|
});
|
|
act = submenu->addAction(QObject::tr("Tracking"));
|
|
act->setToolTip(
|
|
QObject::tr(
|
|
"Copies the linked object when its configuration is changed.\n"
|
|
"Also auto redo the copy if the original linked object is changed.\n"
|
|
)
|
|
);
|
|
act->setData(-1);
|
|
func->trigger(act, [ext]() {
|
|
try {
|
|
App::AutoTransaction guard("Enable Link tracking");
|
|
ext->getLinkCopyOnChangeProperty()->setValue(3);
|
|
Command::updateActive();
|
|
}
|
|
catch (Base::Exception& e) {
|
|
e.reportException();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (ext->getLinkCopyOnChangeValue() != 2 && ext->getLinkCopyOnChangeValue() != 0) {
|
|
QAction* act = menu->addAction(QObject::tr("Disable Copy on Change"));
|
|
act->setData(-1);
|
|
if (!func) {
|
|
func = new Gui::ActionFunction(menu);
|
|
}
|
|
func->trigger(act, [ext]() {
|
|
try {
|
|
App::AutoTransaction guard("Disable copy on change");
|
|
ext->getLinkCopyOnChangeProperty()->setValue((long)0);
|
|
Command::updateActive();
|
|
}
|
|
catch (Base::Exception& e) {
|
|
e.reportException();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (ext->isLinkMutated()) {
|
|
QAction* act = menu->addAction(QObject::tr("Refresh Configurable Object"));
|
|
act->setToolTip(
|
|
QObject::tr(
|
|
"Synchronizes the original configurable source object by\n"
|
|
"creating a new deep copy. Any changes made to\n"
|
|
"the current copy will be lost.\n"
|
|
)
|
|
);
|
|
act->setData(-1);
|
|
if (!func) {
|
|
func = new Gui::ActionFunction(menu);
|
|
}
|
|
func->trigger(act, [ext]() {
|
|
try {
|
|
App::AutoTransaction guard("Link refresh");
|
|
ext->syncCopyOnChange();
|
|
Command::updateActive();
|
|
}
|
|
catch (Base::Exception& e) {
|
|
e.reportException();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void ViewProviderLink::_setupContextMenu(
|
|
App::LinkBaseExtension* ext,
|
|
QMenu* menu,
|
|
QObject* receiver,
|
|
const char* member
|
|
)
|
|
{
|
|
if (linkEdit(ext)) {
|
|
if (auto linkvp = freecad_cast<ViewProviderLink*>(linkView->getLinkedView())) {
|
|
linkvp->_setupContextMenu(ext, menu, receiver, member);
|
|
}
|
|
else {
|
|
linkView->getLinkedView()->setupContextMenu(menu, receiver, member);
|
|
}
|
|
}
|
|
|
|
if (ext->getLinkedObjectProperty() && ext->_getShowElementProperty()
|
|
&& ext->_getElementCountValue() > 1) {
|
|
auto action = menu->addAction(QObject::tr("Toggle Array Elements"), [ext] {
|
|
try {
|
|
App::AutoTransaction guard(QT_TRANSLATE_NOOP("Command", "Toggle array elements"));
|
|
ext->getShowElementProperty()->setValue(!ext->getShowElementValue());
|
|
Command::updateActive();
|
|
}
|
|
catch (Base::Exception& e) {
|
|
e.reportException();
|
|
}
|
|
});
|
|
action->setToolTip(
|
|
QObject::tr("Changes whether to show each link array element as individual objects")
|
|
);
|
|
}
|
|
|
|
if ((ext->getPlacementProperty() && !ext->getPlacementProperty()->isReadOnly())
|
|
|| (ext->getLinkPlacementProperty() && !ext->getLinkPlacementProperty()->isReadOnly())) {
|
|
bool found = false;
|
|
const auto actions = menu->actions();
|
|
for (auto action : actions) {
|
|
if (action->data().toInt() == ViewProvider::Transform) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
QIcon iconObject = mergeGreyableOverlayIcons(
|
|
Gui::BitmapFactory().pixmap("Std_TransformManip.svg")
|
|
);
|
|
QAction* act = menu->addAction(iconObject, QObject::tr("Transform"), receiver, member);
|
|
act->setToolTip(QObject::tr("Transforms the object at the origin of the placement"));
|
|
act->setData(QVariant((int)ViewProvider::Transform));
|
|
}
|
|
}
|
|
|
|
if (ext->getColoredElementsProperty()) {
|
|
bool found = false;
|
|
const auto actions = menu->actions();
|
|
for (auto action : actions) {
|
|
if (action->data().toInt() == ViewProvider::Color) {
|
|
action->setText(QObject::tr("Override Colors"));
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
QAction* act = menu->addAction(QObject::tr("Override Colors"), receiver, member);
|
|
act->setData(QVariant((int)ViewProvider::Color));
|
|
}
|
|
}
|
|
|
|
auto cmd = Application::Instance->commandManager().getCommandByName("Std_LinkSelectLinked");
|
|
menu->addAction(cmd->getAction()->action());
|
|
}
|
|
|
|
bool ViewProviderLink::initDraggingPlacement()
|
|
{
|
|
Base::PyGILStateLocker lock;
|
|
try {
|
|
auto* proxy = getPropertyByName("Proxy");
|
|
if (proxy && proxy->is<App::PropertyPythonObject>()) {
|
|
Py::Object feature = static_cast<App::PropertyPythonObject*>(proxy)->getValue();
|
|
const char* fname = "initDraggingPlacement";
|
|
if (feature.hasAttr(fname)) {
|
|
Py::Callable method(feature.getAttr(fname));
|
|
Py::Tuple arg;
|
|
Py::Object ret(method.apply(arg));
|
|
if (ret.isTuple()) {
|
|
PyObject *pymat, *pypla, *pybbox;
|
|
if (!PyArg_ParseTuple(
|
|
ret.ptr(),
|
|
"O!O!O!",
|
|
&Base::MatrixPy::Type,
|
|
&pymat,
|
|
&Base::PlacementPy::Type,
|
|
&pypla,
|
|
&Base::BoundBoxPy::Type,
|
|
&pybbox
|
|
)) {
|
|
FC_ERR("initDraggingPlacement() expects return of type tuple(matrix,placement,boundbox)");
|
|
return false;
|
|
}
|
|
dragCtx = std::make_unique<DraggerContext>();
|
|
dragCtx->initialPlacement
|
|
= *static_cast<Base::PlacementPy*>(pypla)->getPlacementPtr();
|
|
dragCtx->preTransform = *static_cast<Base::MatrixPy*>(pymat)->getMatrixPtr();
|
|
dragCtx->bbox = *static_cast<Base::BoundBoxPy*>(pybbox)->getBoundBoxPtr();
|
|
return true;
|
|
}
|
|
else if (!ret.isTrue()) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Py::Exception&) {
|
|
Base::PyException e;
|
|
e.reportException();
|
|
return false;
|
|
}
|
|
|
|
auto ext = getLinkExtension();
|
|
if (!ext) {
|
|
FC_ERR("no link extension");
|
|
return false;
|
|
}
|
|
if (!ext->hasPlacement()) {
|
|
FC_ERR("no placement");
|
|
return false;
|
|
}
|
|
auto doc = Application::Instance->editDocument();
|
|
if (!doc) {
|
|
FC_ERR("no editing document");
|
|
return false;
|
|
}
|
|
|
|
dragCtx = std::make_unique<DraggerContext>();
|
|
|
|
dragCtx->preTransform = doc->getEditingTransform();
|
|
const auto& pla = getObject()->getPlacementProperty()->getValue();
|
|
|
|
// Cancel out our own transformation from the editing transform, because
|
|
// the dragger is meant to change our transformation.
|
|
dragCtx->preTransform *= pla.inverse().toMatrix();
|
|
|
|
dragCtx->bbox = getBoundingBox(nullptr, false);
|
|
// The returned bounding box is before our own transform, but we still need
|
|
// to scale it to get the correct center.
|
|
auto scale = ext->getScaleVector();
|
|
dragCtx->bbox.ScaleX(scale.x);
|
|
dragCtx->bbox.ScaleY(scale.y);
|
|
dragCtx->bbox.ScaleZ(scale.z);
|
|
|
|
App::PropertyPlacement* propPla = nullptr;
|
|
if (ext->getLinkTransformValue() && ext->getLinkedObjectValue()) {
|
|
propPla = freecad_cast<App::PropertyPlacement*>(
|
|
ext->getLinkedObjectValue()->getPropertyByName("Placement")
|
|
);
|
|
}
|
|
if (propPla) {
|
|
dragCtx->initialPlacement = pla * propPla->getValue();
|
|
dragCtx->mat *= propPla->getValue().inverse().toMatrix();
|
|
}
|
|
else {
|
|
dragCtx->initialPlacement = pla;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
ViewProvider* ViewProviderLink::startEditing(int mode)
|
|
{
|
|
if (mode == ViewProvider::Color) {
|
|
auto ext = getLinkExtension();
|
|
if (!ext || !ext->getColoredElementsProperty()) {
|
|
if (linkEdit(ext)) {
|
|
return linkView->getLinkedView()->startEditing(mode);
|
|
}
|
|
}
|
|
return inherited::startEditing(mode);
|
|
}
|
|
|
|
static thread_local bool _pendingTransform;
|
|
static thread_local Matrix4D _editingTransform;
|
|
|
|
auto doc = Application::Instance->editDocument();
|
|
|
|
if (mode == ViewProvider::Transform) {
|
|
if (_pendingTransform && doc) {
|
|
doc->setEditingTransform(_editingTransform);
|
|
}
|
|
|
|
if (!initDraggingPlacement()) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (auto result = inherited::startEditing(mode)) {
|
|
if (transformDragger.get()) {
|
|
transformDragger->addStartCallback(dragStartCallback, this);
|
|
transformDragger->addFinishCallback(dragFinishCallback, this);
|
|
transformDragger->addMotionCallback(dragMotionCallback, this);
|
|
|
|
setDraggerPlacement(dragCtx->initialPlacement);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
if (!linkEdit()) {
|
|
FC_ERR("unsupported edit mode " << mode);
|
|
return nullptr;
|
|
}
|
|
|
|
// TODO: the 0x8000 mask here is for caller to disambiguate the intention
|
|
// here, whether they want to, say transform the link itself or the linked
|
|
// object. Use of a mask here will allow forwarding those editing modes that
|
|
// are supported by both the link and the linked object, such as transform
|
|
// and set color. We need to find a better place to declare this constant.
|
|
mode &= ~0x8000;
|
|
|
|
if (!doc) {
|
|
FC_ERR("no editing document");
|
|
return nullptr;
|
|
}
|
|
|
|
// We are forwarding the editing request to linked object. We need to
|
|
// adjust the editing transformation.
|
|
Base::Matrix4D mat;
|
|
auto linked = getObject()->getLinkedObject(true, &mat, false);
|
|
if (!linked || linked == getObject()) {
|
|
FC_ERR("no linked object");
|
|
return nullptr;
|
|
}
|
|
auto vpd = freecad_cast<ViewProviderDocumentObject*>(
|
|
Application::Instance->getViewProvider(linked)
|
|
);
|
|
if (!vpd) {
|
|
FC_ERR("no linked viewprovider");
|
|
return nullptr;
|
|
}
|
|
// Amend the editing transformation with the link transformation.
|
|
// But save it first in case the linked object reroute the editing request
|
|
// back to us.
|
|
_editingTransform = doc->getEditingTransform();
|
|
doc->setEditingTransform(doc->getEditingTransform() * mat);
|
|
Base::FlagToggler<> guard(_pendingTransform);
|
|
return vpd->startEditing(mode);
|
|
}
|
|
|
|
bool ViewProviderLink::setEdit(int ModNum)
|
|
{
|
|
if (ModNum == ViewProvider::Color) {
|
|
auto ext = getLinkExtension();
|
|
if (!ext || !ext->getColoredElementsProperty()) {
|
|
return false;
|
|
}
|
|
TaskView::TaskDialog* dlg = Control().activeDialog();
|
|
if (dlg) {
|
|
Control().showDialog(dlg);
|
|
return false;
|
|
}
|
|
Selection().clearSelection();
|
|
return true;
|
|
}
|
|
|
|
return inherited::setEdit(ModNum);
|
|
}
|
|
|
|
void ViewProviderLink::setEditViewer(Gui::View3DInventorViewer* viewer, int ModNum)
|
|
{
|
|
if (ModNum == ViewProvider::Color) {
|
|
Gui::Control().showDialog(new TaskElementColors(this));
|
|
return;
|
|
}
|
|
|
|
ViewProviderDragger::setEditViewer(viewer, ModNum);
|
|
|
|
viewer->setupEditingRoot(transformDragger, &dragCtx->preTransform);
|
|
}
|
|
|
|
void ViewProviderLink::unsetEditViewer(Gui::View3DInventorViewer* viewer)
|
|
{
|
|
dragCtx.reset();
|
|
|
|
inherited::unsetEditViewer(viewer);
|
|
}
|
|
|
|
bool ViewProviderLink::callDraggerProxy(const char* fname)
|
|
{
|
|
if (!transformDragger) {
|
|
return false;
|
|
}
|
|
|
|
Base::PyGILStateLocker lock;
|
|
try {
|
|
auto* proxy = getPropertyByName("Proxy");
|
|
if (proxy && proxy->is<App::PropertyPythonObject>()) {
|
|
Py::Object feature = static_cast<App::PropertyPythonObject*>(proxy)->getValue();
|
|
if (feature.hasAttr(fname)) {
|
|
Py::Callable method(feature.getAttr(fname));
|
|
Py::Tuple args;
|
|
if (method.apply(args).isTrue()) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Py::Exception&) {
|
|
Base::PyException e;
|
|
e.reportException();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void ViewProviderLink::dragStartCallback(void* data, SoDragger*)
|
|
{
|
|
auto me = static_cast<ViewProviderLink*>(data);
|
|
|
|
me->dragCtx->initialPlacement = me->getDraggerPlacement();
|
|
me->callDraggerProxy("onDragStart");
|
|
}
|
|
|
|
void ViewProviderLink::dragFinishCallback(void* data, SoDragger*)
|
|
{
|
|
auto me = static_cast<ViewProviderLink*>(data);
|
|
me->callDraggerProxy("onDragEnd");
|
|
|
|
if (me->dragCtx->cmdPending) {
|
|
if (me->getDraggerPlacement() == me->dragCtx->initialPlacement) {
|
|
me->getDocument()->abortCommand();
|
|
}
|
|
else {
|
|
me->getDocument()->commitCommand();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ViewProviderLink::dragMotionCallback(void* data, SoDragger*)
|
|
{
|
|
auto me = static_cast<ViewProviderLink*>(data);
|
|
me->callDraggerProxy("onDragMotion");
|
|
}
|
|
|
|
void ViewProviderLink::updateLinks(ViewProvider* vp)
|
|
{
|
|
try {
|
|
auto ext = vp->getExtensionByType<ViewProviderLinkObserver>(true);
|
|
if (ext && ext->linkInfo) {
|
|
ext->linkInfo->update();
|
|
}
|
|
}
|
|
catch (const Base::TypeError& e) {
|
|
e.reportException();
|
|
}
|
|
catch (const Base::ValueError& e) {
|
|
e.reportException();
|
|
}
|
|
}
|
|
|
|
PyObject* ViewProviderLink::getPyObject()
|
|
{
|
|
if (!pyViewObject) {
|
|
pyViewObject = new ViewProviderLinkPy(this);
|
|
}
|
|
pyViewObject->IncRef();
|
|
return pyViewObject;
|
|
}
|
|
|
|
PyObject* ViewProviderLink::getPyLinkView()
|
|
{
|
|
return linkView->getPyObject();
|
|
}
|
|
|
|
std::map<std::string, Base::Color> ViewProviderLink::getElementColors(const char* subname) const
|
|
{
|
|
bool isPrefix = true;
|
|
if (!subname) {
|
|
subname = "";
|
|
}
|
|
else {
|
|
auto len = strlen(subname);
|
|
isPrefix = !len || subname[len - 1] == '.';
|
|
}
|
|
std::map<std::string, Base::Color> colors;
|
|
auto ext = getLinkExtension();
|
|
if (!ext || !ext->getColoredElementsProperty()) {
|
|
return colors;
|
|
}
|
|
const auto& subs = ext->getColoredElementsProperty()->getShadowSubs();
|
|
int size = OverrideColorList.getSize();
|
|
|
|
std::string wildcard(subname);
|
|
if (wildcard == "Face" || wildcard == "Face*" || wildcard.empty()) {
|
|
if (wildcard.size() == 4 || OverrideMaterial.getValue()) {
|
|
Base::Color c = ShapeMaterial.getValue().diffuseColor;
|
|
c.setTransparency(ShapeMaterial.getValue().transparency);
|
|
colors["Face"] = c;
|
|
if (wildcard.size() == 4) {
|
|
return colors;
|
|
}
|
|
}
|
|
if (!wildcard.empty()) {
|
|
wildcard.resize(4);
|
|
}
|
|
}
|
|
else if (wildcard == "Edge*") {
|
|
wildcard.resize(4);
|
|
}
|
|
else if (wildcard == "Vertex*") {
|
|
wildcard.resize(5);
|
|
}
|
|
else if (wildcard == ViewProvider::hiddenMarker() + "*") {
|
|
wildcard.resize(ViewProvider::hiddenMarker().size());
|
|
}
|
|
else {
|
|
wildcard.clear();
|
|
}
|
|
|
|
int i = -1;
|
|
if (!wildcard.empty()) {
|
|
for (const auto& sub : subs) {
|
|
if (++i >= size) {
|
|
break;
|
|
}
|
|
auto pos = sub.oldName.rfind('.');
|
|
if (pos == std::string::npos) {
|
|
pos = 0;
|
|
}
|
|
else {
|
|
++pos;
|
|
}
|
|
const char* element = sub.oldName.c_str() + pos;
|
|
if (boost::starts_with(element, wildcard)) {
|
|
colors[sub.oldName] = OverrideColorList[i];
|
|
}
|
|
else if (!element[0] && wildcard == "Face") {
|
|
colors[sub.oldName.substr(0, element - sub.oldName.c_str()) + wildcard]
|
|
= OverrideColorList[i];
|
|
}
|
|
}
|
|
|
|
// In case of multi-level linking, we recursively call into each level,
|
|
// and merge the colors
|
|
auto vp = this;
|
|
while (true) {
|
|
if (wildcard != ViewProvider::hiddenMarker() && vp->OverrideMaterial.getValue()) {
|
|
auto color = ShapeMaterial.getValue().diffuseColor;
|
|
color.setTransparency(ShapeMaterial.getValue().transparency);
|
|
colors.emplace(wildcard, color);
|
|
}
|
|
auto link = vp->getObject()->getLinkedObject(false);
|
|
if (!link || link == vp->getObject()) {
|
|
break;
|
|
}
|
|
auto next = freecad_cast<ViewProviderLink*>(Application::Instance->getViewProvider(link));
|
|
if (!next) {
|
|
break;
|
|
}
|
|
for (const auto& v : next->getElementColors(subname)) {
|
|
colors.insert(v);
|
|
}
|
|
vp = next;
|
|
}
|
|
if (wildcard != ViewProvider::hiddenMarker()) {
|
|
// Get collapsed array color override.
|
|
auto ext = vp->getLinkExtension();
|
|
if (ext->_getElementCountValue() && !ext->_getShowElementValue()) {
|
|
const auto& overrides = vp->OverrideMaterialList.getValues();
|
|
int i = -1;
|
|
for (const auto& mat : vp->MaterialList.getValues()) {
|
|
if (++i >= (int)overrides.size()) {
|
|
break;
|
|
}
|
|
if (!overrides[i]) {
|
|
continue;
|
|
}
|
|
auto color = mat.diffuseColor;
|
|
color.setTransparency(mat.transparency);
|
|
colors.emplace(std::to_string(i) + "." + wildcard, color);
|
|
}
|
|
}
|
|
}
|
|
return colors;
|
|
}
|
|
|
|
int element_count = ext->getElementCountValue();
|
|
|
|
for (const auto& sub : subs) {
|
|
if (++i >= size) {
|
|
break;
|
|
}
|
|
|
|
int offset = 0;
|
|
|
|
if (!sub.oldName.empty() && element_count && !std::isdigit(sub.oldName[0])) {
|
|
// For checking and expanding color override of array base
|
|
if (!subname[0]) {
|
|
std::ostringstream ss;
|
|
ss << "0." << sub.oldName;
|
|
if (getObject()->getSubObject(ss.str().c_str())) {
|
|
for (int j = 0; j < element_count; ++j) {
|
|
ss.str("");
|
|
ss << j << '.' << sub.oldName;
|
|
colors.emplace(ss.str(), OverrideColorList[i]);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
else if (std::isdigit(subname[0])) {
|
|
const char* dot = strchr(subname, '.');
|
|
if (dot) {
|
|
offset = dot - subname + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isPrefix) {
|
|
if (!boost::starts_with(sub.newName, subname + offset)
|
|
&& !boost::starts_with(sub.oldName, subname + offset)) {
|
|
continue;
|
|
}
|
|
}
|
|
else if (sub.newName != subname + offset && sub.oldName != subname + offset) {
|
|
continue;
|
|
}
|
|
|
|
if (offset) {
|
|
colors.emplace(std::string(subname, offset) + sub.oldName, OverrideColorList[i]);
|
|
}
|
|
else {
|
|
colors[sub.oldName] = OverrideColorList[i];
|
|
}
|
|
}
|
|
|
|
if (!subname[0]) {
|
|
return colors;
|
|
}
|
|
|
|
bool found = true;
|
|
if (colors.empty()) {
|
|
found = false;
|
|
colors.emplace(subname, Base::Color());
|
|
}
|
|
std::map<std::string, Base::Color> ret;
|
|
for (const auto& v : colors) {
|
|
const char* pos = nullptr;
|
|
auto sobj = getObject()->resolve(v.first.c_str(), nullptr, nullptr, &pos);
|
|
if (!sobj || !pos) {
|
|
continue;
|
|
}
|
|
auto link = sobj->getLinkedObject(true);
|
|
if (!link || link == getObject()) {
|
|
continue;
|
|
}
|
|
auto vp = Application::Instance->getViewProvider(sobj->getLinkedObject(true));
|
|
if (!vp) {
|
|
continue;
|
|
}
|
|
for (const auto& v2 : vp->getElementColors(!pos[0] ? "Face" : pos)) {
|
|
std::string name;
|
|
if (pos[0]) {
|
|
name = v.first.substr(0, pos - v.first.c_str()) + v2.first;
|
|
}
|
|
else {
|
|
name = v.first;
|
|
}
|
|
ret[name] = found ? v.second : v2.second;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void ViewProviderLink::setElementColors(const std::map<std::string, Base::Color>& colorMap)
|
|
{
|
|
auto ext = getLinkExtension();
|
|
if (!ext || !ext->getColoredElementsProperty()) {
|
|
return;
|
|
}
|
|
|
|
// For checking and collapsing array element color
|
|
std::map<std::string, std::map<int, Base::Color>> subMap;
|
|
int element_count = ext->getElementCountValue();
|
|
|
|
std::vector<std::string> subs;
|
|
std::vector<Base::Color> colors;
|
|
Base::Color faceColor;
|
|
bool hasFaceColor = false;
|
|
for (const auto& v : colorMap) {
|
|
if (!hasFaceColor && v.first == "Face") {
|
|
hasFaceColor = true;
|
|
faceColor = v.second;
|
|
continue;
|
|
}
|
|
|
|
if (element_count && !v.first.empty() && std::isdigit(v.first[0])) {
|
|
// In case of array, check if there are override of the same
|
|
// sub-element for every array element. And collapse those overrides
|
|
// into one without the index.
|
|
const char* dot = strchr(v.first.c_str(), '.');
|
|
if (dot) {
|
|
subMap[dot + 1][std::atoi(v.first.c_str())] = v.second;
|
|
continue;
|
|
}
|
|
}
|
|
subs.push_back(v.first);
|
|
colors.push_back(v.second);
|
|
}
|
|
for (auto& v : subMap) {
|
|
if (element_count == (int)v.second.size()) {
|
|
Base::Color firstColor = v.second.begin()->second;
|
|
subs.push_back(v.first);
|
|
colors.push_back(firstColor);
|
|
for (auto it = v.second.begin(); it != v.second.end();) {
|
|
if (it->second == firstColor) {
|
|
it = v.second.erase(it);
|
|
}
|
|
else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
std::ostringstream ss;
|
|
for (const auto& colorInfo : v.second) {
|
|
ss.str("");
|
|
ss << colorInfo.first << '.' << v.first;
|
|
subs.push_back(ss.str());
|
|
colors.push_back(colorInfo.second);
|
|
}
|
|
}
|
|
|
|
auto prop = ext->getColoredElementsProperty();
|
|
if (subs != prop->getSubValues() || colors != OverrideColorList.getValues()) {
|
|
prop->setStatus(App::Property::User3, true);
|
|
prop->setValue(getObject(), subs);
|
|
prop->setStatus(App::Property::User3, false);
|
|
OverrideColorList.setValues(colors);
|
|
}
|
|
if (hasFaceColor) {
|
|
auto mat = ShapeMaterial.getValue();
|
|
mat.diffuseColor = faceColor;
|
|
mat.transparency = faceColor.transparency();
|
|
ShapeMaterial.setStatus(App::Property::User3, true);
|
|
ShapeMaterial.setValue(mat);
|
|
ShapeMaterial.setStatus(App::Property::User3, false);
|
|
}
|
|
OverrideMaterial.setValue(hasFaceColor);
|
|
}
|
|
|
|
void ViewProviderLink::applyColors()
|
|
{
|
|
auto ext = getLinkExtension();
|
|
if (!ext || !ext->getColoredElementsProperty()) {
|
|
return;
|
|
}
|
|
|
|
SoSelectionElementAction action(SoSelectionElementAction::Color, true);
|
|
// reset color and visibility first
|
|
action.apply(linkView->getLinkRoot());
|
|
|
|
std::map<std::string, std::map<std::string, Base::Color>> colorMap;
|
|
std::set<std::string> hideList;
|
|
auto colors = getElementColors();
|
|
colors.erase("Face");
|
|
for (const auto& v : colors) {
|
|
const char* subname = v.first.c_str();
|
|
const char* element = nullptr;
|
|
auto sobj = getObject()->resolve(subname, nullptr, nullptr, &element);
|
|
if (!sobj || !element) {
|
|
continue;
|
|
}
|
|
if (ViewProvider::hiddenMarker() == element) {
|
|
hideList.emplace(subname, element - subname);
|
|
}
|
|
else {
|
|
colorMap[std::string(subname, element - subname)][element] = v.second;
|
|
}
|
|
}
|
|
|
|
SoTempPath path(10);
|
|
path.ref();
|
|
for (auto& v : colorMap) {
|
|
action.swapColors(v.second);
|
|
if (v.first.empty()) {
|
|
action.apply(linkView->getLinkRoot());
|
|
continue;
|
|
}
|
|
SoDetail* det = nullptr;
|
|
path.truncate(0);
|
|
if (getDetailPath(v.first.c_str(), &path, false, det)) {
|
|
action.apply(&path);
|
|
}
|
|
delete det;
|
|
}
|
|
|
|
action.setType(SoSelectionElementAction::Hide);
|
|
for (const auto& sub : hideList) {
|
|
SoDetail* det = nullptr;
|
|
path.truncate(0);
|
|
if (!sub.empty() && getDetailPath(sub.c_str(), &path, false, det)) {
|
|
action.apply(&path);
|
|
}
|
|
delete det;
|
|
}
|
|
path.unrefNoDelete();
|
|
}
|
|
|
|
void ViewProviderLink::setOverrideMode(const std::string& mode)
|
|
{
|
|
auto ext = getLinkExtension();
|
|
if (!ext) {
|
|
return;
|
|
}
|
|
auto obj = ext->getTrueLinkedObject(false);
|
|
if (obj && obj != getObject()) {
|
|
auto vp = Application::Instance->getViewProvider(obj);
|
|
vp->setOverrideMode(mode);
|
|
}
|
|
if (childVp) {
|
|
childVp->setOverrideMode(mode);
|
|
}
|
|
}
|
|
|
|
void ViewProviderLink::onBeforeChange(const App::Property* prop)
|
|
{
|
|
if (prop == &ChildViewProvider) {
|
|
if (childVp) {
|
|
childVp->beforeDelete();
|
|
pcModeSwitch->replaceChild(1, linkView->getLinkRoot());
|
|
childVpLink.reset();
|
|
childVp = nullptr;
|
|
}
|
|
}
|
|
inherited::onBeforeChange(prop);
|
|
}
|
|
|
|
static bool isExcludedProperties(const char* name)
|
|
{
|
|
#define CHECK_EXCLUDE_PROP(_name) \
|
|
if (strcmp(name, #_name) == 0) \
|
|
return true;
|
|
CHECK_EXCLUDE_PROP(Proxy);
|
|
return false;
|
|
}
|
|
|
|
App::Property* ViewProviderLink::getPropertyByName(const char* name) const
|
|
{
|
|
auto prop = inherited::getPropertyByName(name);
|
|
if (prop || isExcludedProperties(name)) {
|
|
return prop;
|
|
}
|
|
if (childVp) {
|
|
prop = childVp->getPropertyByName(name);
|
|
if (prop && !prop->testStatus(App::Property::Hidden)) {
|
|
return prop;
|
|
}
|
|
prop = nullptr;
|
|
}
|
|
if (pcObject && pcObject->canLinkProperties()) {
|
|
auto linked = getLinkedViewProvider(nullptr, true);
|
|
if (linked && linked != this) {
|
|
prop = linked->getPropertyByName(name);
|
|
}
|
|
}
|
|
return prop;
|
|
}
|
|
|
|
void ViewProviderLink::getPropertyMap(std::map<std::string, App::Property*>& Map) const
|
|
{
|
|
inherited::getPropertyMap(Map);
|
|
if (!childVp) {
|
|
return;
|
|
}
|
|
std::map<std::string, App::Property*> childMap;
|
|
childVp->getPropertyMap(childMap);
|
|
for (const auto& v : childMap) {
|
|
auto ret = Map.insert(v);
|
|
if (!ret.second) {
|
|
auto myProp = ret.first->second;
|
|
if (myProp->testStatus(App::Property::Hidden)) {
|
|
ret.first->second = v.second;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ViewProviderLink::visitProperties(const std::function<void(App::Property*)>& visitor) const
|
|
{
|
|
inherited::visitProperties(visitor);
|
|
if (childVp != nullptr) {
|
|
childVp->visitProperties(visitor);
|
|
}
|
|
}
|
|
|
|
void ViewProviderLink::getPropertyList(std::vector<App::Property*>& List) const
|
|
{
|
|
std::map<std::string, App::Property*> Map;
|
|
getPropertyMap(Map);
|
|
List.reserve(List.size() + Map.size());
|
|
for (const auto& v : Map) {
|
|
List.push_back(v.second);
|
|
}
|
|
}
|
|
|
|
ViewProviderDocumentObject* ViewProviderLink::getLinkedViewProvider(
|
|
std::string* subname,
|
|
bool recursive
|
|
) const
|
|
{
|
|
auto self = const_cast<ViewProviderLink*>(this);
|
|
auto ext = getLinkExtension();
|
|
if (!ext) {
|
|
return self;
|
|
}
|
|
App::DocumentObject* linked = nullptr;
|
|
if (!recursive) {
|
|
linked = ext->getLink();
|
|
const char* s = ext->getSubName();
|
|
if (subname && s) {
|
|
*subname = s;
|
|
}
|
|
}
|
|
else {
|
|
linked = ext->getTrueLinkedObject(recursive);
|
|
}
|
|
if (!linked) {
|
|
return self;
|
|
}
|
|
auto res = freecad_cast<ViewProviderDocumentObject*>(
|
|
Application::Instance->getViewProvider(linked)
|
|
);
|
|
if (res) {
|
|
return res;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
void ViewProviderLink::setTransformation(const Base::Matrix4D& rcMatrix)
|
|
{
|
|
inherited::setTransformation(rcMatrix);
|
|
auto ext = getLinkExtension();
|
|
if (ext) {
|
|
if (ext->getScaleVectorProperty()) {
|
|
updateDataPrivate(getLinkExtension(), ext->getScaleVectorProperty());
|
|
}
|
|
else {
|
|
updateDataPrivate(getLinkExtension(), ext->getScaleProperty());
|
|
}
|
|
}
|
|
}
|
|
|
|
void ViewProviderLink::setTransformation(const SbMatrix& rcMatrix)
|
|
{
|
|
inherited::setTransformation(rcMatrix);
|
|
auto ext = getLinkExtension();
|
|
if (ext) {
|
|
if (ext->getScaleVectorProperty()) {
|
|
updateDataPrivate(getLinkExtension(), ext->getScaleVectorProperty());
|
|
}
|
|
else {
|
|
updateDataPrivate(getLinkExtension(), ext->getScaleProperty());
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
namespace Gui
|
|
{
|
|
PROPERTY_SOURCE_TEMPLATE(Gui::ViewProviderLinkPython, Gui::ViewProviderLink)
|
|
template class GuiExport ViewProviderFeaturePythonT<ViewProviderLink>;
|
|
} // namespace Gui
|