diff --git a/src/App/DocumentObserverPython.cpp b/src/App/DocumentObserverPython.cpp index 2161961532..c567dcb418 100644 --- a/src/App/DocumentObserverPython.cpp +++ b/src/App/DocumentObserverPython.cpp @@ -455,7 +455,7 @@ void DocumentObserverPython::slotAppendDynamicProperty(const App::Property& Prop auto container = Prop.getContainer(); Py::Callable method(this->inst.getAttr(std::string("slotAppendDynamicProperty"))); Py::Tuple args(2); - args.setItem(0, Py::Object(static_cast(container)->getPyObject(), true)); + args.setItem(0, Py::Object(container->getPyObject(), true)); // If a property is touched but not part of a document object then its name is null. // In this case the slot function must not be called. const char* prop_name = container->getPropertyName(&Prop); @@ -479,7 +479,7 @@ void DocumentObserverPython::slotRemoveDynamicProperty(const App::Property& Prop auto container = Prop.getContainer(); Py::Callable method(this->inst.getAttr(std::string("slotRemoveDynamicProperty"))); Py::Tuple args(2); - args.setItem(0, Py::Object(static_cast(container)->getPyObject(), true)); + args.setItem(0, Py::Object(container->getPyObject(), true)); // If a property is touched but not part of a document object then its name is null. // In this case the slot function must not be called. const char* prop_name = container->getPropertyName(&Prop); @@ -503,7 +503,7 @@ void DocumentObserverPython::slotChangePropertyEditor(const App::Property& Prop) auto container = Prop.getContainer(); Py::Callable method(this->inst.getAttr(std::string("slotChangePropertyEditor"))); Py::Tuple args(2); - args.setItem(0, Py::Object(static_cast(container)->getPyObject(), true)); + args.setItem(0, Py::Object(container->getPyObject(), true)); // If a property is touched but not part of a document object then its name is null. // In this case the slot function must not be called. const char* prop_name = container->getPropertyName(&Prop); diff --git a/src/Gui/Application.h b/src/Gui/Application.h index 2da8e8a3fe..eda3a1962f 100644 --- a/src/Gui/Application.h +++ b/src/Gui/Application.h @@ -251,6 +251,9 @@ public: static PyObject* sCreateViewer (PyObject *self,PyObject *args); static PyObject* sGetMarkerIndex (PyObject *self,PyObject *args); + + static PyObject* sAddDocObserver (PyObject *self,PyObject *args); + static PyObject* sRemoveDocObserver (PyObject *self,PyObject *args); static PyMethodDef Methods[]; diff --git a/src/Gui/ApplicationPy.cpp b/src/Gui/ApplicationPy.cpp index 4ae44fb0ef..5d3a6508c5 100644 --- a/src/Gui/ApplicationPy.cpp +++ b/src/Gui/ApplicationPy.cpp @@ -57,6 +57,7 @@ #include "Language/Translator.h" #include "DownloadManager.h" #include "DlgPreferencesImp.h" +#include "DocumentObserverPython.h" #include #include #include @@ -188,6 +189,13 @@ PyMethodDef Application::Methods[] = { {"getMarkerIndex", (PyCFunction) Application::sGetMarkerIndex, METH_VARARGS, "Get marker index according to marker size setting"}, + + {"addDocumentObserver", (PyCFunction) Application::sAddDocObserver, METH_VARARGS, + "addDocumentObserver() -> None\n\n" + "Add an observer to get notified about changes on documents."}, + {"removeDocumentObserver", (PyCFunction) Application::sRemoveDocObserver, METH_VARARGS, + "removeDocumentObserver() -> None\n\n" + "Remove an added document observer."}, {NULL, NULL, 0, NULL} /* Sentinel */ }; @@ -1268,3 +1276,26 @@ PyObject* Application::sGetMarkerIndex(PyObject * /*self*/, PyObject *args) return Py_BuildValue("i", Gui::Inventor::MarkerBitmaps::getMarkerIndex("CIRCLE_FILLED", hGrp->GetInt("MarkerSize", defSize))); }PY_CATCH; } + + +PyObject* Application::sAddDocObserver(PyObject * /*self*/, PyObject *args) +{ + PyObject* o; + if (!PyArg_ParseTuple(args, "O",&o)) + return NULL; + PY_TRY { + DocumentObserverPython::addObserver(Py::Object(o)); + Py_Return; + } PY_CATCH; +} + +PyObject* Application::sRemoveDocObserver(PyObject * /*self*/, PyObject *args) +{ + PyObject* o; + if (!PyArg_ParseTuple(args, "O",&o)) + return NULL; + PY_TRY { + DocumentObserverPython::removeObserver(Py::Object(o)); + Py_Return; + } PY_CATCH; +} diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index 92838b4fa8..3bd96e01a6 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -1110,6 +1110,7 @@ SET(FreeCADGui_CPP_SRCS DocumentModel.cpp DocumentPyImp.cpp DocumentObserver.cpp + DocumentObserverPython.cpp ExpressionBinding.cpp GraphicsViewZoom.cpp ExpressionCompleter.cpp @@ -1136,6 +1137,7 @@ SET(FreeCADGui_SRCS Document.h DocumentModel.h DocumentObserver.h + DocumentObserverPython.h ExpressionBinding.cpp ExpressionCompleter.h FreeCADGuiInit.py diff --git a/src/Gui/DocumentObserverPython.cpp b/src/Gui/DocumentObserverPython.cpp new file mode 100644 index 0000000000..9177126c2f --- /dev/null +++ b/src/Gui/DocumentObserverPython.cpp @@ -0,0 +1,236 @@ +/*************************************************************************** + * Copyright (c) 2018 Stefan Tröger * + * * + * 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 "PreCompiled.h" + +#ifndef _PreComp_ +#endif + +#include "Application.h" +#include "Document.h" +#include "ViewProvider.h" +#include "DocumentObserverPython.h" +#include +#include + +using namespace Gui; + +std::vector DocumentObserverPython::_instances; + +void DocumentObserverPython::addObserver(const Py::Object& obj) +{ + _instances.push_back(new DocumentObserverPython(obj)); +} + +void DocumentObserverPython::removeObserver(const Py::Object& obj) +{ + DocumentObserverPython* obs=0; + for (std::vector::iterator it = + _instances.begin(); it != _instances.end(); ++it) { + if ((*it)->inst == obj) { + obs = *it; + _instances.erase(it); + break; + } + } + + delete obs; +} + +DocumentObserverPython::DocumentObserverPython(const Py::Object& obj) : inst(obj) +{ + this->connectApplicationCreatedDocument = Gui::Application::Instance->signalNewDocument.connect(boost::bind + (&DocumentObserverPython::slotCreatedDocument, this, _1)); + this->connectApplicationDeletedDocument = Gui::Application::Instance->signalDeleteDocument.connect(boost::bind + (&DocumentObserverPython::slotDeletedDocument, this, _1)); + this->connectApplicationRelabelDocument = Gui::Application::Instance->signalRelabelDocument.connect(boost::bind + (&DocumentObserverPython::slotRelabelDocument, this, _1)); + this->connectApplicationRenameDocument = Gui::Application::Instance->signalRenameDocument.connect(boost::bind + (&DocumentObserverPython::slotRelabelDocument, this, _1)); + this->connectApplicationActivateDocument = Gui::Application::Instance->signalActiveDocument.connect(boost::bind + (&DocumentObserverPython::slotActivateDocument, this, _1)); + + this->connectDocumentCreatedObject = Gui::Application::Instance->signalNewObject.connect(boost::bind + (&DocumentObserverPython::slotCreatedObject, this, _1)); + this->connectDocumentDeletedObject = Gui::Application::Instance->signalDeletedObject.connect(boost::bind + (&DocumentObserverPython::slotDeletedObject, this, _1)); + this->connectDocumentChangedObject = Gui::Application::Instance->signalChangedObject.connect(boost::bind + (&DocumentObserverPython::slotChangedObject, this, _1, _2)); + +} + +DocumentObserverPython::~DocumentObserverPython() +{ + this->connectApplicationCreatedDocument.disconnect(); + this->connectApplicationDeletedDocument.disconnect(); + this->connectApplicationRelabelDocument.disconnect(); + this->connectApplicationRenameDocument.disconnect(); + this->connectApplicationActivateDocument.disconnect(); + + this->connectDocumentCreatedObject.disconnect(); + this->connectDocumentDeletedObject.disconnect(); + this->connectDocumentChangedObject.disconnect(); +} + +void DocumentObserverPython::slotCreatedDocument(const Gui::Document& Doc) +{ + Base::PyGILStateLocker lock; + try { + if (this->inst.hasAttr(std::string("slotCreatedDocument"))) { + Py::Callable method(this->inst.getAttr(std::string("slotCreatedDocument"))); + Py::Tuple args(1); + args.setItem(0, Py::Object(const_cast(Doc).getPyObject(), true)); + method.apply(args); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +} + +void DocumentObserverPython::slotDeletedDocument(const Gui::Document& Doc) +{ + Base::PyGILStateLocker lock; + try { + if (this->inst.hasAttr(std::string("slotDeletedDocument"))) { + Py::Callable method(this->inst.getAttr(std::string("slotDeletedDocument"))); + Py::Tuple args(1); + args.setItem(0, Py::Object(const_cast(Doc).getPyObject(), true)); + method.apply(args); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +} + +void DocumentObserverPython::slotRelabelDocument(const Gui::Document& Doc) +{ + Base::PyGILStateLocker lock; + try { + if (this->inst.hasAttr(std::string("slotRelabelDocument"))) { + Py::Callable method(this->inst.getAttr(std::string("slotRelabelDocument"))); + Py::Tuple args(1); + args.setItem(0, Py::Object(const_cast(Doc).getPyObject(), true)); + method.apply(args); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +} + +void DocumentObserverPython::slotRenameDocument(const Gui::Document& Doc) +{ + Base::PyGILStateLocker lock; + try { + if (this->inst.hasAttr(std::string("slotRenameDocument"))) { + Py::Callable method(this->inst.getAttr(std::string("slotRenameDocument"))); + Py::Tuple args(1); + args.setItem(0, Py::Object(const_cast(Doc).getPyObject(), true)); + method.apply(args); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +} + +void DocumentObserverPython::slotActivateDocument(const Gui::Document& Doc) +{ + Base::PyGILStateLocker lock; + try { + if (this->inst.hasAttr(std::string("slotActivateDocument"))) { + Py::Callable method(this->inst.getAttr(std::string("slotActivateDocument"))); + Py::Tuple args(1); + args.setItem(0, Py::Object(const_cast(Doc).getPyObject(), true)); + method.apply(args); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +} + +void DocumentObserverPython::slotCreatedObject(const Gui::ViewProvider& Obj) +{ + Base::PyGILStateLocker lock; + try { + if (this->inst.hasAttr(std::string("slotCreatedObject"))) { + Py::Callable method(this->inst.getAttr(std::string("slotCreatedObject"))); + Py::Tuple args(1); + args.setItem(0, Py::Object(const_cast(Obj).getPyObject(), true)); + method.apply(args); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +} + +void DocumentObserverPython::slotDeletedObject(const Gui::ViewProvider& Obj) +{ + Base::PyGILStateLocker lock; + try { + if (this->inst.hasAttr(std::string("slotDeletedObject"))) { + Py::Callable method(this->inst.getAttr(std::string("slotDeletedObject"))); + Py::Tuple args(1); + args.setItem(0, Py::Object(const_cast(Obj).getPyObject(), true)); + method.apply(args); + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +} + +void DocumentObserverPython::slotChangedObject(const Gui::ViewProvider& Obj, + const App::Property& Prop) +{ + Base::PyGILStateLocker lock; + try { + if (this->inst.hasAttr(std::string("slotChangedObject"))) { + Py::Callable method(this->inst.getAttr(std::string("slotChangedObject"))); + Py::Tuple args(2); + args.setItem(0, Py::Object(const_cast(Obj).getPyObject(), true)); + // If a property is touched but not part of a document object then its name is null. + // In this case the slot function must not be called. + const char* prop_name = Obj.getPropertyName(&Prop); + if (prop_name) { + args.setItem(1, Py::String(prop_name)); + method.apply(args); + } + } + } + catch (Py::Exception&) { + Base::PyException e; // extract the Python error text + e.ReportException(); + } +} diff --git a/src/Gui/DocumentObserverPython.h b/src/Gui/DocumentObserverPython.h new file mode 100644 index 0000000000..0087b97b7f --- /dev/null +++ b/src/Gui/DocumentObserverPython.h @@ -0,0 +1,87 @@ +/*************************************************************************** + * Copyright (c) 2018 Stefan Tröger * + * * + * 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 * + * * + ***************************************************************************/ + + +#ifndef GUI_DOCUMENTOBSERVERPYTHON_H +#define GUI_DOCUMENTOBSERVERPYTHON_H + +#include + +#include +#include + +namespace Gui +{ + +/** + * The DocumentObserverPython class is used to notify registered Python instances + * whenever something happens to a document, like creation, destruction, adding or + * removing viewproviders or when viewprovider property changes. This is the equivalent to the app + * python document observer +*/ +class GuiExport DocumentObserverPython +{ + +public: + /// Constructor + DocumentObserverPython(const Py::Object& obj); + virtual ~DocumentObserverPython(); + + static void addObserver(const Py::Object& obj); + static void removeObserver(const Py::Object& obj); + +private: + /** Checks if a new document was created */ + void slotCreatedDocument(const Gui::Document& Doc); + /** Checks if the given document is about to be closed */ + void slotDeletedDocument(const Gui::Document& Doc); + /** Checks if the given document is relabeled */ + void slotRelabelDocument(const Gui::Document& Doc); + /** Checks if the given document is renamed */ + void slotRenameDocument(const Gui::Document& Doc); + /** Checks if the given document is activated */ + void slotActivateDocument(const Gui::Document& Doc); + /** Checks if a new object was added. */ + void slotCreatedObject(const Gui::ViewProvider& Obj); + /** Checks if the given object is about to be removed. */ + void slotDeletedObject(const Gui::ViewProvider& Obj); + /** The property of an observed object has changed */ + void slotChangedObject(const Gui::ViewProvider& Obj, const App::Property& Prop); + +private: + Py::Object inst; + static std::vector _instances; + + typedef boost::signals::connection Connection; + Connection connectApplicationCreatedDocument; + Connection connectApplicationDeletedDocument; + Connection connectApplicationRelabelDocument; + Connection connectApplicationRenameDocument; + Connection connectApplicationActivateDocument; + Connection connectDocumentCreatedObject; + Connection connectDocumentDeletedObject; + Connection connectDocumentChangedObject; +}; + +} //namespace Gui + +#endif // GUI_DOCUMENTOBSERVERPYTHON_H diff --git a/src/Mod/Test/Document.py b/src/Mod/Test/Document.py index 79873a59c5..87e0ba5af6 100644 --- a/src/Mod/Test/Document.py +++ b/src/Mod/Test/Document.py @@ -1407,7 +1407,46 @@ class DocumentObserverCases(unittest.TestCase): self.parameter.append(obj) self.parameter2.append(prop) + class GuiObserver(): + signal = [] + parameter = [] + parameter2 = [] + + def slotCreatedDocument(self, doc): + self.signal.append('DocCreated'); + self.parameter.append(doc); + + def slotDeletedDocument(self, doc): + self.signal.append('DocDeleted'); + self.parameter.append(doc); + + def slotRelabelDocument(self, doc): + self.signal.append('DocRelabled'); + self.parameter.append(doc); + + def slotRenameDocument(self, doc): + self.signal.append('DocRenamed'); + self.parameter.append(doc); + + def slotActivateDocument(self, doc): + self.signal.append('DocActivated'); + self.parameter.append(doc); + + def slotCreatedObject(self, obj): + self.signal.append('ObjCreated'); + self.parameter.append(obj); + + def slotDeletedObject(self, obj): + self.signal.append('ObjDeleted'); + self.parameter.append(obj) + + def slotChangedObject(self, obj, prop): + self.signal.append('ObjChanged'); + self.parameter.append(obj) + self.parameter2.append(prop) + + def setUp(self): self.Obs = self.Observer(); FreeCAD.addDocumentObserver(self.Obs); @@ -1590,6 +1629,98 @@ class DocumentObserverCases(unittest.TestCase): self.Obs.signal = [] self.Obs.parameter = [] self.Obs.parameter2 = [] + + def testGuiObserver(self): + + self.GuiObs = self.GuiObserver() + FreeCAD.Gui.addDocumentObserver(self.GuiObs) + self.Doc1 = FreeCAD.newDocument("Observer1"); + self.GuiDoc1 = FreeCAD.Gui.getDocument("Observer1") + self.Obs.signal = [] + self.Obs.parameter = [] + self.Obs.parameter2 = [] + self.failUnless(self.GuiObs.signal.pop(0) == 'DocCreated') + self.failUnless(self.GuiObs.parameter.pop(0) is self.GuiDoc1) + self.failUnless(self.GuiObs.signal.pop(0) == 'DocActivated') + self.failUnless(self.GuiObs.parameter.pop(0) is self.GuiDoc1) + self.failUnless(self.GuiObs.signal.pop(0) == 'DocRelabled') + self.failUnless(self.GuiObs.parameter.pop(0) is self.GuiDoc1) + self.failUnless(not self.GuiObs.signal and not self.GuiObs.parameter and not self.GuiObs.parameter2) + + self.Doc1.Label = "test" + self.failUnless(self.Obs.signal.pop() == 'DocRelabled') + self.failUnless(self.Obs.parameter.pop() is self.Doc1) + #not interested in the change signals + self.Obs.signal = [] + self.Obs.parameter = [] + self.Obs.parameter2 = [] + self.failUnless(self.GuiObs.signal.pop(0) == 'DocRelabled') + self.failUnless(self.GuiObs.parameter.pop(0) is self.GuiDoc1) + self.failUnless(not self.GuiObs.signal and not self.GuiObs.parameter and not self.GuiObs.parameter2) + + FreeCAD.setActiveDocument('Observer1') + self.failUnless(self.Obs.signal.pop() == 'DocActivated') + self.failUnless(self.Obs.parameter.pop() is self.Doc1) + self.failUnless(not self.Obs.signal and not self.Obs.parameter and not self.Obs.parameter2) + self.failUnless(self.GuiObs.signal.pop() == 'DocActivated') + self.failUnless(self.GuiObs.parameter.pop() is self.GuiDoc1) + self.failUnless(not self.GuiObs.signal and not self.GuiObs.parameter and not self.GuiObs.parameter2) + + obj = self.Doc1.addObject("App::FeaturePython","obj") + self.failUnless(self.Obs.signal.pop() == 'ObjCreated') + self.failUnless(self.Obs.parameter.pop() is obj) + #there are multiple object change signals + self.Obs.signal = [] + self.Obs.parameter = [] + self.Obs.parameter2 = [] + self.failUnless(self.GuiObs.signal.pop() == "ObjCreated") + self.failUnless(self.GuiObs.parameter.pop() is obj.ViewObject) + self.failUnless(not self.GuiObs.signal and not self.GuiObs.parameter and not self.GuiObs.parameter2) + + obj.ViewObject.Visibility = False + self.failUnless(not self.Obs.signal and not self.Obs.parameter and not self.Obs.parameter2) + self.failUnless(self.GuiObs.signal.pop(0) == 'ObjChanged') + self.failUnless(self.GuiObs.parameter.pop(0) is obj.ViewObject) + self.failUnless(self.GuiObs.parameter2.pop(0) == "Visibility") + self.failUnless(not self.Obs.signal and not self.Obs.parameter and not self.Obs.parameter2) + + obj.ViewObject.addProperty("App::PropertyLength","Prop","Group","test property") + self.failUnless(self.Obs.signal.pop() == 'ObjAddDynProp') + self.failUnless(self.Obs.parameter.pop() is obj.ViewObject) + self.failUnless(self.Obs.parameter2.pop() == 'Prop') + self.failUnless(not self.Obs.signal and not self.Obs.parameter and not self.Obs.parameter2) + self.failUnless(not self.GuiObs.signal and not self.GuiObs.parameter and not self.GuiObs.parameter2) + + obj.ViewObject.setEditorMode('Prop', ['ReadOnly']) + self.failUnless(self.Obs.signal.pop() == 'ObjChangePropEdit') + self.failUnless(self.Obs.parameter.pop() is obj.ViewObject) + self.failUnless(self.Obs.parameter2.pop() == 'Prop') + self.failUnless(not self.Obs.signal and not self.Obs.parameter and not self.Obs.parameter2) + self.failUnless(not self.GuiObs.signal and not self.GuiObs.parameter and not self.GuiObs.parameter2) + + obj.ViewObject.removeProperty('Prop') + self.failUnless(self.Obs.signal.pop() == 'ObjRemoveDynProp') + self.failUnless(self.Obs.parameter.pop() is obj.ViewObject) + self.failUnless(self.Obs.parameter2.pop() == 'Prop') + self.failUnless(not self.Obs.signal and not self.Obs.parameter and not self.Obs.parameter2) + self.failUnless(not self.GuiObs.signal and not self.GuiObs.parameter and not self.GuiObs.parameter2) + + vo = obj.ViewObject + FreeCAD.ActiveDocument.removeObject(obj.Name) + self.failUnless(self.Obs.signal.pop() == 'ObjDeleted') + self.failUnless(self.Obs.parameter.pop() is obj) + self.failUnless(not self.Obs.signal and not self.Obs.parameter and not self.Obs.parameter2) + self.failUnless(self.GuiObs.signal.pop() == 'ObjDeleted') + self.failUnless(self.GuiObs.parameter.pop() is vo) + self.failUnless(not self.GuiObs.signal and not self.GuiObs.parameter and not self.GuiObs.parameter2) + + FreeCAD.closeDocument('Observer1') + self.Obs.signal = [] + self.Obs.parameter = [] + self.Obs.parameter2 = [] + self.failUnless(self.GuiObs.signal.pop() == 'DocDeleted') + self.failUnless(self.GuiObs.parameter.pop() is self.GuiDoc1) + self.failUnless(not self.GuiObs.signal and not self.GuiObs.parameter and not self.GuiObs.parameter2) def tearDown(self): #closing doc