I have done some serious change to FreeCAD, which affects everything. For that reason, I am asking for a bit of testing, just to know that workbenches I'm not familiar with are still working (i.e. Arch, FEM, and maybe some add-ons). Make sure you disable Part-o-Magic, if you have it.
https://github.com/DeepSOIC/FreeCAD-ell ... Container1
------------------------------------------------------------------------
Introducing: App.ActiveContainer, and Container interface class
== FOR USERS ==
From now on, body activation and part activation have FreeCAD-wide influence. If a Part is active, any new object is automatically added to it (including new Parts).
Same applies to PartDesign Body. As a result, attempting to create a Cube from Part workbench when PartDesign Body is active will fail with an error. Note that new sketches from Sketcher workbench will be accepted by Body, as well as most Draft primitives. That means you can create Draft ShapeString in a Body.
Also, you will have to deactivate a Body before creating a new Body (to deactivate a body, double-click it).
It used to be possible to activate a Part and a Body independently, which didn't really make sense. This was removed. There is only one active container now. Activating a Part will deactivate a Body.
== FOR PYTHON SCRIPTERS ==
From now on, the recommended way of creating new objects is:
App.ActiveContainer.newObject(...)
Doing so, you are specific about that you are creating an object into active container. If you for whatever reason want to create an object into any other container, use:
App.Containers.Container(App.ActiveDocument.Group).newObject(...)
All calls to App.ActiveDocument.addObject(...) are implicitly redirected to App.ActiveContainer.newObject(...). You can use App.ActiveDocument.newObject if you want to ignore active container.
You can create a standard interface around any object-containing thing like this: App.Containers.Container(thing). Thing can be a Document, a Group, a Part, Body, or Origin (Origin is there only for querying its children; adding objects to there is blocked).
The standard interface offers object creation, deletion, withdrawal, children enumeration, etc. It offers a canAccept() method, which can be used to test if an object of certain type can be created in the container. This is to be used in Gui Command code to gray out the button if object can't be created. I even think, it is worth adding another field to Command definition, which will make this test implicit.
Container() is an interface object. It has very small memory footprint, and operates on the underlying document/documentobject. It is OK to create it on the fly as needed.
To subscribe to changes of active container:
Code: Select all
class ACObserver(object):
def slotDocActiveContainer(self, doc, newContainer, oldContainer):
# called whenever active container in a document changes
pass
def slotAppActiveContainer(self, newContainer, oldContainer):
# called whenever App.ActiveContainer changes. This can happen either because
# active container in active document changes, or because active document is switched.
pass
observer = ACObserver()
App.addDocumentObserver(observer)
body = Gui.ActiveDocument.ActiveView.getActiveObject('pdbody')
Instead, use App.ActiveContainer.Object, or App.getDocument(name_of_document).ActiveContainer.
@shaise: you'll have to patch up sheetmetal again, sorry =)
== FOR C++ ==
In C++, Container object works very similarly.
App::GetApplication().getActiveContainer() returns a Container object. For everything else, Container interface should be constructed on the fly like so: Container(object)
It is intended to be handled in a by-value manner, simplifying memory management.
In C++, the container interface is even more useful, because it should eliminate a lot of type casting.
== IMPLEMENTATION DETAILS ==
Why interface class?
The options I was considering were:
1) just add ActiveContainer, assign object to it (either Group, or Document, or None) and do no more.
2) use None/nullptr for when document itself is active.
3) add common interface to Document, GroupExtension, and Origin. This could be by either:
3.1) make Document and Origin inherit from GroupExtension.
3.2) make GroupExtension inherit from Document, and Origin inherit GroupExtension
3.3) make Document, Group and Origin inherit from a standalone interface (abstract) object.
4) implement an independent interface object, which knows the three container types and translates calls.
Option 1 was the first thing I did. Then, when I started porting PartDesign and other things, it quickly became clear that dealing with three cases, and doing corresponding rather lengthy type casting was very annoying and inproductive. The final problem came up when I gave a try at porting object duplication. There I realized that Origin is causing trouble: first, it wasn't recognized as being in the Part/Body, second it was a kind of container itself. So I had to do something about it.
Option 2 really doesn't look very different from 1.
Option 3 is interesting, especially 3.2. Actually really interesting. It could allow to store Parts in files, and to have several objects with the same name in one project. But this means file format has to change, and .. the way viewproviders are organized, expressions and... well.. almost all FreeCAD core has to change, all at once. That will probably require forking FreeCAD and long development by a team. I'm definitely unable to do that.
So what I have done is option 4. It isn't as extensible and cool as 3.2, but it is a relatively small task that I can manage. Also, extensibility is provided by GroupExtension thanks to @ickby, so it is unlikely it will ever be needed to add more fundamental container types.
Why not 3.1 or 3.3? Because I remember the trouble @ickby had introducing extensions, and dealing with multiple inheritance. So I decided that option 4 is better just because it's dead simple. In addition, it allows to deploy another container-like system I plan to call Workspace, without much trouble.
-----------
Container remembers the object by taking a Python reference to the object. This is for delete-proofing (e.g. if the object the container interface is around gets deleted, the pointer will become invalid and crash may result). I originally made it use a raw pointer and subscribed to deletion signals to wipe out the pointer... but when I came up with the Py-like way of doing it, the benefits were obvious (no boost rubbish i.e. manual copy-constructor implementation, no performance penalty).
I've made a small experiment with conversion of C++ exceptions into python exeptions. Container defines quite a number of different exceptions, and I made these exceptions know their corresponding Py exception, which is used when translating the exceptions in PyImp. It is rather successful, so it may be worth adding this for all exceptions, not to just ContainerErrors. @abdullah: you said you want to propose some change to exception system. I'd be curious to know if it is the thing you wanted to do or not. Check out App/Containers/Exceptions.h-.cpp
I decided to not include my changes to Draft Autogroup, where I made it tied with active container. I'll leave that to Yorik to sort out, because I have poor understanding of Draft and Arch. I find it a bit unfortunate, because Draft AutoGroup became a rather nice display/set widget for active container.
I removed ActiveContainer management from Viewer's ActiveObject system. Even though I think it is a rather good idea to make it possible to have different active containers in differen views of the project, this needs the ability to have different states of viewproviders for each viewer. I think it's not coming any time soon. I haven't taken down the viewer activeobject system completely, and active container can be easily tied up to that system again if necessary.
== NEXT STEPS ==
This is only a beginning. I plan to create Workspace interface, which is similar to Container but gathers objects sharing a coordinate system transform. I also plan introducing ContainerPath and WorkspacePath objects. WorkspacePath's primary purpose will be to compute placement transforms, and ContainerPath is useful in Part-o-magic, hence will be useful in FreeCAD Gui as well. Before I spend time implementing those, I want the approval of the Container concept, the approval being a merge =) .