Draft: allow objects in layers to have overrides (#19207)

* Draft: allow objects in layers to have overrides

Fixes #17844.

1. When an object is put in a layer it always adopts the properties of that layer.
2. Properties of the object that are subsequently changed are considered overrides.
3. When the properties of the layer, or the object in the layer, are then changed so that they match again, there is no longer an override, and properties are synced again.
4. The layer and the layer container object get an extra Tree view context menu option "Reassign properties of layer"/"Reassign properties of all layers".
This commit is contained in:
Roy-043
2025-01-28 20:42:45 +01:00
committed by GitHub
parent 05a3e8d750
commit dfe670e1ae
2 changed files with 195 additions and 131 deletions

View File

@@ -113,12 +113,49 @@ class Layer:
"""Execute when the object is created or recomputed. Do nothing."""
pass
def _get_other_layers(self, obj, child):
other_lyrs = []
for find in child.Document.findObjects(Type="App::FeaturePython"):
if utils.get_type(find) == "Layer" and find != obj and child in find.Group:
other_lyrs.append(find)
return other_lyrs
def onBeforeChange(self, obj, prop):
if prop == "Group":
self.oldGroup = obj.Group
def onChanged(self, obj, prop):
if prop != "Group":
return
vobj = getattr(obj, "ViewObject", None)
old_grp = getattr(self, "oldGroup", [])
for child in obj.Group:
if child in old_grp:
continue
for other_lyr in self._get_other_layers(obj, child):
other_grp = other_lyr.Group
other_grp.remove(child)
other_lyr.Group = other_grp
if vobj is None:
continue
for prop in ("LineColor", "ShapeAppearance", "LineWidth", "DrawStyle", "Visibility"):
vobj.Proxy.change_view_properties(vobj, prop, old_prop=None, targets=[child])
def addObject(self, obj, child):
"""Add an object to this object if not in the Group property."""
group = obj.Group
if child not in group:
group.append(child)
obj.Group = group
if utils.get_type(child) in ("Layer", "LayerContainer"):
return
grp = obj.Group
if child in grp:
return
grp.append(child)
obj.Group = grp
def removeObject(self, obj, child):
grp = obj.Group
if not child in grp:
return
grp.remove(child)
obj.Group = grp
# Alias for compatibility with v0.18 and earlier
@@ -147,9 +184,9 @@ class LayerContainer:
Update the value of `Group` by sorting the contained layers
by `Label`.
"""
group = obj.Group
group.sort(key=lambda layer: layer.Label)
obj.Group = group
grp = obj.Group
grp.sort(key=lambda layer: layer.Label)
obj.Group = grp
def dumps(self):
"""Return a tuple of objects to save or None."""
@@ -162,7 +199,6 @@ class LayerContainer:
self.Type = state
# Similar function as in view_layer.py
def get_layer(obj):
"""Get the layer the object belongs to."""
finds = obj.Document.findObjects(Name="LayerContainer")

View File

@@ -29,14 +29,14 @@
## \addtogroup draftviewproviders
# @{
import pivy.coin as coin
import PySide.QtCore as QtCore
import PySide.QtGui as QtGui
from pivy import coin
from PySide import QtCore
from PySide import QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD as App
import FreeCADGui as Gui
from draftobjects.layer import Layer
from draftobjects.layer import get_layer
from draftutils import params
from draftutils import utils
from draftutils.translate import translate
@@ -216,14 +216,43 @@ class ViewProviderLayer:
def updateData(self, obj, prop):
"""Execute when a property from the Proxy class is changed."""
if prop == "Group":
for _prop in ("LineColor", "ShapeAppearance", "LineWidth",
"DrawStyle", "Visibility"):
self.onChanged(obj.ViewObject, _prop)
if prop == "Label":
self._paint_tree_icon(obj.ViewObject)
def change_view_properties(self, vobj, prop):
"""Iterate over the contents and change the properties."""
obj = vobj.Object
def change_view_properties(self, vobj, prop, old_prop=None, targets=None):
"""Change the properties of the targets, or of all objects in the layer group.
A child's property is only changed if it is not overridden (if its value
matches old_prop).
"""
def _color_is_same(col1, col2):
# Ignore alpha and round RGB values
return [round(c, 2) for c in col1[:3]] == [round(c, 2) for c in col2[:3]]
def _app_material_is_same(mat1, mat2):
for prop in (
"AmbientColor",
"DiffuseColor",
"EmissiveColor",
"Shininess",
"SpecularColor",
"Transparency",
):
if "Color" in prop:
if not _color_is_same(getattr(mat1, prop), getattr(mat2, prop)):
return False
elif getattr(mat1, prop) != getattr(mat2, prop):
return False
return True
def _prop_is_same(prop1, prop2):
if isinstance(prop1, tuple):
if isinstance(prop1[0], App.Material):
# We do not check the length of the ShapeAppearance
return _app_material_is_same(prop1[0], prop2[0])
return _color_is_same(prop1, prop2)
return prop1 == prop2
# Return if the property does not exist
if not hasattr(vobj, prop):
@@ -231,27 +260,30 @@ class ViewProviderLayer:
# If the override properties are not set return without change
if prop == "LineColor" and not vobj.OverrideLineColorChildren:
return
elif prop == "ShapeAppearance" and not vobj.OverrideShapeAppearanceChildren:
if prop == "ShapeAppearance" and not vobj.OverrideShapeAppearanceChildren:
return
for target_obj in obj.Group:
for target_obj in (targets if targets is not None else vobj.Object.Group):
target_vobj = target_obj.ViewObject
# This checks that the property exists in the target object,
# and then sets the target property accordingly
if hasattr(target_vobj, prop):
setattr(target_vobj, prop, getattr(vobj, prop))
if old_prop is None \
or _prop_is_same(getattr(target_vobj, prop), old_prop):
setattr(target_vobj, prop, getattr(vobj, prop))
# Use the line color for the point color and text color
if prop == "LineColor":
if hasattr(target_vobj, "PointColor"):
target_vobj.PointColor = vobj.LineColor
if hasattr(target_vobj, "TextColor"):
target_vobj.TextColor = vobj.LineColor
# Use the line width for the point size
elif prop == "LineWidth":
if hasattr(target_vobj, "PointSize"):
target_vobj.PointSize = vobj.LineWidth
# Use the line color for the point color and text color,
# and the line width for the point size
dic = {"LineColor": ("PointColor", "TextColor"), "LineWidth": ("PointSize", )}
if prop in dic:
for target_prop in dic[prop]:
if hasattr(target_vobj, target_prop):
if old_prop is None \
or _prop_is_same(getattr(target_vobj, target_prop), old_prop):
setattr(target_vobj, target_prop, getattr(vobj, prop))
def onBeforeChange(self, vobj, prop):
if prop in ("LineColor", "ShapeAppearance", "LineWidth", "DrawStyle", "Visibility"):
setattr(self, "old" + prop, getattr(vobj, prop))
def onChanged(self, vobj, prop):
"""Execute when a view property is changed."""
@@ -290,58 +322,52 @@ class ViewProviderLayer:
"DrawStyle", "Visibility")
and hasattr(vobj, "OverrideLineColorChildren")
and hasattr(vobj, "OverrideShapeAppearanceChildren")):
self.change_view_properties(vobj, prop)
old_prop = getattr(self, "old" + prop, None)
self.change_view_properties(vobj, prop, old_prop)
if hasattr(self, "old" + prop):
delattr(self, "old" + prop)
# Paint the layer icon in the tree view:
if (prop in ("LineColor", "ShapeAppearance")
and hasattr(vobj, "LineColor")
and hasattr(vobj, "ShapeAppearance")):
l_color = vobj.LineColor
s_color = vobj.ShapeAppearance[0].DiffuseColor
if prop in ("LineColor", "ShapeAppearance"):
self._paint_tree_icon(vobj)
l_color = QtGui.QColor(int(l_color[0] * 255),
int(l_color[1] * 255),
int(l_color[2] * 255))
s_color = QtGui.QColor(int(s_color[0] * 255),
int(s_color[1] * 255),
int(s_color[2] * 255))
p1 = QtCore.QPointF(2, 17)
p2 = QtCore.QPointF(13, 8)
p3 = QtCore.QPointF(30, 15)
p4 = QtCore.QPointF(20, 25)
def _paint_tree_icon(self, vobj):
"""Paint the layer icon in the tree view."""
if not hasattr(vobj, "LineColor"):
return
if not hasattr(vobj, "ShapeAppearance"):
return
l_color = vobj.LineColor
s_color = vobj.ShapeAppearance[0].DiffuseColor
image = QtGui.QImage(32, 32, QtGui.QImage.Format_ARGB32)
image.fill(QtCore.Qt.transparent)
l_color = QtGui.QColor(int(l_color[0] * 255),
int(l_color[1] * 255),
int(l_color[2] * 255))
s_color = QtGui.QColor(int(s_color[0] * 255),
int(s_color[1] * 255),
int(s_color[2] * 255))
p1 = QtCore.QPointF(2, 17)
p2 = QtCore.QPointF(13, 8)
p3 = QtCore.QPointF(30, 15)
p4 = QtCore.QPointF(20, 25)
pt = QtGui.QPainter(image)
pt.setBrush(QtGui.QBrush(s_color, QtCore.Qt.SolidPattern))
pt.drawPolygon([p1, p2, p3, p4])
pt.setPen(QtGui.QPen(l_color, 2,
QtCore.Qt.SolidLine, QtCore.Qt.FlatCap))
pt.drawPolygon([p1, p2, p3, p4])
pt.end()
image = QtGui.QImage(32, 32, QtGui.QImage.Format_ARGB32)
image.fill(QtCore.Qt.transparent)
byte_array = QtCore.QByteArray()
buffer = QtCore.QBuffer(byte_array)
buffer.open(QtCore.QIODevice.WriteOnly)
image.save(buffer, "XPM")
pt = QtGui.QPainter(image)
pt.setBrush(QtGui.QBrush(s_color, QtCore.Qt.SolidPattern))
pt.drawPolygon([p1, p2, p3, p4])
pt.setPen(QtGui.QPen(l_color, 2,
QtCore.Qt.SolidLine, QtCore.Qt.FlatCap))
pt.drawPolygon([p1, p2, p3, p4])
pt.end()
self.icondata = byte_array.data().decode("latin1")
vobj.signalChangeIcon()
byte_array = QtCore.QByteArray()
buffer = QtCore.QBuffer(byte_array)
buffer.open(QtCore.QIODevice.WriteOnly)
image.save(buffer, "XPM")
def _get_layer(self, obj):
"""Get the layer the object belongs to.
"""
from draftmake.make_layer import get_layer_container
# First look in the LayerContainer:
for layer in get_layer_container().Group:
if utils.get_type(layer) == "Layer" and obj in layer.Group:
return layer
# If not found, look through all App::FeaturePython objects (not just layers):
for find in obj.Document.findObjects(Type="App::FeaturePython"):
if utils.get_type(find) == "Layer" and obj in find.Group:
return find
return None
self.icondata = byte_array.data().decode("latin1")
vobj.signalChangeIcon()
def canDragObject(self, obj):
"""Return True to allow dragging one object from the Layer.
@@ -356,7 +382,7 @@ class ViewProviderLayer:
if hasattr(parent, "Group"):
old_data.append([parent, parent.Group])
# Layers are not in the Inlist because a layer's Group is App::PropertyLinkListHidden:
layer = self._get_layer(obj)
layer = get_layer(obj)
if layer is not None:
old_data.append([layer, layer.Group])
if old_data:
@@ -369,14 +395,11 @@ class ViewProviderLayer:
"""Return True to allow dragging many objects from the Layer."""
return True
def dragObject(self, vobj, otherobj):
def dragObject(self, vobj, child):
"""Remove the object that was dragged from the layer."""
layer = vobj.Object
if otherobj in layer.Group:
group = layer.Group
group.remove(otherobj)
layer.Group = group
App.ActiveDocument.recompute()
obj = vobj.Object
obj.Proxy.removeObject(obj, child)
App.ActiveDocument.recompute()
def canDropObject(self, obj):
"""Return true to allow dropping one object.
@@ -395,30 +418,14 @@ class ViewProviderLayer:
"""Return true to allow dropping many objects."""
return True
def dropObject(self, vobj, otherobj):
"""Add object that was dropped into the Layer to the group.
def dropObject(self, vobj, child):
"""Add the object that was dropped on the Layer to the group.
If the object being dropped is itself a `'Layer'`,
return immediately to prevent dropping a layer inside a layer,
at least for now.
This also results in a call to `change_view_properties` to update the
view properties of the child.
"""
if utils.get_type(otherobj) == "Layer":
return
# We assume a single old layer...
old_layer = self._get_layer(otherobj)
if old_layer is not None:
group = old_layer.Group
group.remove(otherobj)
old_layer.Group = group
new_layer = vobj.Object
if otherobj not in new_layer.Group:
group = new_layer.Group
group.append(otherobj)
new_layer.Group = group
obj = vobj.Object
obj.Proxy.addObject(obj, child)
App.ActiveDocument.recompute()
def update_groups_after_drag_drop(self):
@@ -450,7 +457,7 @@ class ViewProviderLayer:
old_layer = old_parent
break
new_layer = self._get_layer(child)
new_layer = get_layer(child)
if new_layer == old_layer:
continue
@@ -497,6 +504,12 @@ class ViewProviderLayer:
action_activate.triggered.connect(self.activate)
menu.addAction(action_activate)
action_reassign = QtGui.QAction(QtGui.QIcon(":/icons/Draft_Apply.svg"),
translate("draft", "Reassign properties of layer"),
menu)
action_reassign.triggered.connect(self.reassign_props)
menu.addAction(action_reassign)
action_select = QtGui.QAction(QtGui.QIcon(":/icons/Draft_SelectGroup.svg"),
translate("draft", "Select layer contents"),
menu)
@@ -511,11 +524,15 @@ class ViewProviderLayer:
Gui.activateWorkbench("DraftWorkbench")
Gui.runCommand("Draft_AutoGroup")
def reassign_props(self):
for prop in ("LineColor", "ShapeAppearance", "LineWidth", "DrawStyle", "Visibility"):
self.onChanged(self.Object.ViewObject, prop)
def select_contents(self):
"""Select the contents of the layer."""
Gui.Selection.clearSelection()
for layer_obj in self.Object.Group:
Gui.Selection.addSelection(layer_obj)
for obj in self.Object.Group:
Gui.Selection.addSelection(obj)
class ViewProviderLayerContainer:
@@ -535,17 +552,41 @@ class ViewProviderLayerContainer:
def setupContextMenu(self, vobj, menu):
"""Set up actions to perform in the context menu."""
action_add = QtGui.QAction(QtGui.QIcon(":/icons/Draft_NewLayer.svg"),
translate("draft", "Add new layer"),
menu)
action_add.triggered.connect(self.add_layer)
menu.addAction(action_add)
action_reassign = QtGui.QAction(QtGui.QIcon(":/icons/Draft_Apply.svg"),
translate("draft", "Reassign properties of all layers"),
menu)
action_reassign.triggered.connect(self.reassign_props)
menu.addAction(action_reassign)
action_merge = QtGui.QAction(QtGui.QIcon(":/icons/Draft_Layers.svg"),
translate("draft", "Merge layer duplicates"),
menu)
action_merge.triggered.connect(self.merge_by_name)
menu.addAction(action_merge)
action_add = QtGui.QAction(QtGui.QIcon(":/icons/Draft_NewLayer.svg"),
translate("draft", "Add new layer"),
menu)
action_add.triggered.connect(self.add_layer)
menu.addAction(action_add)
def add_layer(self):
"""Creates a new layer"""
import Draft
doc = App.ActiveDocument
doc.openTransaction(translate("draft", "Add new layer"))
Draft.make_layer(name=None, line_color=None, shape_color=None,
line_width=None, draw_style=None, transparency=None)
doc.recompute()
doc.commitTransaction()
def reassign_props(self):
for obj in self.Object.Group:
if utils.get_type(obj) == "Layer":
obj.ViewObject.Proxy.reassign_props()
def merge_by_name(self):
"""Merge the layers that have the same base label."""
@@ -590,19 +631,6 @@ class ViewProviderLayerContainer:
doc.recompute()
doc.commitTransaction()
def add_layer(self):
"""Creates a new layer"""
import Draft
doc = App.ActiveDocument
doc.openTransaction(translate("draft", "Add new layer"))
Draft.make_layer(name=None, line_color=None, shape_color=None,
line_width=None, draw_style=None, transparency=None)
doc.recompute()
doc.commitTransaction()
def dumps(self):
"""Return a tuple of objects to save or None."""
return None