2
// PropertyGridTable.cs
5
// Lluis Sanchez <lluis@xamarin.com>
7
// Copyright (c) 2012 Xamarin Inc
9
// Permission is hereby granted, free of charge, to any person obtaining a copy
10
// of this software and associated documentation files (the "Software"), to deal
11
// in the Software without restriction, including without limitation the rights
12
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
// copies of the Software, and to permit persons to whom the Software is
14
// furnished to do so, subject to the following conditions:
16
// The above copyright notice and this permission notice shall be included in
17
// all copies or substantial portions of the Software.
19
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29
using System.ComponentModel;
30
using System.Collections.Generic;
34
namespace MonoDevelop.Components.PropertyGrid
36
class PropertyGridTable: Gtk.EventBox
38
EditorManager editorManager;
39
List<TableRow> rows = new List<TableRow> ();
40
Dictionary<Gtk.Widget, Gdk.Rectangle> children = new Dictionary<Gtk.Widget, Gdk.Rectangle> ();
41
EditSession editSession;
42
Gtk.Widget currentEditor;
43
TableRow currentEditorRow;
45
Gdk.Pixbuf discloseDown;
46
Gdk.Pixbuf discloseUp;
49
const int CategoryTopBottomPadding = 6;
50
const int CategoryLeftPadding = 8;
51
const int PropertyTopBottomPadding = 5;
52
const int PropertyLeftPadding = 8;
53
const int PropertyContentLeftPadding = 8;
54
const int PropertyIndent = 8;
55
static readonly Cairo.Color LabelBackgroundColor = new Cairo.Color (250d/255d, 250d/255d, 250d/255d);
56
static readonly Cairo.Color DividerColor = new Cairo.Color (217d/255d, 217d/255d, 217d/255d);
57
static readonly Cairo.Color CategoryLabelColor = new Cairo.Color (128d/255d, 128d/255d, 128d/255d);
59
const uint animationTimeSpan = 10;
60
const int animationStepSize = 20;
62
double dividerPosition = 0.5;
64
Gdk.Cursor resizeCursor;
65
Gdk.Cursor handCursor;
69
public bool IsCategory;
71
public object Instace;
72
public PropertyDescriptor Property;
73
public List<TableRow> ChildRows;
75
public bool Enabled = true;
76
public Gdk.Rectangle EditorBounds;
77
public bool AnimatingExpand;
78
public int AnimationHeight;
79
public int ChildrenHeight;
80
public uint AnimationHandle;
83
public PropertyGridTable (EditorManager editorManager, PropertyGrid parentGrid)
85
Mono.TextEditor.GtkWorkarounds.FixContainerLeak (this);
87
this.editorManager = editorManager;
88
WidgetFlags |= Gtk.WidgetFlags.AppPaintable;
89
Events |= Gdk.EventMask.PointerMotionMask;
91
resizeCursor = new Cursor (CursorType.SbHDoubleArrow);
92
handCursor = new Cursor (CursorType.Hand1);
93
discloseDown = Gdk.Pixbuf.LoadFromResource ("disclose-arrow-down.png");
94
discloseUp = Gdk.Pixbuf.LoadFromResource ("disclose-arrow-up.png");
97
protected override void OnDestroyed ()
101
resizeCursor.Dispose ();
102
handCursor.Dispose ();
105
public event EventHandler Changed;
107
public PropertySort PropertySort { get; set; }
109
public ShadowType ShadowType { get; set; }
111
public void CommitChanges ()
116
HashSet<string> expandedStatus;
118
public void SaveStatus ()
120
expandedStatus = new HashSet<string> ();
121
foreach (var r in rows.Where (r => r.IsCategory))
123
expandedStatus.Add (r.Label);
126
public void RestoreStatus ()
128
if (expandedStatus == null)
131
foreach (var row in rows.Where (r => r.IsCategory))
132
row.Expanded = !expandedStatus.Contains (row.Label);
134
expandedStatus = null;
140
public virtual void Clear ()
142
heightMeasured = false;
143
StopAllAnimations ();
150
internal void Populate (PropertyDescriptorCollection properties, object instance)
152
bool categorised = PropertySort == PropertySort.Categorized;
154
//transcribe browsable properties
155
var sorted = new List<PropertyDescriptor>();
157
foreach (PropertyDescriptor descriptor in properties)
158
if (descriptor.IsBrowsable)
159
sorted.Add (descriptor);
161
if (sorted.Count == 0)
165
if (PropertySort != PropertySort.NoSort)
166
sorted.Sort ((a,b) => a.DisplayName.CompareTo (b.DisplayName));
167
foreach (PropertyDescriptor pd in sorted)
168
AppendProperty (rows, pd, instance);
171
sorted.Sort ((a,b) => {
172
var c = a.Category.CompareTo (b.Category);
173
return c != 0 ? c : a.DisplayName.CompareTo (b.DisplayName);
175
TableRow lastCat = null;
176
List<TableRow> rowList = rows;
178
foreach (PropertyDescriptor pd in sorted) {
179
if (!string.IsNullOrEmpty (pd.Category) && (lastCat == null || pd.Category != lastCat.Label)) {
180
TableRow row = new TableRow ();
181
row.IsCategory = true;
183
row.Label = pd.Category;
184
row.ChildRows = new List<TableRow> ();
187
rowList = row.ChildRows;
189
AppendProperty (rowList, pd, instance);
196
internal void Update (PropertyDescriptorCollection properties, object instance)
198
foreach (PropertyDescriptor pd in properties)
199
UpdateProperty (pd, instance, rows);
204
bool UpdateProperty (PropertyDescriptor pd, object instance, IEnumerable<TableRow> rowList)
206
foreach (var row in rowList) {
207
if (row.Property != null && row.Property.Name == pd.Name && row.Instace == instance) {
211
if (row.ChildRows != null) {
212
if (UpdateProperty (pd, instance, row.ChildRows))
219
void AppendProperty (PropertyDescriptor prop, object instance)
221
AppendProperty (rows, prop, new InstanceData (instance));
224
void AppendProperty (List<TableRow> rowList, PropertyDescriptor prop, object instance)
226
TableRow row = new TableRow () {
229
Label = prop.DisplayName,
234
TypeConverter tc = prop.Converter;
235
if (typeof (ExpandableObjectConverter).IsAssignableFrom (tc.GetType ())) {
236
object cob = prop.GetValue (instance);
237
row.ChildRows = new List<TableRow> ();
238
foreach (PropertyDescriptor cprop in TypeDescriptor.GetProperties (cob))
239
AppendProperty (row.ChildRows, cprop, cob);
243
PropertyEditorCell GetCell (TableRow row)
245
var e = editorManager.GetEditor (row.Property);
246
e.Initialize (this, editorManager, row.Property, row.Instace);
250
protected override void ForAll (bool includeInternals, Gtk.Callback callback)
252
base.ForAll (includeInternals, callback);
253
foreach (var c in children.Keys.ToArray ())
257
protected override void OnSizeRequested (ref Requisition requisition)
259
requisition.Width = 20;
261
int dx = (int)((double)Allocation.Width * dividerPosition) - PropertyContentLeftPadding;
264
MeasureHeight (rows, ref y);
265
requisition.Height = y;
267
foreach (var c in children)
268
c.Key.SizeRequest ();
271
protected override void OnSizeAllocated (Gdk.Rectangle allocation)
273
base.OnSizeAllocated (allocation);
275
MeasureHeight (rows, ref y);
276
if (currentEditorRow != null)
277
children [currentEditor] = currentEditorRow.EditorBounds;
278
foreach (var cr in children) {
280
cr.Key.SizeAllocate (new Gdk.Rectangle (r.X, r.Y, r.Width, r.Height));
284
public void SetAllocation (Gtk.Widget w, Gdk.Rectangle rect)
290
protected override void OnAdded (Gtk.Widget widget)
292
children.Add (widget, new Gdk.Rectangle (0,0,0,0));
293
widget.Parent = this;
297
protected override void OnRemoved (Gtk.Widget widget)
299
children.Remove (widget);
303
void MeasureHeight (IEnumerable<TableRow> rowList, ref int y)
305
heightMeasured = true;
306
Pango.Layout layout = new Pango.Layout (PangoContext);
307
foreach (var r in rowList) {
308
layout.SetText (r.Label);
310
layout.GetPixelSize (out w, out h);
312
r.EditorBounds = new Gdk.Rectangle (0, y, Allocation.Width, h + CategoryTopBottomPadding * 2);
313
y += h + CategoryTopBottomPadding * 2;
317
int dividerX = (int)((double)Allocation.Width * dividerPosition);
318
var cell = GetCell (r);
319
cell.GetSize (Allocation.Width - dividerX, out w, out eh);
320
eh = Math.Max (h + PropertyTopBottomPadding * 2, eh);
321
r.EditorBounds = new Gdk.Rectangle (dividerX + PropertyContentLeftPadding, y, Allocation.Width - dividerX - PropertyContentLeftPadding, eh);
324
if (r.ChildRows != null && (r.Expanded || r.AnimatingExpand)) {
326
MeasureHeight (r.ChildRows, ref y);
327
r.ChildrenHeight = y - py;
328
if (r.AnimatingExpand)
329
y = py + r.AnimationHeight;
335
protected override bool OnExposeEvent (EventExpose evnt)
337
using (Cairo.Context ctx = CairoHelper.Create (evnt.Window)) {
338
int dx = (int)((double)Allocation.Width * dividerPosition);
340
ctx.Rectangle (0, 0, dx, Allocation.Height);
341
ctx.Color = LabelBackgroundColor;
343
ctx.Rectangle (dx, 0, Allocation.Width - dx, Allocation.Height);
344
ctx.Color = new Cairo.Color (1, 1, 1);
346
ctx.MoveTo (dx + 0.5, 0);
347
ctx.RelLineTo (0, Allocation.Height);
348
ctx.Color = DividerColor;
352
Draw (ctx, rows, dx, PropertyLeftPadding, ref y);
354
return base.OnExposeEvent (evnt);
357
void Draw (Cairo.Context ctx, List<TableRow> rowList, int dividerX, int x, ref int y)
362
Pango.Layout layout = new Pango.Layout (PangoContext);
363
TableRow lastCategory = null;
365
foreach (var r in rowList) {
367
layout.SetText (r.Label);
368
layout.GetPixelSize (out w, out h);
372
var rh = h + CategoryTopBottomPadding*2;
373
ctx.Rectangle (0, y, Allocation.Width, rh);
374
Cairo.LinearGradient gr = new LinearGradient (0, y, 0, rh);
375
gr.AddColorStop (0, new Cairo.Color (248d/255d, 248d/255d, 248d/255d));
376
gr.AddColorStop (1, new Cairo.Color (240d/255d, 240d/255d, 240d/255d));
380
if (lastCategory == null || lastCategory.Expanded || lastCategory.AnimatingExpand) {
381
ctx.MoveTo (0, y + 0.5);
382
ctx.LineTo (Allocation.Width, y + 0.5);
384
ctx.MoveTo (0, y + rh - 0.5);
385
ctx.LineTo (Allocation.Width, y + rh - 0.5);
386
ctx.Color = DividerColor;
389
ctx.MoveTo (x, y + CategoryTopBottomPadding);
390
ctx.Color = CategoryLabelColor;
391
Pango.CairoHelper.ShowLayout (ctx, layout);
393
var img = r.Expanded ? discloseUp : discloseDown;
394
CairoHelper.SetSourcePixbuf (ctx, img, Allocation.Width - img.Width - CategoryTopBottomPadding, y + (rh - img.Height) / 2);
401
var cell = GetCell (r);
402
r.Enabled = !r.Property.IsReadOnly || cell.EditsReadOnlyObject;
403
var state = r.Enabled ? State : Gtk.StateType.Insensitive;
405
ctx.Rectangle (0, y, dividerX, h + PropertyTopBottomPadding*2);
407
ctx.MoveTo (x, y + PropertyTopBottomPadding);
408
ctx.Color = Style.Text (state).ToCairoColor ();
409
Pango.CairoHelper.ShowLayout (ctx, layout);
412
if (r != currentEditorRow)
413
cell.Render (GdkWindow, ctx, r.EditorBounds, state);
415
y += r.EditorBounds.Height;
416
indent = PropertyIndent;
419
if (r.ChildRows != null && r.ChildRows.Count > 0 && (r.Expanded || r.AnimatingExpand)) {
423
if (r.AnimatingExpand)
424
ctx.Rectangle (0, y, Allocation.Width, r.AnimationHeight);
426
ctx.Rectangle (0, 0, Allocation.Width, Allocation.Height);
429
Draw (ctx, r.ChildRows, dividerX, x + indent, ref y);
432
if (r.AnimatingExpand) {
433
y = py + r.AnimationHeight;
434
// Repaing the background because the cairo clip doesn't work for gdk primitives
435
int dx = (int)((double)Allocation.Width * dividerPosition);
436
ctx.Rectangle (0, y, dx, Allocation.Height - y);
437
ctx.Color = LabelBackgroundColor;
439
ctx.Rectangle (dx + 1, y, Allocation.Width - dx - 1, Allocation.Height - y);
440
ctx.Color = new Cairo.Color (1, 1, 1);
447
IEnumerable<TableRow> GetAllRows (bool onlyVisible)
449
return GetAllRows (rows, onlyVisible);
452
IEnumerable<TableRow> GetAllRows (IEnumerable<TableRow> rows, bool onlyVisible)
454
foreach (var r in rows) {
456
if (r.ChildRows != null && (!onlyVisible || r.Expanded || r.AnimatingExpand)) {
457
foreach (var cr in GetAllRows (r.ChildRows, onlyVisible))
463
protected override bool OnButtonPressEvent (EventButton evnt)
465
if (evnt.Type != EventType.ButtonPress)
466
return base.OnButtonPressEvent (evnt);
468
var cat = rows.FirstOrDefault (r => r.IsCategory && r.EditorBounds.Contains ((int)evnt.X, (int)evnt.Y));
470
cat.Expanded = !cat.Expanded;
472
StartExpandAnimation (cat);
474
StartCollapseAnimation (cat);
479
int dx = (int)((double)Allocation.Width * dividerPosition);
480
if (Math.Abs (dx - evnt.X) < 4) {
481
draggingDivider = true;
482
GdkWindow.Cursor = resizeCursor;
486
TableRow clickedEditor = null;
487
foreach (var r in GetAllRows (true).Where (r => !r.IsCategory)) {
488
if (r.EditorBounds.Contains ((int)evnt.X, (int)evnt.Y)) {
493
if (clickedEditor != null && clickedEditor.Enabled)
494
StartEditing (clickedEditor);
500
return base.OnButtonPressEvent (evnt);
503
protected override bool OnButtonReleaseEvent (EventButton evnt)
505
if (draggingDivider) {
506
draggingDivider = false;
509
return base.OnButtonReleaseEvent (evnt);
512
protected override bool OnMotionNotifyEvent (EventMotion evnt)
514
if (draggingDivider) {
518
else if (px > Allocation.Width - 10)
519
px = Allocation.Width - 10;
520
dividerPosition = px / (double) Allocation.Width;
525
var cat = rows.FirstOrDefault (r => r.IsCategory && r.EditorBounds.Contains ((int)evnt.X, (int)evnt.Y));
527
GdkWindow.Cursor = handCursor;
531
int dx = (int)((double)Allocation.Width * dividerPosition);
532
if (Math.Abs (dx - evnt.X) < 4) {
533
GdkWindow.Cursor = resizeCursor;
537
GdkWindow.Cursor = null;
538
return base.OnMotionNotifyEvent (evnt);
542
TooltipPopoverWindow tooltipWindow;
544
void ShowTooltip (EventMotion evnt)
547
tooltipTimeout = GLib.Timeout.Add (500, delegate {
548
ShowTooltipWindow ((int)evnt.X, (int)evnt.Y);
555
if (tooltipTimeout != 0) {
556
GLib.Source.Remove (tooltipTimeout);
559
if (tooltipWindow != null) {
560
tooltipWindow.Destroy ();
561
tooltipWindow = null;
565
void ShowTooltipWindow (int x, int y)
568
int dx = (int)((double)Allocation.Width * dividerPosition);
571
var row = GetAllRows (true).FirstOrDefault (r => !r.IsCategory && y >= r.EditorBounds.Y && y <= r.EditorBounds.Bottom);
573
tooltipWindow = new TooltipPopoverWindow ();
574
tooltipWindow.ShowArrow = true;
575
var s = "<b>" + row.Property.DisplayName + "</b>\n\n";
576
s += GLib.Markup.EscapeText (row.Property.Description);
577
tooltipWindow.Markup = s;
578
tooltipWindow.ShowPopup (this, new Gdk.Rectangle (0, row.EditorBounds.Y, Allocation.Width, row.EditorBounds.Height), PopupPosition.Right);
582
protected override void OnUnrealized ()
585
base.OnUnrealized ();
588
protected override bool OnLeaveNotifyEvent (EventCrossing evnt)
591
return base.OnLeaveNotifyEvent (evnt);
594
void StartExpandAnimation (TableRow row)
597
if (row.AnimatingExpand) {
598
GLib.Source.Remove (row.AnimationHandle);
600
row.AnimationHeight = 0;
602
row.AnimatingExpand = true;
603
row.AnimationHandle = GLib.Timeout.Add (animationTimeSpan, delegate {
604
row.AnimationHeight += animationStepSize;
606
if (row.AnimationHeight >= row.ChildrenHeight) {
607
row.AnimatingExpand = false;
614
void StartCollapseAnimation (TableRow row)
617
if (row.AnimatingExpand) {
618
GLib.Source.Remove (row.AnimationHandle);
620
row.AnimationHeight = row.ChildrenHeight;
622
row.AnimatingExpand = true;
623
row.AnimationHandle = GLib.Timeout.Add (animationTimeSpan, delegate {
624
row.AnimationHeight -= animationStepSize;
626
if (row.AnimationHeight <= 0) {
627
row.AnimatingExpand = false;
634
void StopAllAnimations ()
636
foreach (var r in GetAllRows (false)) {
637
if (r.AnimatingExpand) {
638
GLib.Source.Remove (r.AnimationHandle);
639
r.AnimatingExpand = false;
644
protected override void OnDragLeave (DragContext context, uint time_)
646
if (!draggingDivider)
647
GdkWindow.Cursor = null;
648
base.OnDragLeave (context, time_);
653
if (editSession != null) {
654
Remove (currentEditor);
655
currentEditor.Destroy ();
656
editSession.Dispose ();
658
currentEditorRow = null;
663
void StartEditing (TableRow row)
666
currentEditorRow = row;
667
var cell = GetCell (row);
668
editSession = cell.StartEditing (row.EditorBounds, State);
669
currentEditor = (Gtk.Widget) editSession.Editor;
671
SetAllocation (currentEditor, row.EditorBounds);
672
currentEditor.Show ();
673
currentEditor.CanFocus = true;
674
currentEditor.GrabFocus ();
675
ConnectTabEvent (currentEditor);
676
editSession.Changed += delegate {
678
Changed (this, EventArgs.Empty);
680
var vs = ((Gtk.Viewport)Parent).Vadjustment;
681
if (row.EditorBounds.Top < vs.Value)
682
vs.Value = row.EditorBounds.Top;
683
else if (row.EditorBounds.Bottom > vs.Value + vs.PageSize)
684
vs.Value = row.EditorBounds.Bottom - vs.PageSize;
688
void ConnectTabEvent (Gtk.Widget w)
690
w.KeyPressEvent += HandleKeyPressEvent;
691
if (w is Gtk.Container) {
692
foreach (var c in ((Gtk.Container)w).Children)
698
void HandleKeyPressEvent (object o, KeyPressEventArgs args)
700
if (args.Event.Key == Gdk.Key.Tab || args.Event.Key == Gdk.Key.ISO_Left_Tab || args.Event.Key == Gdk.Key.KP_Tab) {
701
var r = args.Event.State == ModifierType.ShiftMask ? GetPreviousRow (currentEditorRow) : GetNextRow (currentEditorRow);
703
Gtk.Application.Invoke (delegate {
711
TableRow GetNextRow (TableRow row)
714
foreach (var r in GetAllRows (true)) {
715
if (r.IsCategory || !r.Enabled)
725
TableRow GetPreviousRow (TableRow row)
727
TableRow prev = null;
728
foreach (var r in GetAllRows (true)) {
729
if (r.IsCategory || !r.Enabled)
741
public InstanceData (object instance)
746
public object Instance;