// // PropertyGridTree.cs // // Author: // Lluis Sanchez Gual // // Copyright (C) 2007 Novell, Inc (http://www.novell.com) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Collections; using System.ComponentModel; using Gtk; using Gdk; using MonoDevelop.Core; using MonoDevelop.Components.PropertyGrid.PropertyEditors; namespace MonoDevelop.Components.PropertyGrid { internal class PropertyGridTree: CompactScrolledWindow { Gtk.TreeStore store; InternalTree tree; TreeViewColumn editorColumn; Hashtable propertyRows; ArrayList collapseStatus = new ArrayList (); EditorManager editorManager; PropertyGrid parentGrid; public event EventHandler Changed; private PropertySort propertySort = PropertySort.Categorized; public PropertyGridTree (EditorManager editorManager, PropertyGrid parentGrid) { this.editorManager = editorManager; this.parentGrid = parentGrid; propertyRows = new Hashtable (); store = new TreeStore (typeof (string), typeof(object), typeof(bool), typeof(object)); tree = new InternalTree (this, store); CellRendererText crt; TreeViewColumn col; col = new TreeViewColumn (); col.Title = GettextCatalog.GetString ("Property"); crt = new CellRendererPropertyGroup (tree); crt.Xpad = 0; col.PackStart (crt, true); col.SetCellDataFunc (crt, new TreeCellDataFunc (GroupData)); col.Resizable = true; col.Expand = false; col.Sizing = TreeViewColumnSizing.Fixed; col.FixedWidth = 180; tree.AppendColumn (col); editorColumn = new TreeViewColumn (); editorColumn.Title = GettextCatalog.GetString ("Value"); CellRendererProperty crp = new CellRendererProperty (tree); editorColumn.PackStart (crp, true); editorColumn.SetCellDataFunc (crp, new TreeCellDataFunc (PropertyData)); editorColumn.Sizing = TreeViewColumnSizing.Fixed; editorColumn.Resizable = false; editorColumn.Expand = true; tree.AppendColumn (editorColumn); tree.HeadersVisible = false; this.ShadowType = Gtk.ShadowType.None; this.HscrollbarPolicy = Gtk.PolicyType.Never; Add (tree); ShowAll (); tree.Selection.Changed += OnSelectionChanged; } public void CommitChanges () { tree.Selection.UnselectAll (); } public void SaveStatus () { collapseStatus.Clear (); TreeIter iter; if (!tree.Model.GetIterFirst (out iter)) return; do { if (!tree.GetRowExpanded (tree.Model.GetPath (iter))) { collapseStatus.Add (tree.Model.GetValue (iter, 0)); } } while (tree.Model.IterNext (ref iter)); } public void RestoreStatus () { TreeIter iter; if (!tree.Model.GetIterFirst (out iter)) return; // If the tree only has one group, show it always expanded TreeIter iter2 = iter; if (!tree.Model.IterNext (ref iter2)) { tree.ExpandRow (tree.Model.GetPath (iter), true); return; } do { object grp = tree.Model.GetValue (iter, 0); if (!collapseStatus.Contains (grp)) tree.ExpandRow (tree.Model.GetPath (iter), false); } while (tree.Model.IterNext (ref iter)); } public virtual void Clear () { store.Clear (); propertyRows.Clear (); } public virtual void Update () { // Just repaint the cells QueueDraw (); } public PropertySort PropertySort { get { return propertySort; } set { propertySort = value; } } internal void Populate (PropertyDescriptorCollection properties, object instance) { bool categorised = PropertySort == PropertySort.Categorized; //transcribe browsable properties ArrayList sorted = new ArrayList(); foreach (PropertyDescriptor descriptor in properties) if (descriptor.IsBrowsable) sorted.Add (descriptor); if (sorted.Count == 0) return; InstanceData idata = new InstanceData (instance); if (!categorised) { if (PropertySort != PropertySort.NoSort) sorted.Sort(new SortByName ()); foreach (PropertyDescriptor pd in sorted) AppendProperty (TreeIter.Zero, pd, idata); } else { sorted.Sort (new SortByCat ()); string oldCat = null; TreeIter catIter = TreeIter.Zero; foreach (PropertyDescriptor pd in sorted) { if (pd.Category != oldCat) { catIter = store.AppendValues (pd.Category, null, true, idata); oldCat = pd.Category; } AppendProperty (catIter, pd, idata); } } } internal void Update (PropertyDescriptorCollection properties, object instance) { foreach (PropertyDescriptor pd in properties) { TreeIter it; if (!store.GetIterFirst (out it)) continue; UpdateProperty (pd, it, instance); } } bool UpdateProperty (PropertyDescriptor pd, TreeIter it, object instance) { do { PropertyDescriptor prop = (PropertyDescriptor) store.GetValue (it, 1); InstanceData idata = (InstanceData) store.GetValue (it, 3); if (prop != null && idata != null && prop.Name == pd.Name && idata.Instance == instance) { // Don't update the current editing node, since it may cause tree update problems if (!tree.Editing || !store.GetPath (tree.EditingIter).Equals (store.GetPath (it))) store.SetValue (it, 1, pd); return true; } TreeIter ci; if (store.IterChildren (out ci, it)) { if (UpdateProperty (pd, ci, instance)) return true; } } while (store.IterNext (ref it)); return false; } protected void AppendProperty (PropertyDescriptor prop, object instance) { AppendProperty (TreeIter.Zero, prop, new InstanceData (instance)); } protected void AppendProperty (TreeIter piter, PropertyDescriptor prop, object instance) { AppendProperty (piter, prop, new InstanceData (instance)); } void AppendProperty (TreeIter piter, PropertyDescriptor prop, InstanceData idata) { TreeIter iter; if (piter.Equals (TreeIter.Zero)) iter = store.AppendValues (prop.DisplayName, prop, false, idata); else iter = store.AppendValues (piter, prop.DisplayName, prop, false, idata); propertyRows [prop] = store.GetStringFromIter (iter); TypeConverter tc = prop.Converter; if (typeof (ExpandableObjectConverter).IsAssignableFrom (tc.GetType ())) { object cob = prop.GetValue (idata.Instance); foreach (PropertyDescriptor cprop in TypeDescriptor.GetProperties (cob)) AppendProperty (iter, cprop, cob); } } protected virtual void OnObjectChanged () { // Delay the notification of the change. There may be problems if the // handler of this event starts its own gui loop. GLib.Timeout.Add (0, delegate { if (Changed != null) Changed (this, EventArgs.Empty); return false; }); } void OnSelectionChanged (object s, EventArgs a) { TreePath[] rows = tree.Selection.GetSelectedRows (); if (!tree.dragging && rows != null && rows.Length > 0) { tree.SetCursor (rows[0], editorColumn, true); } TreeIter iter; if (tree.Selection.GetSelected (out iter)) { PropertyDescriptor prop = (PropertyDescriptor) store.GetValue (iter, 1); if (prop != null) parentGrid.SetHelp (prop.DisplayName, prop.Description); else parentGrid.SetHelp (string.Empty, string.Empty); } else { parentGrid.SetHelp (string.Empty, string.Empty); } } internal void NotifyChanged () { OnObjectChanged (); } void PropertyData (Gtk.TreeViewColumn tree_column, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter) { CellRendererProperty rc = (CellRendererProperty) cell; bool group = (bool) model.GetValue (iter, 2); if (group) { rc.SetData (null, null, null); } else { PropertyDescriptor prop = (PropertyDescriptor) model.GetValue (iter, 1); PropertyEditorCell propCell = editorManager.GetEditor (prop); InstanceData idata = (InstanceData) model.GetValue (iter, 3); propCell.Initialize (tree, editorManager, prop, idata.Instance); rc.SetData (idata.Instance, prop, propCell); } } void GroupData (Gtk.TreeViewColumn tree_column, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter) { CellRendererPropertyGroup rc = (CellRendererPropertyGroup) cell; rc.IsGroup = (bool) model.GetValue (iter, 2); rc.Text = (string) model.GetValue (iter, 0); PropertyDescriptor prop = (PropertyDescriptor) model.GetValue (iter, 1); if (prop != null) rc.SensitiveProperty = !prop.IsReadOnly; else rc.SensitiveProperty = true; } private class SortByCat : IComparer { public int Compare (object x, object y) { int catcomp = ((PropertyDescriptor)x).Category.CompareTo (((PropertyDescriptor)y).Category); if (catcomp == 0) return ((PropertyDescriptor)x).DisplayName.CompareTo (((PropertyDescriptor)y).DisplayName); else return catcomp; } } private class SortByName : IComparer { public int Compare(object x, object y) { return ((PropertyDescriptor)x).DisplayName.CompareTo (((PropertyDescriptor)y).DisplayName); } } } class InternalTree: TreeView { internal ArrayList Groups = new ArrayList (); Pango.Layout layout; bool editing; TreeIter editingIter; PropertyGridTree tree; internal bool dragging; int dragPos; Gdk.Cursor resizeCursor = new Gdk.Cursor (CursorType.SbHDoubleArrow); public InternalTree (PropertyGridTree tree, TreeModel model): base (model) { this.tree = tree; layout = new Pango.Layout (this.PangoContext); layout.Wrap = Pango.WrapMode.Char; } public bool Editing { get { return editing; } set { editing = value; } } public TreeIter EditingIter { get { return editingIter; } set { editingIter = value; } } public PropertyGridTree PropertyTree { get { return tree; } } protected override bool OnExposeEvent (Gdk.EventExpose e) { Groups.Clear (); bool res = base.OnExposeEvent (e); foreach (TreeGroup grp in Groups) { layout.SetMarkup ("" + grp.Group + ""); e.Window.DrawLayout (this.Style.TextGC (grp.State), grp.X, grp.Y, layout); } return res; } protected override void OnDestroyed () { base.OnDestroyed (); if (resizeCursor != null) { resizeCursor.Dispose (); resizeCursor = null; } } protected override bool OnMotionNotifyEvent (EventMotion evnt) { if (dragging) { int nw = (int)(evnt.X) + dragPos; if (nw <= 40) nw = 40; GLib.Idle.Add (delegate { Columns[0].FixedWidth = nw; return false; }); } else { int w = Columns[0].Width; if (Math.Abs (w - evnt.X) < 5) this.GdkWindow.Cursor = resizeCursor; else this.GdkWindow.Cursor = null; } return base.OnMotionNotifyEvent (evnt); } protected override bool OnButtonPressEvent (EventButton evnt) { int w = Columns[0].Width; if (Math.Abs (w - evnt.X) < 5) { TreePath[] rows = Selection.GetSelectedRows (); if (rows != null && rows.Length > 0) SetCursor (rows[0], Columns[0], false); dragging = true; dragPos = w - (int) evnt.X; this.GdkWindow.Cursor = resizeCursor; } return base.OnButtonPressEvent (evnt); } protected override bool OnButtonReleaseEvent (EventButton evnt) { if (dragging) { this.GdkWindow.Cursor = null; dragging = false; } return base.OnButtonReleaseEvent (evnt); } public virtual void Update () { } } class TreeGroup { public string Group; public int X; public int Y; public StateType State; } class CellRendererProperty: CellRenderer { TreeView tree; PropertyDescriptor property; object instance; int rowHeight; PropertyEditorCell editorCell; bool sensitive = true; bool visible = true; public CellRendererProperty (TreeView tree) { this.tree = tree; Xalign = 0; Xpad = 3; Mode |= Gtk.CellRendererMode.Editable; Entry dummyEntry = new Gtk.Entry (); dummyEntry.HasFrame = false; rowHeight = dummyEntry.SizeRequest ().Height; } public void SetData (object instance, PropertyDescriptor property, PropertyEditorCell editor) { this.instance = instance; this.property = property; if (property == null) { this.CellBackgroundGdk = tree.Style.MidColors [(int) Gtk.StateType.Normal]; sensitive = true; } else { this.CellBackground = null; sensitive = !property.IsReadOnly || (editor != null && editor.EditsReadOnlyObject); } editorCell = editor; } public override void GetSize (Widget widget, ref Rectangle cell_area, out int x_offset, out int y_offset, out int width, out int height) { if (editorCell != null) editorCell.GetSize ((int)(cell_area.Width - this.Xpad * 2), out width, out height); else { width = height = 0; } width += (int) this.Xpad * 2; height += (int) this.Ypad * 2; x_offset = 0; y_offset = 0; if (height < rowHeight) height = rowHeight; } protected override void Render (Drawable window, Widget widget, Rectangle background_area, Rectangle cell_area, Rectangle expose_area, CellRendererState flags) { if (instance == null || !visible) return; int width = 0, height = 0; int iwidth = cell_area.Width - (int) this.Xpad * 2; if (editorCell != null) editorCell.GetSize ((int)(cell_area.Width - this.Xpad * 2), out width, out height); Rectangle bounds = new Rectangle (); bounds.Width = width > iwidth ? iwidth : width; bounds.Height = height; bounds.X = (int) (cell_area.X + this.Xpad); bounds.Y = cell_area.Y + (cell_area.Height - height) / 2; StateType state = GetState (flags); if (editorCell != null) editorCell.Render (window, bounds, state); } public override CellEditable StartEditing (Gdk.Event ev, Widget widget, string path, Gdk.Rectangle background_area, Gdk.Rectangle cell_area, CellRendererState flags) { if (property == null || editorCell == null || !sensitive) return null; StateType state = GetState (flags); EditSession session = editorCell.StartEditing (cell_area, state); if (session == null) return null; Gtk.Widget propEditor = (Gtk.Widget) session.Editor; propEditor.Show (); HackEntry e = new HackEntry (session, propEditor); e.Show (); session.Changed += delegate { ((InternalTree)widget).PropertyTree.NotifyChanged (); }; TreeIter it; ((InternalTree)widget).Model.GetIterFromString (out it, path); ((InternalTree)widget).EditingIter = it; return e; } StateType GetState (CellRendererState flags) { if (!sensitive) return StateType.Insensitive; else if ((flags & CellRendererState.Selected) != 0) return StateType.Selected; else return StateType.Normal; } } class CellRendererPropertyGroup: CellRendererText { TreeView tree; Pango.Layout layout; bool isGroup; bool sensitive = true; public bool IsGroup { get { return isGroup; } set { isGroup = value; if (value) this.CellBackgroundGdk = tree.Style.MidColors [(int) Gtk.StateType.Normal]; else this.CellBackground = null; } } public bool SensitiveProperty { get { return sensitive; } set { sensitive = value; } } public CellRendererPropertyGroup (TreeView tree) { this.tree = tree; layout = new Pango.Layout (tree.PangoContext); layout.Wrap = Pango.WrapMode.Char; } protected void GetCellSize (Widget widget, int availableWidth, out int width, out int height) { layout.SetMarkup (Text); layout.Width = -1; layout.GetPixelSize (out width, out height); } public override void GetSize (Widget widget, ref Rectangle cell_area, out int x_offset, out int y_offset, out int width, out int height) { GetCellSize (widget, (int)(cell_area.Width - this.Xpad * 2), out width, out height); width += (int) this.Xpad * 2; height += (int) this.Ypad * 2; x_offset = y_offset = 0; if (IsGroup) width = 0; } protected override void Render (Drawable window, Widget widget, Rectangle background_area, Rectangle cell_area, Rectangle expose_area, CellRendererState flags) { int width, height; GetCellSize (widget, (int)(cell_area.Width - this.Xpad * 2), out width, out height); int x = (int) (cell_area.X + this.Xpad); int y = cell_area.Y + (cell_area.Height - height) / 2; StateType state; if (!sensitive) state = StateType.Insensitive; else if ((flags & CellRendererState.Selected) != 0) state = StateType.Selected; else state = StateType.Normal; if (IsGroup) { TreeGroup grp = new TreeGroup (); grp.X = x; grp.Y = y; grp.Group = Text; grp.State = state; InternalTree tree = (InternalTree) widget; tree.Groups.Add (grp); } else { window.DrawLayout (widget.Style.TextGC (state), x, y, layout); int bx = background_area.X + background_area.Width - 1; Gdk.GC gc = new Gdk.GC (window); gc.RgbFgColor = tree.Style.MidColors [(int) Gtk.StateType.Normal]; window.DrawLine (gc, bx, background_area.Y, bx, background_area.Y + background_area.Height); } } } class HackEntry: Entry { EventBox box; EditSession session; public HackEntry (EditSession session, Gtk.Widget child) { this.session = session; box = new EventBox (); box.ButtonPressEvent += new ButtonPressEventHandler (OnClickBox); box.ModifyBg (StateType.Normal, Style.White); box.Add (child); } [GLib.ConnectBefore] void OnClickBox (object s, ButtonPressEventArgs args) { // Avoid forwarding the button press event to the // tree, since it would hide the cell editor. args.RetVal = true; } protected override void OnParentSet (Gtk.Widget parent) { base.OnParentSet (parent); if (Parent != null) { if (this.ParentWindow != null) box.ParentWindow = this.ParentWindow; box.Parent = Parent; box.Show (); ((InternalTree)Parent).Editing = true; } else { session.Dispose (); ((InternalTree)parent).Editing = false; if (box.Parent != null && box.Parent.IsRealized) box.Unparent (); } } protected override void OnShown () { // Do nothing. } protected override void OnSizeAllocated (Gdk.Rectangle allocation) { base.OnSizeAllocated (allocation); box.SizeRequest (); box.Allocation = allocation; } } class InstanceData { public InstanceData (object instance) { Instance = instance; } public object Instance; } }