// // GraphicObject.cs // // Author: // Jean-Philippe Bruyère // // Copyright (c) 2013-2017 Jean-Philippe Bruyère // // 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.Generic; using System.ComponentModel; using System.Xml.Serialization; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using Cairo; using System.Diagnostics; using Crow.IML; namespace Crow { /// /// The base class for all the graphic tree elements. /// public class GraphicObject : ILayoutable, IValueChange, IDisposable { #region IDisposable implementation protected bool disposed = false; public void Dispose(){ Dispose(true); GC.SuppressFinalize(this); } ~GraphicObject(){ Dispose(false); } protected virtual void Dispose(bool disposing){ if (disposed){ #if DEBUG_DISPOSE Debug.WriteLine ("Trying to dispose already disposed obj: {0}", this.ToString()); #endif return; } if (disposing) { #if DEBUG_DISPOSE Debug.WriteLine ("Disposing: {0}", this.ToString()); if (IsQueueForRedraw) throw new Exception("Trying to dispose an object queued for Redraw: " + this.ToString()); #endif if (CurrentInterface.HoverWidget != null) { if (CurrentInterface.HoverWidget.IsOrIsInside(this)) CurrentInterface.HoverWidget = null; } if (CurrentInterface.ActiveWidget != null) { if (CurrentInterface.ActiveWidget.IsOrIsInside (this)) CurrentInterface.ActiveWidget = null; } if (CurrentInterface.FocusedWidget != null) { if (CurrentInterface.FocusedWidget.IsOrIsInside (this)) CurrentInterface.FocusedWidget = null; } if (!localDataSourceIsNull) DataSource = null; parent = null; } else Debug.WriteLine ("!!! Finalized by GC: {0}", this.ToString ()); Clipping?.Dispose (); bmp?.Dispose (); disposed = true; } #endregion internal static ulong currentUid = 0; internal ulong uid = 0; internal bool isPopup = false; internal GraphicObject focusParent { get { return (isPopup ? LogicalParent : parent) as GraphicObject; } } /// /// interface this widget is bound to, this should not be changed once the instance is created /// public Interface CurrentInterface = null; /// /// contains the dirty rectangles in the coordinate system of the cache. those dirty zones /// are repeated at each cached levels of the tree with correspondig coordinate system. This is done /// in a dedicated step of the update between layouting and drawing. /// public Region Clipping; #region IValueChange implementation /// /// Raise to notify that the value of a property has changed, the binding system /// rely mainly on this event. the member name may not be present in the class, this is /// used in **propertyless** bindings, this allow to raise custom named events without needing /// to create an new one in the class or a new property. /// public event EventHandler ValueChanged; /// /// Helper function to raise the value changed event /// public virtual void NotifyValueChanged(string MemberName, object _value) { //Debug.WriteLine ("Value changed: {0}->{1} = {2}", this, MemberName, _value); ValueChanged.Raise(this, new ValueChangeEventArgs(MemberName, _value)); } #endregion #region CTOR /// /// default private parameter less constructor use in instantiators, it should not be used /// when creating widget from code because widgets has to be bound to an interface before any other /// action. /// protected GraphicObject () { Clipping = new Region (); #if DEBUG uid = currentUid; currentUid++; #endif } /// /// This constructor **must** be used when creating widget from code. /// /// When creating new widgets derived from GraphicObject, both parameterless and this constructors are /// facultatives, the compiler will create the parameterless one automaticaly if no other one exists. /// But if you intend to be able to create instances of the new widget in code and override the constructor /// with the Interface parameter, you **must** also provide the override of the parameterless constructor because /// compiler will not create it automatically because of the presence of the other one. /// /// Iface. public GraphicObject (Interface iface) : this() { CurrentInterface = iface; Initialize (); } #endregion internal bool initialized = false; /// /// Initialize this Graphic object instance by setting style and default values and loading template if required /// public virtual void Initialize(){ loadDefaultValues (); initialized = true; } #region private fields LayoutingType registeredLayoutings = LayoutingType.All; ILayoutable logicalParent; ILayoutable parent; string name; Fill background = Color.Transparent; Fill foreground = Color.White; Font font = "droid, 10"; Measure width, height; int left, top; double cornerRadius = 0; int margin = 0; bool focusable = false; bool hasFocus = false; bool isActive = false; bool mouseRepeat; protected bool isVisible = true; bool isEnabled = true; VerticalAlignment verticalAlignment = VerticalAlignment.Center; HorizontalAlignment horizontalAlignment = HorizontalAlignment.Center; Size maximumSize = "0,0"; Size minimumSize = "0,0"; bool cacheEnabled = false; bool clipToClientRect = true; protected object dataSource; string style; object tag; bool isDragged; bool allowDrag; bool allowDrop; string tooltip; IList contextCommands; #endregion #region public fields /// /// Current size and position computed during layouting pass /// public Rectangle Slot = new Rectangle (); /// /// keep last slot components for each layouting pass to track /// changes and trigger update of other component accordingly /// public Rectangle LastSlots; /// /// keep last slot painted on screen to clear traces if moved or resized /// version to clear effective oldslot if parents have been moved or resized. /// IDEA is to add a ScreenCoordinates function that use only lastPaintedSlots /// //TODO: we should ensure the whole parsed widget tree is the last painted public Rectangle LastPaintedSlot; /// Prevent requeuing multiple times the same widget public bool IsQueueForRedraw = false; /// drawing Cache, if null, a redraw is done, cached or not public Surface bmp; public bool IsDirty = true; /// /// This size is computed on each child' layout changes. /// In stacking widget, it is used to compute the remaining space for the stretched /// widget inside the stack, which is never added to the contentSize, instead, its size /// is deducted from (parent.ClientRectangle - contentSize) /// internal Size contentSize; #endregion #region ILayoutable [XmlIgnore]public LayoutingType RegisteredLayoutings { get { return registeredLayoutings; } set { registeredLayoutings = value; } } //TODO: it would save the recurent cost of a cast in event bubbling if parent type was GraphicObject // or we could add to the interface the mouse events /// /// Parent in the graphic tree, used for rendering and layouting /// [XmlIgnore]public virtual ILayoutable Parent { get { return parent; } set { if (parent == value) return; DataSourceChangeEventArgs e = new DataSourceChangeEventArgs (parent, value); lock (CurrentInterface.LayoutMutex) { parent = value; } onParentChanged (this, e); } } [XmlIgnore]public ILayoutable LogicalParent { get { return logicalParent == null ? Parent : logicalParent; } set { if (logicalParent == value) return; if (logicalParent != null) (logicalParent as GraphicObject).DataSourceChanged -= onLogicalParentDataSourceChanged; DataSourceChangeEventArgs dsce = new DataSourceChangeEventArgs (LogicalParent, null); logicalParent = value; dsce.NewDataSource = LogicalParent; if (logicalParent != null) (logicalParent as GraphicObject).DataSourceChanged += onLogicalParentDataSourceChanged; onLogicalParentChanged (this, dsce); } } [XmlIgnore]public virtual Rectangle ClientRectangle { get { Rectangle cb = Slot.Size; cb.Inflate ( - Margin); return cb; } } public virtual Rectangle ContextCoordinates(Rectangle r){ GraphicObject go = Parent as GraphicObject; if (go == null) return r + Parent.ClientRectangle.Position; return go.CacheEnabled ? r + Parent.ClientRectangle.Position : Parent.ContextCoordinates (r); } public virtual Rectangle ScreenCoordinates (Rectangle r){ return Parent.ScreenCoordinates(r) + Parent.getSlot().Position + Parent.ClientRectangle.Position; } public virtual Rectangle getSlot () { return Slot;} #endregion #region EVENT HANDLERS /// Occurs when mouse wheel is rolled in this object. It bubbles to the root public event EventHandler MouseWheelChanged; /// Occurs when mouse button is released in this object. It bubbles to the root public event EventHandler MouseUp; /// Occurs when mouse button is pressed in this object. It bubbles to the root public event EventHandler MouseDown; /// Occurs when mouse button has been pressed then relesed in this object. It bubbles to the root public event EventHandler MouseClick; /// Occurs when mouse button has been pressed then relesed 2 times in this object. It bubbles to the root public event EventHandler MouseDoubleClick; /// Occurs when mouse mouve in this object. It bubbles to the root public event EventHandler MouseMove; /// Occurs when mouse enter this object public event EventHandler MouseEnter; /// Occurs when mouse leave this object public event EventHandler MouseLeave; /// Occurs when key is pressed when this object is active public event EventHandler KeyDown; /// Occurs when key is released when this object is active public event EventHandler KeyUp; /// Occurs when translated key event occurs in the host when this object is active public event EventHandler KeyPress; /// Occurs when this object received focus public event EventHandler Focused; /// Occurs when this object loose focus public event EventHandler Unfocused; /// Occurs when the enabled state this object is set to true public event EventHandler Enabled; /// Occurs when the enabled state this object is set to false public event EventHandler Disabled; public event EventHandler StartDrag; public event EventHandler DragEnter; public event EventHandler DragLeave; public event EventHandler EndDrag; public event EventHandler Drop; /// Occurs when one part of the rendering slot changed public event EventHandler LayoutChanged; /// Occurs when DataSource changed public event EventHandler DataSourceChanged; /// Occurs when the parent has changed public event EventHandler ParentChanged; /// Occurs when the logical parent has changed public event EventHandler LogicalParentChanged; #endregion #region public properties /// Random value placeholder [XmlAttributeAttribute] public object Tag { get { return tag; } set { if (tag == value) return; tag = value; NotifyValueChanged ("Tag", tag); } } /// /// If enabled, resulting bitmap of graphic object is cached /// speeding up rendering of complex object. Default is enabled. /// [XmlAttributeAttribute][DefaultValue(true)] public virtual bool CacheEnabled { get { return cacheEnabled; } set { if (cacheEnabled == value) return; cacheEnabled = value; NotifyValueChanged ("CacheEnabled", cacheEnabled); } } /// /// If true, rendering of GraphicObject is clipped inside client rectangle /// [XmlAttributeAttribute][DefaultValue(true)] public virtual bool ClipToClientRect { get { return clipToClientRect; } set { if (clipToClientRect == value) return; clipToClientRect = value; NotifyValueChanged ("ClipToClientRect", clipToClientRect); this.RegisterForRedraw (); } } #if DEBUG [XmlIgnore]public string TreePath { get { return this.GetType().Name + uid.ToString (); } } #endif /// /// Name is used in binding to reference other GraphicObjects inside the graphic tree /// and by template controls to find special element in their template implementation such /// as a container or a group to put children in. /// [XmlAttributeAttribute][DefaultValue(null)] public virtual string Name { get { #if DEBUG return string.IsNullOrEmpty(name) ? this.GetType().Name + uid.ToString () : name; #else return name; #endif } set { if (name == value) return; name = value; NotifyValueChanged("Name", name); } } /// /// Vertical alignment inside parent, disabled if height is stretched /// or top coordinate is not null /// [XmlAttributeAttribute ()][DefaultValue(VerticalAlignment.Center)] public virtual VerticalAlignment VerticalAlignment { get { return verticalAlignment; } set { if (verticalAlignment == value) return; verticalAlignment = value; NotifyValueChanged("VerticalAlignment", verticalAlignment); RegisterForLayouting (LayoutingType.Y); } } /// /// Horizontal alignment inside parent, disabled if width is stretched /// or left coordinate is not null /// [XmlAttributeAttribute()][DefaultValue(HorizontalAlignment.Center)] public virtual HorizontalAlignment HorizontalAlignment { get { return horizontalAlignment; } set { if (horizontalAlignment == value) return; horizontalAlignment = value; NotifyValueChanged("HorizontalAlignment", horizontalAlignment); RegisterForLayouting (LayoutingType.X); } } /// /// x position inside parent /// [XmlAttributeAttribute()][DefaultValue(0)] public virtual int Left { get { return left; } set { if (left == value) return; left = value; NotifyValueChanged ("Left", left); this.RegisterForLayouting (LayoutingType.X); } } /// /// y position inside parent /// [XmlAttributeAttribute()][DefaultValue(0)] public virtual int Top { get { return top; } set { if (top == value) return; top = value; NotifyValueChanged ("Top", top); this.RegisterForLayouting (LayoutingType.Y); } } /// /// Helper property used to set width and height to fit in one call /// [XmlAttributeAttribute()][DefaultValue(false)] public virtual bool Fit { get { return Width == Measure.Fit && Height == Measure.Fit ? true : false; } set { if (value == Fit) return; Width = Height = Measure.Fit; } } /// /// Width of this control, by default inherited from parent. May have special values /// such as Stretched or Fit. It may be proportionnal or absolute. /// [XmlAttributeAttribute()][DefaultValue("Inherit")] public virtual Measure Width { get { return width.Units == Unit.Inherit ? Parent is GraphicObject ? (Parent as GraphicObject).WidthPolicy : Measure.Stretched : width; } set { if (width == value) return; if (value.IsFixed) { if (value < MinimumSize.Width || (value > MaximumSize.Width && MaximumSize.Width > 0)) return; } Measure lastWP = WidthPolicy; width = value; NotifyValueChanged ("Width", width); if (WidthPolicy != lastWP) { NotifyValueChanged ("WidthPolicy", WidthPolicy); //contentSize in Stacks are only update on childLayoutChange, and the single stretched //child of the stack is not counted in contentSize, so when changing size policy of a child //we should adapt contentSize //TODO:check case when child become stretched, and another stretched item already exists. if (parent is GenericStack) {//TODO:check if I should test Group instead if ((parent as GenericStack).Orientation == Orientation.Horizontal) { if (lastWP == Measure.Fit) (parent as GenericStack).contentSize.Width -= this.LastSlots.Width; else (parent as GenericStack).contentSize.Width += this.LastSlots.Width; } } } this.RegisterForLayouting (LayoutingType.Width); } } /// /// Height of this control, by default inherited from parent. May have special values /// such as Stretched or Fit. It may be proportionnal or absolute. /// [XmlAttributeAttribute()][DefaultValue("Inherit")] public virtual Measure Height { get { return height.Units == Unit.Inherit ? Parent is GraphicObject ? (Parent as GraphicObject).HeightPolicy : Measure.Stretched : height; } set { if (height == value) return; if (value.IsFixed) { if (value < MinimumSize.Height || (value > MaximumSize.Height && MaximumSize.Height > 0)) return; } Measure lastHP = HeightPolicy; height = value; NotifyValueChanged ("Height", height); if (HeightPolicy != lastHP) { NotifyValueChanged ("HeightPolicy", HeightPolicy); if (parent is GenericStack) { if ((parent as GenericStack).Orientation == Orientation.Vertical) { if (lastHP == Measure.Fit) (parent as GenericStack).contentSize.Height -= this.LastSlots.Height; else (parent as GenericStack).contentSize.Height += this.LastSlots.Height; } } } this.RegisterForLayouting (LayoutingType.Height); } } /// /// Used for binding on dimensions, this property will never hold fixed size, but instead only /// Fit or Stretched, **with inherited state implementation, it is not longer used in binding** /// [XmlIgnore]public virtual Measure WidthPolicy { get { return Width.IsFit ? Measure.Fit : Measure.Stretched; } } /// /// Used for binding on dimensions, this property will never hold fixed size, but instead only /// Fit or Stretched, **with inherited state implementation, it is not longer used in binding** /// [XmlIgnore]public virtual Measure HeightPolicy { get { return Height.IsFit ? Measure.Fit : Measure.Stretched; } } /// /// Indicate that this object may received focus or not, if not focusable all the descendants are /// affected. /// [XmlAttributeAttribute()][DefaultValue(false)] public virtual bool Focusable { get { return focusable; } set { if (focusable == value) return; focusable = value; NotifyValueChanged ("Focusable", focusable); } } /// /// True when this control has the focus, only one control per interface may have it. /// [XmlIgnore]public virtual bool HasFocus { get { return hasFocus; } set { if (value == hasFocus) return; hasFocus = value; if (hasFocus) onFocused (this, null); else onUnfocused (this, null); NotifyValueChanged ("HasFocus", hasFocus); } } /// /// true if this control is active, this means that mouse has been pressed in it and not yet released. It could /// be used for other two states periferic action. /// [XmlIgnore]public virtual bool IsActive { get { return isActive; } set { if (value == isActive) return; isActive = value; NotifyValueChanged ("IsActive", isActive); } } /// /// true if holding mouse button down should trigger multiple click events /// [XmlAttributeAttribute()][DefaultValue(false)] public virtual bool MouseRepeat { get { return mouseRepeat; } set { if (mouseRepeat == value) return; mouseRepeat = value; NotifyValueChanged ("MouseRepeat", mouseRepeat); } } bool clearBackground = false; /// /// background fill of the control, maybe solid color, gradient, image, or svg /// [XmlAttributeAttribute()][DefaultValue("Transparent")] public virtual Fill Background { get { return background; } set { if (background == value) return; clearBackground = false; if (value == null) return; background = value; NotifyValueChanged ("Background", background); RegisterForRedraw (); if (background is SolidColor) { if ((Background as SolidColor).Equals (Color.Clear)) clearBackground = true; } } } /// /// Foreground fill of the control, usage may be different among derived controls /// [XmlAttributeAttribute()][DefaultValue("White")] public virtual Fill Foreground { get { return foreground; } set { if (foreground == value) return; foreground = value; NotifyValueChanged ("Foreground", foreground); RegisterForRedraw (); } } /// /// Font being used in many controls, it is defined in the base GraphicObject class. /// [XmlAttributeAttribute()][DefaultValue("sans,10")] public virtual Font Font { get { return font; } set { if (value == font) return; font = value; NotifyValueChanged ("Font", font); RegisterForGraphicUpdate (); } } /// /// to get rounded corners /// [XmlAttributeAttribute()][DefaultValue(0.0)] public virtual double CornerRadius { get { return cornerRadius; } set { if (value == cornerRadius) return; cornerRadius = value; NotifyValueChanged ("CornerRadius", cornerRadius); RegisterForRedraw (); } } /// /// This is a single integer for the 4 direction, a gap between the control and it's container, /// by default it is filled with the background. /// [XmlAttributeAttribute()][DefaultValue(0)] public virtual int Margin { get { return margin; } set { if (value == margin) return; margin = value; NotifyValueChanged ("Margin", margin); RegisterForGraphicUpdate (); } } /// /// set the visible state of the control, invisible controls does reserve space in the layouting system. /// [XmlAttributeAttribute][DefaultValue(true)] public virtual bool Visible { get { return isVisible; } set { if (value == isVisible) return; isVisible = value; RegisterForLayouting (LayoutingType.Sizing); //trigger a mouse to handle possible hover changes //CurrentInterface.ProcessMouseMove (CurrentInterface.Mouse.X, CurrentInterface.Mouse.Y); NotifyValueChanged ("Visible", isVisible); } } /// /// get or set the enabled state, disabling a control will affect focuability and /// also it's rendering which will be grayed /// [XmlAttributeAttribute][DefaultValue(true)] public virtual bool IsEnabled { get { return isEnabled; } set { if (value == isEnabled) return; isEnabled = value; if (isEnabled) onEnable (this, null); else onDisable (this, null); NotifyValueChanged ("IsEnabled", isEnabled); RegisterForRedraw (); } } /// /// Minimal width and height for this control /// [XmlAttributeAttribute()][DefaultValue("1,1")] public virtual Size MinimumSize { get { return minimumSize; } set { if (value == minimumSize) return; minimumSize = value; NotifyValueChanged ("MinimumSize", minimumSize); RegisterForLayouting (LayoutingType.Sizing); } } /// /// Maximum width and height for this control, unlimited if null. /// [XmlAttributeAttribute()][DefaultValue("0,0")] public virtual Size MaximumSize { get { return maximumSize; } set { if (value == maximumSize) return; maximumSize = value; NotifyValueChanged ("MaximumSize", maximumSize); RegisterForLayouting (LayoutingType.Sizing); } } /// /// Seek first logical tree upward if logicalParent is set, or seek graphic tree for /// a not null dataSource that will be active for all descendants having dataSource=null /// [XmlAttributeAttribute]//[DefaultValue(null)] public virtual object DataSource { set { if (DataSource == value) return; DataSourceChangeEventArgs dse = new DataSourceChangeEventArgs (DataSource, null); dataSource = value; dse.NewDataSource = DataSource; //prevent setting null causing stack overflow in specific case if (dse.NewDataSource == dse.OldDataSource) return; lock (CurrentInterface.LayoutMutex) { OnDataSourceChanged (this, dse); } NotifyValueChanged ("DataSource", DataSource); } get { return dataSource == null ? LogicalParent == null ? null : LogicalParent is GraphicObject ? (LogicalParent as GraphicObject).DataSource : null : dataSource; } } protected virtual void onLogicalParentDataSourceChanged(object sender, DataSourceChangeEventArgs e){ if (localDataSourceIsNull) OnDataSourceChanged (this, e); } internal bool localDataSourceIsNull { get { return dataSource == null; } } public bool localLogicalParentIsNull { get { return logicalParent == null; } } public virtual void OnDataSourceChanged(object sender, DataSourceChangeEventArgs e){ DataSourceChanged.Raise (this, e); #if DEBUG_BINDING Debug.WriteLine("New DataSource for => {0} \n\t{1}=>{2}", this.ToString(),e.OldDataSource,e.NewDataSource); #endif } /// /// Style key to use for this control /// [XmlAttributeAttribute] public virtual string Style { get { return style; } set { if (value == style) return; style = value; NotifyValueChanged ("Style", style); } } [XmlAttributeAttribute] public virtual string Tooltip { get { return tooltip; } set { if (tooltip == value) return; tooltip = value; NotifyValueChanged("Tooltip", tooltip); } } [XmlAttributeAttribute] public IList ContextCommands { get { return contextCommands; } set { if (contextCommands == value) return; contextCommands = value; NotifyValueChanged("ContextCommands", contextCommands); } } #endregion #region Default and Style Values loading /// Loads the default values from XML attributes default public void loadDefaultValues() { #if DEBUG_LOAD Debug.WriteLine ("LoadDefValues for " + this.ToString ()); #endif Type thisType = this.GetType (); if (!string.IsNullOrEmpty (Style)) { if (CurrentInterface.DefaultValuesLoader.ContainsKey (Style)) { CurrentInterface.DefaultValuesLoader [Style] (this); return; } } else { if (CurrentInterface.DefaultValuesLoader.ContainsKey (thisType.FullName)) { CurrentInterface.DefaultValuesLoader [thisType.FullName] (this); return; } else if (!CurrentInterface.Styling.ContainsKey (thisType.FullName)) { if (CurrentInterface.DefaultValuesLoader.ContainsKey (thisType.Name)) { CurrentInterface.DefaultValuesLoader [thisType.Name] (this); return; } } } List