App: support CopyOnChange property in Link

New property status bit 'CopyOnChange' is added for any document object
to publish any property as a 'Configuration' option. When Link is
linked to any object with such property, it will duplicate those
properties and added it Link itself as dynamic properties. If the user
changes any of these dynamic properties, the Link will auto copy the
linked to object and apply the new configuration to it.

The Link has a new property 'LinkCopyOnChange' to allow user to
enable/disable this feature.

Spreadsheet's 'Configuration Table' feature will publish its
configuration property with 'CopyOnChange'.

Currently, once the linked object is copied, it will be independent with
the original object. There is no mechanism to auto sync changes back to
the copy.
This commit is contained in:
Zheng, Lei
2020-01-01 18:19:24 +08:00
committed by Chris Hennes
parent 1f604ec632
commit 4f29e81d0c
5 changed files with 231 additions and 4 deletions

View File

@@ -28,6 +28,7 @@
#include <boost/algorithm/string/predicate.hpp>
#include <boost/preprocessor/stringize.hpp>
#include <Base/Tools.h>
#include "Application.h"
#include "Document.h"
#include "GroupExtension.h"
@@ -53,7 +54,7 @@ namespace bp = boost::placeholders;
EXTENSION_PROPERTY_SOURCE(App::LinkBaseExtension, App::DocumentObjectExtension)
LinkBaseExtension::LinkBaseExtension(void)
:enableLabelCache(false),hasOldSubElement(false)
:enableLabelCache(false),hasOldSubElement(false),hasCopyOnChange(true)
{
initExtensionType(LinkBaseExtension::getExtensionClassTypeId());
EXTENSION_ADD_PROPERTY_TYPE(_LinkTouched, (false), " Link",
@@ -139,7 +140,15 @@ void LinkBaseExtension::setProperty(int idx, Property *prop) {
if(!propLinkMode->getEnums())
propLinkMode->setEnums(linkModeEnums);
break;
} case PropLinkTransform:
}
case PropLinkCopyOnChange: {
static const char *enums[] = {"Disabled","Enabled","Owned",0};
auto propEnum = freecad_dynamic_cast<PropertyEnumeration>(prop);
if(!propEnum->getEnums())
propEnum->setEnums(enums);
break;
}
case PropLinkTransform:
case PropLinkPlacement:
case PropPlacement:
if(getLinkTransformProperty() &&
@@ -181,6 +190,8 @@ void LinkBaseExtension::setProperty(int idx, Property *prop) {
}
}
static const char _GroupPrefix[] = "Configuration (";
App::DocumentObjectExecReturn *LinkBaseExtension::extensionExecute(void) {
// The actual value of LinkRecompouted is not important, just to notify view
// provider that the link (in fact, its dependents, i.e. linked ones) have
@@ -193,6 +204,24 @@ App::DocumentObjectExecReturn *LinkBaseExtension::extensionExecute(void) {
return new App::DocumentObjectExecReturn("Link broken");
App::DocumentObject *container = getContainer();
setupCopyOnChange(container);
if(hasCopyOnChange && getLinkCopyOnChangeValue()==0) {
hasCopyOnChange = false;
std::vector<Property*> props;
container->getPropertyList(props);
for(auto prop : props) {
if(isCopyOnChangeProperty(container, *prop)) {
try {
container->removeDynamicProperty(prop->getName());
} catch (Base::Exception &e) {
e.ReportException();
} catch (...) {
}
}
}
}
PropertyPythonObject *proxy = 0;
if(getLinkExecuteProperty()
&& !boost::iequals(getLinkExecuteValue(), "none")
@@ -253,6 +282,165 @@ short LinkBaseExtension::extensionMustExecute(void) {
return link->mustExecute();
}
bool LinkBaseExtension::isCopyOnChangeProperty(DocumentObject *obj, const App::Property &prop) {
if(obj!=prop.getContainer() || !prop.testStatus(App::Property::PropDynamic))
return false;
auto group = prop.getGroup();
return group && boost::starts_with(group,_GroupPrefix);
}
void LinkBaseExtension::setupCopyOnChange(DocumentObject *parent) {
copyOnChangeConns.clear();
auto linked = getTrueLinkedObject(false);
if(!linked || getLinkCopyOnChangeValue()==0)
return;
hasCopyOnChange = setupCopyOnChange(parent,linked,&copyOnChangeConns,hasCopyOnChange);
}
bool LinkBaseExtension::setupCopyOnChange(DocumentObject *parent, DocumentObject *linked,
std::vector<boost::signals2::scoped_connection> *copyOnChangeConns, bool checkExisting)
{
if(!parent || !linked)
return false;
bool res = false;
std::unordered_map<Property*, Property*> newProps;
std::vector<Property*> props;
linked->getPropertyList(props);
for(auto prop : props) {
if(!prop->testStatus(Property::CopyOnChange)
|| prop->getContainer()!=linked)
continue;
res = true;
const char* linkedGroupName = prop->getGroup();
if(!linkedGroupName || !linkedGroupName[0])
linkedGroupName = "Base";
std::string groupName;
groupName = _GroupPrefix;
if(boost::starts_with(linkedGroupName,_GroupPrefix))
groupName += linkedGroupName + sizeof(_GroupPrefix)-1;
else {
groupName += linkedGroupName;
groupName += ")";
}
auto p = parent->getPropertyByName(prop->getName());
if(p) {
if(p->getContainer()!=parent)
p = 0;
else {
const char* otherGroupName = p->getGroup();
if(!otherGroupName || !boost::starts_with(otherGroupName, _GroupPrefix)) {
FC_WARN(p->getFullName() << " shadows another CopyOnChange property "
<< prop->getFullName());
continue;
}
if(p->getTypeId() != prop->getTypeId() || groupName != otherGroupName) {
parent->removeDynamicProperty(p->getName());
p = 0;
}
}
}
if(!p) {
p = parent->addDynamicProperty(prop->getTypeId().getName(),
prop->getName(), groupName.c_str(), prop->getDocumentation());
std::unique_ptr<Property> pcopy(prop->Copy());
Base::ObjectStatusLocker<Property::Status,Property> guard(Property::User3, p);
if(pcopy) {
p->Paste(*pcopy);
}
p->setStatusValue(prop->getStatus());
}
newProps[p] = prop;
}
if(checkExisting) {
props.clear();
parent->getPropertyList(props);
for(auto prop : props) {
if(prop->getContainer()!=parent)
continue;
auto gname = prop->getGroup();
if(!gname || !boost::starts_with(gname, _GroupPrefix))
continue;
if(!newProps.count(prop))
parent->removeDynamicProperty(prop->getName());
}
}
if(!copyOnChangeConns)
return res;
for(auto &v : newProps) {
// sync configuration properties
copyOnChangeConns->push_back(v.second->signalChanged.connect([parent](const Property &prop) {
if(!prop.testStatus(Property::CopyOnChange))
return;
auto p = parent->getPropertyByName(prop.getName());
if(p && p->getTypeId()==prop.getTypeId()) {
std::unique_ptr<Property> pcopy(prop.Copy());
// temperoray set Output to prevent touching
p->setStatus(Property::Output, true);
// temperoray block copy on change
Base::ObjectStatusLocker<Property::Status,Property> guard(Property::User3, p);
if(pcopy)
p->Paste(*pcopy);
p->setStatusValue(prop.getStatus());
}
}));
}
return res;
}
void LinkBaseExtension::checkCopyOnChange(
App::DocumentObject *parent, const App::Property &prop)
{
if(parent->getDocument()->isPerformingTransaction())
return;
auto linked = getTrueLinkedObject(false);
if(!linked || getLinkCopyOnChangeValue()==0
|| !isCopyOnChangeProperty(parent,prop))
return;
if(getLinkCopyOnChangeValue()==2) {
auto p = linked->getPropertyByName(prop.getName());
if(p && p->getTypeId()==prop.getTypeId()) {
std::unique_ptr<Property> pcopy(prop.Copy());
if(pcopy)
p->Paste(*pcopy);
}
return;
}
auto linkedProp = linked->getPropertyByName(prop.getName());
if(!linkedProp || linkedProp->getTypeId()!=prop.getTypeId() || linkedProp->isSame(prop))
return;
auto objs = parent->getDocument()->copyObject({linked},true);
if(objs.empty())
return;
linked = objs.back();
linked->Visibility.setValue(false);
linkedProp = linked->getPropertyByName(prop.getName());
if(linkedProp && linkedProp->getTypeId()==prop.getTypeId()) {
std::unique_ptr<Property> pcopy(prop.Copy());
if(pcopy)
linkedProp->Paste(*pcopy);
}
getLinkCopyOnChangeProperty()->setValue((long)0);
getLinkedObjectProperty()->setValue(linked);
getLinkCopyOnChangeProperty()->setValue(2);
}
App::GroupExtension *LinkBaseExtension::linkedPlainGroup() const {
if(mySubElements.size() && mySubElements[0].size())
return 0;
@@ -1098,6 +1286,15 @@ void LinkBaseExtension::update(App::DocumentObject *parent, const Property *prop
_ChildCache.setValue();
parseSubName();
syncElementList();
if(getLinkCopyOnChangeValue()==2)
getLinkCopyOnChangeProperty()->setValue(1);
else
setupCopyOnChange(parent);
}else if(prop == getLinkCopyOnChangeProperty()) {
setupCopyOnChange(parent);
}else if(prop == getLinkTransformProperty()) {
auto linkPlacement = getLinkPlacementProperty();
auto placement = getPlacementProperty();
@@ -1107,6 +1304,9 @@ void LinkBaseExtension::update(App::DocumentObject *parent, const Property *prop
linkPlacement->setStatus(Property::Hidden,!transform);
}
syncElementList();
} else {
checkCopyOnChange(parent, *prop);
}
}
@@ -1156,6 +1356,8 @@ void LinkBaseExtension::syncElementList() {
element->LinkedObject.setStatus(Property::Hidden,link!=0);
element->LinkedObject.setStatus(Property::Immutable,link!=0);
if(element->LinkCopyOnChange.getValue()==2)
continue;
if(xlink) {
if(element->LinkedObject.getValue()!=xlink->getValue() ||
element->LinkedObject.getSubValues()!=xlink->getSubValues())

View File

@@ -96,6 +96,14 @@ public:
(LinkTransform, bool, App::PropertyBool, false, \
"Set to false to override linked object's placement", ##__VA_ARGS__)
#define LINK_PARAM_COPY_ON_CHANGE(...) \
(LinkCopyOnChange, long, App::PropertyEnumeration, ((long)0), \
"Disabled: disable copy on change\n"\
"Enabled: enable copy linked object on change of any of its propert marked as CopyOnChange\n"\
"Owned: indicate the linked object has been copied and is own owned by the link. And the\n"\
" the link will try to sync any change of the original linked object back to the copy.",\
##__VA_ARGS__)
#define LINK_PARAM_SCALE(...) \
(Scale, double, App::PropertyFloat, 1.0, "Scale factor", ##__VA_ARGS__)
@@ -162,6 +170,7 @@ public:
LINK_PARAM(MODE)\
LINK_PARAM(LINK_EXECUTE)\
LINK_PARAM(COLORED_ELEMENTS)\
LINK_PARAM(COPY_ON_CHANGE)\
enum PropIndex {
#define LINK_PINDEX_DEFINE(_1,_2,_param) LINK_PINDEX(_param),
@@ -293,11 +302,18 @@ public:
void cacheChildLabel(int enable=-1) const;
static bool setupCopyOnChange(App::DocumentObject *obj, App::DocumentObject *linked,
std::vector<boost::signals2::scoped_connection> *copyOnChangeConns, bool checkExisting);
static bool isCopyOnChangeProperty(App::DocumentObject *obj, const Property &prop);
protected:
void _handleChangedPropertyName(Base::XMLReader &reader,
const char * TypeName, const char *PropName);
void parseSubName() const;
void update(App::DocumentObject *parent, const Property *prop);
void checkCopyOnChange(App::DocumentObject *parent, const App::Property &prop);
void setupCopyOnChange(App::DocumentObject *parent);
void syncElementList();
void detachElement(App::DocumentObject *obj);
void checkGeoElementMap(const App::DocumentObject *obj,
@@ -318,10 +334,12 @@ protected:
mutable std::unordered_map<std::string,int> myLabelCache; // for label based subname lookup
mutable bool enableLabelCache;
bool hasOldSubElement;
mutable bool checkingProperty = false;
std::vector<boost::signals2::scoped_connection> copyOnChangeConns;
bool hasCopyOnChange;
};
///////////////////////////////////////////////////////////////////////////
@@ -456,6 +474,7 @@ public:
LINK_PARAM_EXT_TYPE(COUNT,App::PropertyIntegerConstraint)\
LINK_PARAM_EXT(LINK_EXECUTE)\
LINK_PARAM_EXT_ATYPE(COLORED_ELEMENTS,App::Prop_Hidden)\
LINK_PARAM_EXT(COPY_ON_CHANGE)\
LINK_PROPS_DEFINE(LINK_PARAMS_LINK)
@@ -495,6 +514,7 @@ public:
LINK_PARAM_EXT(TRANSFORM) \
LINK_PARAM_EXT(LINK_PLACEMENT)\
LINK_PARAM_EXT(PLACEMENT)\
LINK_PARAM_EXT(COPY_ON_CHANGE)\
// defines the actual properties
LINK_PROPS_DEFINE(LINK_PARAMS_ELEMENT)

View File

@@ -256,7 +256,8 @@ void Property::setStatusValue(unsigned long status) {
|(1<<PropReadOnly)
|(1<<PropTransient)
|(1<<PropOutput)
|(1<<PropHidden);
|(1<<PropHidden)
|(1<<Busy);
status &= ~mask;
status |= StatusBits.to_ulong() & mask;

View File

@@ -77,6 +77,7 @@ public:
EvalOnRestore = 14, // In case of expression binding, evaluate the
// expression on restore and touch the object on value change.
Busy = 15, // internal use to avoid recursive signaling
CopyOnChange = 16, // for Link to copy the linked object on change of the property with this flag
// The following bits are corresponding to PropertyType set when the
// property added. These types are meant to be static, and cannot be

View File

@@ -507,6 +507,7 @@ enum MenuAction {
MA_Hidden,
MA_Touched,
MA_EvalOnRestore,
MA_CopyOnChange,
};
void PropertyEditor::contextMenuEvent(QContextMenuEvent *) {
@@ -612,6 +613,7 @@ void PropertyEditor::contextMenuEvent(QContextMenuEvent *) {
ACTION_SETUP(Transient);
_ACTION_SETUP(Touched);
_ACTION_SETUP(EvalOnRestore);
_ACTION_SETUP(CopyOnChange);
}
}
@@ -638,6 +640,7 @@ void PropertyEditor::contextMenuEvent(QContextMenuEvent *) {
ACTION_CHECK(Output);
ACTION_CHECK(Hidden);
ACTION_CHECK(EvalOnRestore);
ACTION_CHECK(CopyOnChange);
case MA_Touched:
for(auto prop : props) {
if(action->isChecked())