Hello friends,
I’ve been working with RhinoCommon and Eto for a couple of years now, and I find myself running into the limits of the documentation that I’ve been able to find for Eto. It seems to me that Eto itself is based on concepts established in other UI frameworks (like Windows Forms or ??? I don’t know much about them, except that Eto is intended to be a cross-platform system so we don’t have to re-write for Windows vs MacOS). It seems like Eto uses certain concepts that are used elsewhere (like data contexts and data bindings) but I’ve not yet found explanations/examples/documentation of those concepts specifically for Eto.
Does anyone have any suggestions for a detailed reference work on a framework similar to Eto, perhaps, that would help guide me on these advanced issues?
One specific example where my conceptual understanding is weak (in addition to the general concept of how data contexts and bindings work) is building a TreeGridView
, that can assign true/false properties (i.e. using check boxes) to an object. Some implementation details:
- For each object, its property data is stored in
Attributes.UserDictionary
, in astring[]
, (e.g.{"main", "all"}
, or{"balcony", "all"}
) - The plugin class has a
Dictionary<string, HashSet<Guid>>
that collects all of thestring
names of the property, as well as all of theGuid
s of the objects that have that property. - Right now, this property is set on a per-Layer basis from another command, with the following code (excerpted from the specific command):
var hitLayers = from layer in doc.Layers
where doc.Objects.GetObjectList(AR_Helpers.ARAllSrfObjTypesEnumSett(layer)).AllObjectsHit(editlist)
select layer.Index;
if (Rhino.UI.Dialogs.ShowSelectMultipleLayersDialog(hitLayers, $"Layers to show Hits on list {editlist}", false, out int[] layerIndices))
{
foreach(int layer in hitLayers)
foreach (RhinoObject obj in doc.Objects.GetObjectList(AR_Helpers.ARAllSrfObjTypesEnumSett(doc.Layers[layer])))
{
ObjectAttributes newattr = obj.Attributes.Duplicate();
if(newattr.UserDictionary.ContainsKey(ARHitObjectID))
{
string[] hitlists = newattr.UserDictionary[ARHitObjectID] as string[];
string[] newlist = hitlists.Except(new[] { editlist }).ToArray();
if (newlist.Length > 0)
newattr.UserDictionary.Set(ARHitObjectID, newlist);
else
newattr.UserDictionary.Remove(ARHitObjectID);
doc.Objects.ModifyAttributes(obj, newattr, true);
}
}
foreach (int layer in layerIndices)
foreach(RhinoObject obj in doc.Objects.GetObjectList(AR_Helpers.ARAllSrfObjTypesEnumSett(doc.Layers[layer])))
{
ObjectAttributes newattr = obj.Attributes.Duplicate();
if (newattr.UserDictionary.ContainsKey(ARHitObjectID))
{
string[] hitlists = newattr.UserDictionary[ARHitObjectID] as string[];
string[] newlist = hitlists.Append(editlist).ToArray();
newattr.UserDictionary.Set(ARHitObjectID, newlist);
}
else newattr.UserDictionary.Set(ARHitObjectID, new[] { editlist });
doc.Objects.ModifyAttributes(obj, newattr, true);
}
return Result.Success;
}
else return Result.Cancel;
- The previous code sets the list on each object in the layer, then the following code is triggered by the
RhinoDoc.ModifyObjectAttributes
event (excerpted), to add or remove those modified surfaces from theDictionary
in the plugin class (described above, which here is represented byparent.ARSim_HitLayers
:
if (rhobj.Attributes.UserDictionary.ContainsKey(ARHitObjectID))
{
string[] lists = rhobj.Attributes.UserDictionary[ARHitObjectID] as string[];
foreach (string list in lists)
{
if (!parent.ARSim_HitLayers.ContainsKey(list)) parent.ARSim_HitLayers.Add(list, new HashSet<Guid>());
parent.ARSim_HitLayers[list].Add(rhobj.Id);
}
}
The point of the panel I’m writing is to be able to modify this attribute (i.e. the members of the string[]
stored in the Attributes.UserDictionary
) on a per-object basis, rather than a per-layer basis. (Please don’t worry about the Absorptive/Reflective/Transparent property; that part works just fine.)
The code for the entire panel follows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Rhino;
using Rhino.UI;
using Rhino.DocObjects;
using static AcousticRays.AR_Helpers;
using static AcousticRays.Simulation.AR_Helpers_Simulation;
using Eto.Forms;
using Eto.Drawing;
using System.Security.Cryptography.X509Certificates;
namespace AR
{
public class AR_PropPage_ObjectMaterial : ObjectPropertiesPage
{
public override string EnglishPageTitle => "AR Material";
private AR_Panel_ObjectMaterial panel;
public override object PageControl => panel ?? (panel = new AR_Panel_ObjectMaterial(p));
public AR_PropPage_ObjectMaterial(ARPlugin p)
{
this.p = p;
}
private ARPlugin p;
public override bool ShouldDisplay(ObjectPropertiesPageEventArgs e)
{
//DebugLine($"arobjmatl shoulddisplay; {e.ObjectCount} objects, includes relevant: {e.IncludesObjectsType(ARSurfaceObjectTypes)}");
return e.IncludesObjectsType(ARSurfaceObjectTypes);
}
public override void UpdatePage(ObjectPropertiesPageEventArgs e)
{
RhinoObject[] selectedObjects = e.GetObjects(ARSurfaceObjectTypes);
panel.SelObjCount = selectedObjects.Length;
panel.CountAbsorptive = 0;
panel.CountReflective = 0;
panel.CountTransparent = 0;
panel.AnyLocked = false;
foreach (RhinoObject obj in selectedObjects)
{
if (obj.Attributes.UserDictionary.TryGetEnumValue(ARMatID, out ARMaterial t))
switch (t)
{
case ARMaterial.Absorptive:
panel.CountAbsorptive++;
break;
case ARMaterial.Reflective:
panel.CountReflective++;
break;
case ARMaterial.Transparent:
panel.CountTransparent++;
break;
}
if (obj.Attributes.UserDictionary.GetBool(ARLockObject, false))
panel.AnyLocked = true;
}
panel.ActiveOnly = false;
panel.TransparentOnly = false;
if (panel.CountReflective == panel.SelObjCount)
{
panel.ActiveOnly = true;
panel.Status = ARMaterial.Reflective;
}
else if (panel.CountAbsorptive == panel.SelObjCount)
{
panel.ActiveOnly = true;
panel.Status = ARMaterial.Absorptive;
}
else if (panel.CountTransparent == panel.SelObjCount)
{
panel.TransparentOnly = true;
panel.Status = ARMaterial.Transparent;
}
else
{
panel.Status = ARMaterial.Unset;
if (panel.CountReflective == 0 && panel.CountAbsorptive == 0)
panel.TransparentOnly = true;
else if ((panel.CountAbsorptive + panel.CountReflective) == panel.SelObjCount)
panel.ActiveOnly = true;
}
panel.doc = e.Document;
panel.RhinoObjects = selectedObjects;
panel.UpdatePanelData();
//DebugLine($"Updatepage SelObjCount: {panel.SelObjCount}, active: {panel.CountActive}, absorptive: {panel.CountAbsorptive}, transparent: {panel.CountTransparent}");
return;
}
public override ObjectType SupportedTypes => ARSurfaceObjectTypes;
}
public class AR_Panel_ObjectMaterial : Panel
{
public ARMaterial Status { get; set; }
public bool ActiveOnly { get; set; }
public bool TransparentOnly { get; set; }
public bool AnyLocked { get; set; }
private RadioButton Reflective;
private RadioButton Absorptive;
private RadioButton Transparent;
private Button clear;
private GridColumn chkColumn, listColumn;
private TreeGridView hitlayerlist;
private bool inUpdate;
internal RhinoDoc doc { private get; set; }
public int CountReflective , CountAbsorptive , CountTransparent , SelObjCount ;
public RhinoObject[] RhinoObjects;
private ARPlugin p;
public AR_Panel_ObjectMaterial(AcousticRays p)
{
DebugLine("material panel constructor");
this.p = p;
Reflective = new RadioButton();
Absorptive = new RadioButton(Reflective);
Transparent = new RadioButton(Reflective);
clear = new Button { Text = "Clear Material", Size = new Size(-1, -1) };
inUpdate = true;
Reflective.CheckedChanged += Radio_CheckedChanged;
Absorptive.CheckedChanged += Radio_CheckedChanged;
Transparent.CheckedChanged += Radio_CheckedChanged;
clear.Click += (sender, e) => { UpdateMaterial(clear: true); };
CustomCell checkboxcells = new CustomCell
{
CreateCell = r =>
{
TreeGridItem item = r.Item as TreeGridItem;
string listname = item.Values[0] as string;
var checkbox = new CheckBox();
checkbox.Checked = (bool)item.Values[0];
checkbox.CheckedChanged += (sender, args) =>
{
CheckBox box = sender as CheckBox;
bool enable = box.Checked ?? false;
foreach (var obj in RhinoObjects)
{
var newattr = obj.Attributes.Duplicate();
if (enable)
{
if (newattr.UserDictionary.ContainsKey(ARHitObjectID))
{
string[] hitlists = newattr.UserDictionary[ARHitObjectID] as string[];
string[] newlist = hitlists.Append(listname).ToArray();
newattr.UserDictionary.Set(ARHitObjectID, newlist);
}
else
newattr.UserDictionary.Set(ARHitObjectID, new[] { listname });
}
else if (newattr.UserDictionary.ContainsKey(ARHitObjectID))
{
string[] hitlists = newattr.UserDictionary[ARHitObjectID] as string[];
string[] newlist = hitlists.Except(new[] { listname }).ToArray();
newattr.UserDictionary.Set(ARHitObjectID, newlist);
}
doc.Objects.ModifyAttributes(obj, newattr, true);
}
};
return checkbox;
}
};
chkColumn = new GridColumn
{
Editable = true,
HeaderText = "",
Resizable = false,
Width = 20,
DataCell = checkboxcells
};
listColumn = new GridColumn
{
Editable = false,
HeaderText = "Hit List",
Resizable = true,
Width = 100,
DataCell = new TextBoxCell(1)
};
hitlayerlist = new TreeGridView
{
Border = BorderType.Line,
ShowHeader = true,
AllowColumnReordering = false,
GridLines = GridLines.None,
Size = new Size(-1, -1),
AllowMultipleSelection = true,
Columns = { chkColumn, listColumn }
};
Padding = new Padding(5);
var layout = new DynamicLayout()
{
Spacing = new Size(5, 5),
Padding = new Padding(5),
Size = new Size(-1, -1),
};
layout.AddRow(Reflective, Absorptive, Transparent, null);
layout.AddRow(clear, null);
layout.Add(null);
var table = new TableLayout
{
Padding = new Padding(5),
Size = new Size(-1, -1),
Rows = { new TableRow(layout), new TableRow(hitlayerlist) }
};
Content = table;
}
private void Radio_CheckedChanged(object sender, EventArgs e)
{
if ((sender as RadioButton).Checked && !inUpdate) UpdateMaterial();
}
private void UpdatePanelText()
{
Reflective.Text = $"Reflective: {CountReflective}";
Absorptive.Text = $"Absorptive: {CountAbsorptive}";
Transparent.Text = $"Transparent: {CountTransparent}";
}
public void UpdatePanelData() // there's probably a better way to do this with events and bindings.
// it doesn't update the item counts always, need extra click somewhere? Radios are fine tho...
{
//DebugLine("updatepanel");
inUpdate = true;
Content.Enabled = !AnyLocked;
UpdatePanelText();
switch (Status)
{
case ARMaterial.Reflective:
Reflective.Checked = true;
break;
case ARMaterial.Absorptive:
Absorptive.Checked = true;
break;
case ARMaterial.Transparent:
Transparent.Checked = true;
break;
case ARMaterial.Unset:
Reflective.Checked = false;
Absorptive.Checked = false;
Transparent.Checked = false;
break;
}
HashSet<Guid> objectids = new HashSet<Guid>(RhinoObjects.Select(x => x.Id));
TreeGridItemCollection hitlayers = new TreeGridItemCollection();
foreach (KeyValuePair<string, HashSet<Guid>> hitlist in p.ARSim_HitLayers)
{
hitlayers.Add(new TreeGridItem(hitlist.Value.IsSupersetOf(objectids), hitlist.Key));
}
hitlayerlist.DataStore = hitlayers;
inUpdate = false;
}
private void UpdateMaterial(bool clear=false)
{
uint undo = doc.BeginUndoRecord("PanelMaterialChange");
if (clear) Status = ARMaterial.Unset;
else if (Reflective.Checked) Status = ARMaterial.Reflective;
else if (Absorptive.Checked) Status = ARMaterial.Absorptive;
else if (Transparent.Checked) Status = ARMaterial.Transparent;
//DebugLine($"updatematerial status: {Status}, obj count: {objects.Length}, objects: {objects}");
CountAbsorptive = 0;
CountReflective = 0;
CountTransparent = 0;
foreach (var obj in RhinoObjects)
{
var newattr = obj.Attributes.Duplicate();
newattr.UserDictionary.SetEnumValue(ARMatID, Status);
doc.Objects.ModifyAttributes(obj, newattr, true);
}
switch (Status)
{
case ARMaterial.Reflective:
CountReflective = SelObjCount;
break;
case ARMaterial.Absorptive:
CountAbsorptive = SelObjCount;
break;
case ARMaterial.Transparent:
CountTransparent = SelObjCount;
break;
case ARMaterial.Unset:
break;
}
if (clear) UpdatePanelData();
else UpdatePanelText();
doc.EndUndoRecord(undo);
return;
}
}
}
This code produces the following panel:
Amazingly, all this works. But I don’t entirely understand why. (I pasted the whole code so that others might benefit from my own struggle, and have a working example to start with.) This StackOverflow topic is the best example of how to use the CustomCell
class I’ve found, but there’s not much meat there.
If I were to initialize the chkColumn
with generic CheckBoxCell
as such:
chkColumn = new GridColumn
{
Editable = true,
HeaderText = "",
Resizable = false,
Width = 20,
DataCell = new CheckBoxCell(0)
};
I don’t understand how I might get access to each CheckBoxCell
to be able to confirm or modify its state (un/checked). I suppose the question is "Is CustomCell
required, or can it be done with CheckBoxCell
? (In this condition, nothing happens.)
I did have some success in the past with the TreeGridView
to make an ersatz-Layer panel that operates as a command dialog (based on the Python code here: Eto.TreeGridView - Keyboard Cell Focus - #2 by spineribjoint1 ):
int levels = 0;
TreeGridItemCollection layerCollection = new TreeGridItemCollection();
foreach (Layer layer in doc.Layers)
{
//need to think about layer.IsReference? Could be for worksessions?
//EXCELLENT It does capture worksession layers, and correctly, too...
string[] names = layer.FullPath.Split(new string[] { Layer.PathSeparator }, StringSplitOptions.None);
int level = names.Length - 1;
if (level > levels) levels = level;
TreeGridItem item = new TreeGridItem(names[level], layer.UserDictionary.GetBool("AR_SWSD_Hit",false), layer.Index);
item.Expanded = layer.IsExpanded;
nodes.Add(new layerlistnodes { level = level, layer = layer, treeItem = item, layerIdx=layer.Index });
}
for (int i = 0; i <= levels; i++)
{
foreach (layerlistnodes node in nodes)
{
if (node.level == i)
{
Guid parent = node.layer.ParentLayerId;
if (parent != Guid.Empty)
{
foreach (layerlistnodes ld in nodes)
{
if (ld.layer.Id == parent)
{
ld.treeItem.Children.Add(node.treeItem);
}
}
}
else layerCollection.Add(node.treeItem);
}
}
}
TreeGridView layerList = new TreeGridView
{
Border = BorderType.Line,
ShowHeader = true,
AllowColumnReordering = false,
GridLines = GridLines.Horizontal,
DataStore = layerCollection,
Size = new Size(-1, -1),
AllowMultipleSelection = true,
Columns = {layColumn,chkColumn}
};
However in this case the condition of each check box is read when the semi-modal panel is closed and the relevant process is started - no need to subscribe to any CheckBox.CheckedChanged
events. Here’s the list box:
So, thanks to everyone who made it this far, and if you don’t have any suggestions on the code that’s fine, I’m really looking for an Eto reference.
Dan