// // Interface.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.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using System.Xml; using System.Xml.Serialization; using Cairo; using System.Globalization; using Crow.IML; using System.Runtime.InteropServices; namespace Crow { /// /// The Interface Class is the root of crow graphic trees. It is thread safe allowing /// application to run multiple interfaces in different threads. /// It provides the Dirty bitmap and zone of the interface to be drawn on screen. /// /// /// The Interface contains : /// - rendering and layouting queues and logic. /// - helpers to load XML interfaces files directely bound to this interface /// - global static constants and variables of CROW /// - Keyboard and Mouse logic /// - the resulting bitmap of the interface /// /// the master branch and the nuget package includes an OpenTK renderer which allows /// the creation of multiple threaded interfaces. /// /// If you intend to create another renderer (GDK, vulkan, etc) the minimal step is to /// put an interface instance as member of your root object and call (optionally in another thread) the update function /// at regular interval. Then you have to call /// mouse, keyboard and resize functions of the interface when those events occurs in the host app. /// /// The resulting surface (a byte array in the OpenTK renderer) is made available and protected with the /// RenderMutex of the interface. /// public class Interface : ILayoutable, IDisposable ,IValueChange { #region IValueChange implementation public event EventHandler ValueChanged; 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 internal IBackend backend; #region CTOR static Interface(){ if (Type.GetType ("Mono.Runtime") == null) { throw new Exception (@"C.R.O.W. run only on Mono, download latest version at: http://www.mono-project.com/download/stable/"); } CrowConfigRoot = System.IO.Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); CrowConfigRoot = System.IO.Path.Combine (CrowConfigRoot, "crow"); if (!Directory.Exists (CrowConfigRoot)) Directory.CreateDirectory (CrowConfigRoot); //ensure all assemblies are loaded, because IML could contains classes not instanciated in source foreach (string af in Directory.GetFiles (AppDomain.CurrentDomain.BaseDirectory, "*.dll")){ try { Assembly.LoadFrom (af); } catch (Exception ex) { Console.WriteLine ("{0} not loaded as assembly.", af); } } FontRenderingOptions = new FontOptions (); FontRenderingOptions.Antialias = Antialias.Subpixel; FontRenderingOptions.HintMetrics = HintMetrics.On; FontRenderingOptions.HintStyle = HintStyle.Full; FontRenderingOptions.SubpixelOrder = SubpixelOrder.Rgb; } public Interface(int width=800, int height=600){ clientRectangle = new Rectangle (0, 0, width, height); Init (); InitBackend (); } #endregion protected virtual void InitBackend () { backend = new Crow.XCB.XCBBackend (); backend.Init (this); Thread t = new Thread (interfaceThread); t.IsBackground = true; t.Start (); } void interfaceThread() { while (true) { Update (); backend.ProcessEvents (); } } #region IDisposable Support private bool disposedValue = false; // To detect redundant calls protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // TODO: dispose managed state (managed objects). } backend.CleanUp (); disposedValue = true; } } // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. ~Interface() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(false); } // This code added to correctly implement the disposable pattern. public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion public void Init () { CultureInfo.DefaultThreadCurrentCulture = System.Globalization.CultureInfo.InvariantCulture; CurrentInterface = this; //loadCursors (); loadStyling (); findAvailableTemplates (); //initTooltip (); //initContextMenus (); #if MEASURE_TIME PerfMeasures.Add (updateMeasure); PerfMeasures.Add (drawingMeasure); PerfMeasures.Add (layoutingMeasure); PerfMeasures.Add (clippingMeasure); #endif } #region Static and constants /// /// Crow configuration root path /// public static string CrowConfigRoot; /// If true, mouse focus is given when mouse is over control public static bool FocusOnHover = false; /// Threshold to catch borders for sizing public static int BorderThreshold = 5; /// delay before tooltip appear public static int ToolTipDelay = 500; /// Double click threshold in milisecond public static int DoubleClick = 250;//max duration between two mouse_down evt for a dbl clk in milisec. /// Time to wait in millisecond before starting repeat loop public static int DeviceRepeatDelay = 700; /// Time interval in millisecond between device event repeat public static int DeviceRepeatInterval = 40; public static float WheelIncrement = 1; /// Tabulation size in Text controls public static int TabSize = 4; public static string LineBreak = "\n"; /// Allow rendering of interface in development environment public static bool DesignerMode = false; /// Disable caching for a widget if this threshold is reached public const int MaxCacheSize = 2048; /// Above this count, the layouting is discard from the current /// update cycle and requeued for the next public const int MaxLayoutingTries = 3; /// Above this count, the layouting is discard for the widget and it /// will not be rendered on screen public const int MaxDiscardCount = 5; /// Global font rendering settings for Cairo public static FontOptions FontRenderingOptions; /// Global font rendering settings for Cairo public static Antialias Antialias = Antialias.Subpixel; /// /// Each control need a ref to the root interface containing it, if not set in GraphicObject.currentInterface, /// the ref of this one will be stored in GraphicObject.currentInterface /// protected static Interface CurrentInterface; internal Stopwatch clickTimer = new Stopwatch(); GraphicObject armedClickSender = null; MouseButtonEventArgs armedClickEvtArgs = null; // internal GraphicObject EligibleForDoubleClick { // get { return eligibleForDoubleClick; } // set { // eligibleForDoubleClick = value; // clickTimer.Restart (); // } // } internal void armeClick (GraphicObject sender, MouseButtonEventArgs e){ armedClickSender = sender; armedClickEvtArgs = e; clickTimer.Restart (); } #endregion #region Events public event EventHandler MouseCursorChanged; public event EventHandler Quit; public event EventHandler MouseWheelChanged; public event EventHandler MouseButtonUp; public event EventHandler MouseButtonDown; public event EventHandler MouseClick; public event EventHandler MouseMove; public event EventHandler KeyboardKeyDown; public event EventHandler KeyboardKeyUp; #endregion /// Main Cairo surface public Surface surf; #region Public Fields /// Graphic Tree of this interface public List GraphicTree = new List(); /// Interface's resulting bitmap public byte[] bmp; /// resulting bitmap limited to last redrawn part public byte[] dirtyBmp; /// True when host has to repaint Interface public bool IsDirty = false; /// Coordinate of the dirty bmp on the original bmp public Rectangle DirtyRect; /// Locked for each layouting operation public object LayoutMutex = new object(); /// Sync mutex between host and Crow for rendering operations (bmp, dirtyBmp,...) public object RenderMutex = new object(); /// Global lock of the update cycle public object UpdateMutex = new object(); /// Global lock of the clipping queue public object ClippingMutex = new object(); //TODO:share resource instances /// /// Store loaded resources instances shared among controls to reduce memory footprint /// public Dictionary Ressources = new Dictionary(); /// The Main layouting queue. public Queue LayoutingQueue = new Queue (); /// Store discarded lqi between two updates public Queue DiscardQueue; /// Main drawing queue, holding layouted controls public Queue ClippingQueue = new Queue(); public string Clipboard;//TODO:use object instead for complex copy paste /// each IML and fragments (such as inline Templates) are compiled as a Dynamic Method stored here /// on the first instance creation of a IML item. /// public Dictionary Instantiators = new Dictionary(); public List CrowThreads = new List();//used to monitor thread finished public DragDropEventArgs DragAndDropOperation = null; public Surface DragImage = null; public int DragImageWidth, DragImageHeight, DragImageX, DragImageY; public void ClearDragImage () { lock (UpdateMutex) { clipping.UnionRectangle(new Rectangle (DragImageX, DragImageY, DragImageWidth, DragImageHeight)); DragImage.Dispose(); DragImage = null; } } #endregion #region Private Fields /// Client rectangle in the host context protected Rectangle clientRectangle; /// Clipping rectangles on the root context Region clipping = new Region(); /// Main Cairo context Context ctx; #endregion #region Default values and Style loading /// Default values of properties from GraphicObjects are retrieve from XML Attributes. /// The reflexion process used to retrieve those values being very slow, it is compiled in MSIL /// and injected as a dynamic method referenced in the DefaultValuesLoader Dictionnary. /// The compilation is done on the first object instancing, and is also done for custom widgets public delegate void LoaderInvoker(object instance); /// Store one loader per StyleKey public Dictionary DefaultValuesLoader = new Dictionary(); /// Store dictionnary of member/value per StyleKey public Dictionary Styling; /// parse all styling data's during application startup and build global Styling Dictionary protected virtual void loadStyling() { Styling = new Dictionary (); //fetch styling info in this order, if member styling is alreadey referenced in previous //assembly, it's ignored. loadStylingFromAssembly (Assembly.GetEntryAssembly ()); loadStylingFromAssembly (Assembly.GetExecutingAssembly ()); } /// Search for .style resources in assembly void loadStylingFromAssembly (Assembly assembly) { if (assembly == null) return; foreach (string s in assembly .GetManifestResourceNames () .Where (r => r.EndsWith (".style", StringComparison.OrdinalIgnoreCase))) { using (Stream stream = assembly.GetManifestResourceStream (s)) { new StyleReader (this.Styling, stream, s); } } } void loadCursors(){ //Load cursors XCursor.Cross = XCursorFile.Load(this, "#Crow.Images.Icons.Cursors.cross").Cursors[0]; XCursor.Default = XCursorFile.Load(this, "#Crow.Images.Icons.Cursors.arrow").Cursors[0]; XCursor.NW = XCursorFile.Load(this, "#Crow.Images.Icons.Cursors.top_left_corner").Cursors[0]; XCursor.NE = XCursorFile.Load(this, "#Crow.Images.Icons.Cursors.top_right_corner").Cursors[0]; XCursor.SW = XCursorFile.Load(this, "#Crow.Images.Icons.Cursors.bottom_left_corner").Cursors[0]; XCursor.SE = XCursorFile.Load(this, "#Crow.Images.Icons.Cursors.bottom_right_corner").Cursors[0]; XCursor.H = XCursorFile.Load(this, "#Crow.Images.Icons.Cursors.sb_h_double_arrow").Cursors[0]; XCursor.V = XCursorFile.Load(this, "#Crow.Images.Icons.Cursors.sb_v_double_arrow").Cursors[0]; XCursor.Text = XCursorFile.Load(this, "#Crow.Images.Icons.Cursors.ibeam").Cursors[0]; } #endregion #region Templates /// Store one default templates resource ID per class. /// Resource ID must be 'fullClassName.template' (not case sensitive) /// Those found in application assembly have priority to the default Crow's one /// public Dictionary DefaultTemplates; /// Finds available default templates at startup void findAvailableTemplates(){ DefaultTemplates = new Dictionary(); searchTemplatesOnDisk ("./"); string defTemplatePath = System.IO.Path.Combine (CrowConfigRoot, "defaultTemplates"); searchTemplatesOnDisk (defTemplatePath); searchTemplatesIn (Assembly.GetEntryAssembly ()); searchTemplatesIn (Assembly.GetExecutingAssembly ()); } void searchTemplatesOnDisk (string templatePath){ if (!Directory.Exists (templatePath)) return; foreach (string f in Directory.GetFiles(templatePath, "*.template",SearchOption.AllDirectories)) { string clsName = System.IO.Path.GetFileNameWithoutExtension(f); if (DefaultTemplates.ContainsKey (clsName)) continue; DefaultTemplates [clsName] = f; } } void searchTemplatesIn(Assembly assembly){ if (assembly == null) return; foreach (string resId in assembly .GetManifestResourceNames () .Where (r => r.EndsWith (".template", StringComparison.OrdinalIgnoreCase))) { string clsName = resId.Substring (0, resId.Length - 9); if (DefaultTemplates.ContainsKey (clsName)) continue; DefaultTemplates[clsName] = "#" + resId; } } #endregion #region Load/Save /// Open file or find a resource from path string /// A file or resource stream /// This could be a normal file path, or an embedded ressource ID /// Resource ID's must be prefixed with '#' character public virtual Stream GetStreamFromPath (string path) { Stream stream = null; if (path.StartsWith ("#")) { string resId = path.Substring (1); //try/catch added to prevent nunit error try { stream = System.Reflection.Assembly.GetEntryAssembly ().GetManifestResourceStream (resId); } catch{} if (stream == null)//try to find ressource in Crow assembly stream = System.Reflection.Assembly.GetExecutingAssembly ().GetManifestResourceStream (resId); if (stream == null) throw new Exception ("Resource not found: " + path); } else { if (!File.Exists (path)) throw new FileNotFoundException ("File not found: ", path); stream = new FileStream (path, FileMode.Open, FileAccess.Read); } return stream; } public static Stream StaticGetStreamFromPath (string path) { Stream stream = null; if (path.StartsWith ("#")) { string resId = path.Substring (1); //try/catch added to prevent nunit error try { stream = System.Reflection.Assembly.GetEntryAssembly ().GetManifestResourceStream (resId); } catch{} if (stream == null)//try to find ressource in Crow assembly stream = System.Reflection.Assembly.GetExecutingAssembly ().GetManifestResourceStream (resId); if (stream == null) throw new Exception ("Resource not found: " + path); } else { if (!File.Exists (path)) throw new FileNotFoundException ("File not found: ", path); stream = new FileStream (path, FileMode.Open, FileAccess.Read); } return stream; } /// /// Add the content of the IML fragment to the graphic tree of this interface /// /// return the new instance for convenience, may be ignored /// a valid IML string public GraphicObject LoadIMLFragment (string imlFragment) { lock (UpdateMutex) { GraphicObject tmp = CreateITorFromIMLFragment (imlFragment).CreateInstance(); AddWidget (tmp); return tmp; } } /// /// Add the content of the IML fragment to the graphic tree of this interface /// /// return the new instance for convenience, may be ignored /// a valid IML string public Instantiator CreateITorFromIMLFragment (string imlFragment) { return Instantiator.CreateFromImlFragment (this, imlFragment); } /// /// Create an instance of a GraphicObject and add it to the GraphicTree of this Interface /// /// new instance of graphic object created /// path of the iml file to load public GraphicObject AddWidget (string path) { lock (UpdateMutex) { GraphicObject tmp = Load (path); AddWidget (tmp); return tmp; } } /// /// Create an instance of a GraphicObject linked to this interface but not added to the GraphicTree /// /// new instance of graphic object created /// path of the iml file to load public virtual GraphicObject Load (string path) { try { return GetInstantiator (path).CreateInstance (); } catch (Exception ex) { throw new Exception ("Error loading <" + path + ">:", ex); } } /// /// Fetch instantiator from cache or create it. /// /// new Instantiator /// path of the iml file to load public Instantiator GetInstantiator(string path){ if (!Instantiators.ContainsKey(path)) Instantiators [path] = new Instantiator(this, path); return Instantiators [path]; } /// Item templates are derived from instantiator, this function /// try to fetch the requested one in the cache or create it. /// They have additional properties for recursivity and /// custom display per item type public virtual ItemTemplate GetItemTemplate(string path){ if (!Instantiators.ContainsKey(path)) Instantiators [path] = new ItemTemplate(this, path); return Instantiators [path] as ItemTemplate; } #endregion #region focus GraphicObject _activeWidget; //button is pressed on widget GraphicObject _hoverWidget; //mouse is over GraphicObject _focusedWidget; //has keyboard (or other perif) focus /// Widget is focused and button is down or another perif action is occuring /// , it can not lose focus while Active public GraphicObject ActiveWidget { get { return _activeWidget; } set { if (_activeWidget == value) return; if (_activeWidget != null) _activeWidget.IsActive = false; _activeWidget = value; if (_activeWidget != null) { _activeWidget.IsActive = true; #if DEBUG_FOCUS Debug.WriteLine("Active => " + _activeWidget.ToString()); }else Debug.WriteLine("Active => null"); #else } #endif } } /// Pointer is over the widget public virtual GraphicObject HoverWidget { get { return _hoverWidget; } set { if (_hoverWidget == value) return; if (_hoverWidget != null) _hoverWidget.IsHover = false; _hoverWidget = value; if (_hoverWidget != null) { _hoverWidget.IsHover = true; #if DEBUG_FOCUS Debug.WriteLine("Hover => " + _hoverWidget.ToString()); }else Debug.WriteLine("Hover => null"); #else } #endif } } /// Widget has the keyboard or mouse focus public GraphicObject FocusedWidget { get { return _focusedWidget; } set { if (_focusedWidget == value) return; if (_focusedWidget != null) _focusedWidget.HasFocus = false; _focusedWidget = value; if (_focusedWidget != null) _focusedWidget.HasFocus = true; } } #endregion #region UPDATE Loops /// Enqueue Graphic object for Repaint, DrawingQueue is locked because /// GraphObj's property Set methods could trigger an update from another thread /// Once in that queue, that means that the layouting of obj and childs have succeed, /// the next step when dequeued will be clipping registration public void EnqueueForRepaint(GraphicObject g) { lock (ClippingMutex) { if (g.IsQueueForClipping) return; #if DEBUG_LOG DebugLog.AddEvent(DbgEvtType.GOEnqueueForRepaint, g); #endif ClippingQueue.Enqueue (g); g.IsQueueForClipping = true; } } /// Main Update loop, executed in this interface thread, protected by the UpdateMutex /// Steps: /// - execute device Repeat events /// - Layouting /// - Clipping /// - Drawing /// Result: the Interface bitmap is drawn in memory (byte[] bmp) and a dirtyRect and bitmap are available /// public void Update(){ if (armedClickSender != null && clickTimer.ElapsedMilliseconds >= Interface.DoubleClick) { armedClickSender.onMouseClick (armedClickSender, armedClickEvtArgs); armedClickSender = null; } if (mouseRepeatCount > 0) { int mc = mouseRepeatCount; mouseRepeatCount -= mc; if (_focusedWidget != null) { mouseRepeatTriggeredAtLeastOnce = true; for (int i = 0; i < mc; i++) { _focusedWidget.onMouseDown (this, new BubblingMouseButtonEventArg(Mouse.X, Mouse.Y, MouseButton.Left, true)); } } } CrowThread[] tmpThreads; lock (CrowThreads) { tmpThreads = new CrowThread[CrowThreads.Count]; Array.Copy (CrowThreads.ToArray (), tmpThreads, CrowThreads.Count); } for (int i = 0; i < tmpThreads.Length; i++) tmpThreads [i].CheckState (); if (!Monitor.TryEnter (UpdateMutex)) return; #if MEASURE_TIME updateMeasure.StartCycle(); #endif processLayouting (); #if DEBUG_LAYOUTING if (curLQIsTries.Count > 0){ LQIsTries.Add(curLQIsTries); curLQIsTries = new LQIList(); LQIs.Add(curLQIs); curLQIs = new LQIList(); } #endif clippingRegistration (); processDrawing (); #if MEASURE_TIME updateMeasure.StopCycle(); #endif Monitor.Exit (UpdateMutex); } /// Layouting loop, this is the first step of the udpate and process registered /// Layouting queue items. Failing LQI's are requeued in this cycle until MaxTry is reached which /// trigger an enqueue for the next Update Cycle protected virtual void processLayouting(){ #if MEASURE_TIME layoutingMeasure.StartCycle(); #endif if (Monitor.TryEnter (LayoutMutex)) { DiscardQueue = new Queue (); //Debug.WriteLine ("======= Layouting queue start ======="); LayoutingQueueItem lqi; while (LayoutingQueue.Count > 0) { lqi = LayoutingQueue.Dequeue (); #if DEBUG_LAYOUTING currentLQI = lqi; curLQIsTries.Add(currentLQI); #endif lqi.ProcessLayouting (); } LayoutingQueue = DiscardQueue; Monitor.Exit (LayoutMutex); DiscardQueue = null; } #if MEASURE_TIME layoutingMeasure.StopCycle(); #endif } /// Degueue Widget to clip from DrawingQueue and register the last painted slot and the new one /// Clipping rectangles are added at each level of the tree from leef to root, that's the way for the painting /// operation to known if it should go down in the tree for further graphic updates and repaints void clippingRegistration(){ #if MEASURE_TIME clippingMeasure.StartCycle(); #endif GraphicObject g = null; while (ClippingQueue.Count > 0) { lock (ClippingMutex) { g = ClippingQueue.Dequeue (); g.IsQueueForClipping = false; } g.ClippingRegistration (); } #if MEASURE_TIME clippingMeasure.StopCycle(); #endif } /// Clipping Rectangles drive the drawing process. For compositing, each object under a clip rectangle should be /// repainted. If it contains also clip rectangles, its cache will be update, or if not cached a full redraw will take place void processDrawing(){ #if MEASURE_TIME drawingMeasure.StartCycle(); #endif if (DragImage != null) clipping.UnionRectangle(new Rectangle (DragImageX, DragImageY, DragImageWidth, DragImageHeight)); //using (surf = new ImageSurface (bmp, Format.Argb32, ClientRectangle.Width, ClientRectangle.Height, ClientRectangle.Width * 4)) { using (ctx = new Context (surf)){ if (!clipping.IsEmpty) { IsDirty = true; for (int i = 0; i < clipping.NumRectangles; i++) ctx.Rectangle(clipping.GetRectangle(i)); ctx.ClipPreserve(); ctx.Operator = Operator.Clear; ctx.Fill(); ctx.Operator = Operator.Over; for (int i = GraphicTree.Count -1; i >= 0 ; i--){ GraphicObject p = GraphicTree[i]; if (!p.Visible) continue; if (clipping.Contains (p.Slot) == RegionOverlap.Out) continue; ctx.Save (); p.Paint (ref ctx); ctx.Restore (); } if (DragAndDropOperation != null) { if (DragImage != null) { DirtyRect += new Rectangle (DragImageX, DragImageY, DragImageWidth, DragImageHeight); DragImageX = Mouse.X - DragImageWidth / 2; DragImageY = Mouse.Y - DragImageHeight / 2; ctx.Save (); ctx.ResetClip (); ctx.SetSourceSurface (DragImage, DragImageX, DragImageY); ctx.PaintWithAlpha (0.8); ctx.Restore (); DirtyRect += new Rectangle (DragImageX, DragImageY, DragImageWidth, DragImageHeight); IsDirty = true; //Console.WriteLine ("dragimage drawn: {0},{1}", DragImageX, DragImageY); } } #if DEBUG_CLIP_RECTANGLE clipping.stroke (ctx, Color.Red.AdjustAlpha(0.5)); #endif clipping.Dispose (); clipping = new Region (); //} //surf.WriteToPng (@"/mnt/data/test.png"); backend?.Flush (); } } #if MEASURE_TIME drawingMeasure.StopCycle(); #endif } #endregion #region GraphicTree handling /// Add widget to the Graphic tree of this interface and register it for layouting public GraphicObject AddWidget(GraphicObject g) { g.Parent = this; int ptr = 0; Window newW = g as Window; if (newW != null) { while (ptr < GraphicTree.Count) { Window w = GraphicTree [ptr] as Window; if (w != null) { if (newW.AlwaysOnTop || !w.AlwaysOnTop) break; } ptr++; } } lock (UpdateMutex) GraphicTree.Insert (ptr, g); g.RegisteredLayoutings = LayoutingType.None; g.RegisterForLayouting (LayoutingType.Sizing | LayoutingType.ArrangeChildren); return g; } /// Set visible state of widget to false and delete if from the graphic tree public void DeleteWidget(GraphicObject g) { lock (UpdateMutex) { RegisterClip (g.ScreenCoordinates (g.LastPaintedSlot)); GraphicTree.Remove (g); g.Parent = null; g.Dispose (); } } /// Set visible state of widget to false and remove if from the graphic tree public void RemoveWidget(GraphicObject g) { // if (g.Contains(HoverWidget)) { // while (HoverWidget != g.focusParent) { // HoverWidget.onMouseLeave (HoverWidget, null); // HoverWidget = HoverWidget.focusParent; // } // } lock (UpdateMutex) { RegisterClip (g.ScreenCoordinates (g.LastPaintedSlot)); GraphicTree.Remove (g); g.Parent = null; } } /// Remove all Graphic objects from top container public void ClearInterface() { lock (UpdateMutex) { while (GraphicTree.Count > 0) { //TODO:parent is not reset to null because object will be added //to ObjectToRedraw list, and without parent, it fails GraphicObject g = GraphicTree [0]; RegisterClip (g.ScreenCoordinates (g.LastPaintedSlot)); GraphicTree.RemoveAt (0); g.Dispose (); } } #if DEBUG_LAYOUTING LQIsTries = new List(); curLQIsTries = new LQIList(); LQIs = new List(); curLQIs = new LQIList(); #endif } /// Put widget on top of other root widgets public void PutOnTop(GraphicObject g, bool isOverlay = false) { int ptr = 0; Window newW = g as Window; if (newW != null) { while (ptr < GraphicTree.Count) { Window w = GraphicTree [ptr] as Window; if (w != null) { if (newW.AlwaysOnTop || !w.AlwaysOnTop) break; } ptr++; } } if (GraphicTree.IndexOf(g) > ptr) { lock (UpdateMutex) { GraphicTree.Remove (g); GraphicTree.Insert (ptr, g); } EnqueueForRepaint (g); } } /// Search a Graphic object in the tree named 'nameToFind' public GraphicObject FindByName (string nameToFind) { foreach (GraphicObject w in GraphicTree) { GraphicObject r = w.FindByName (nameToFind); if (r != null) return r; } return null; } #endregion /// /// Resize the interface. This function should be called by the host /// when window resize event occurs. /// /// bounding box of the interface public virtual void ProcessResize(Rectangle bounds){ lock (UpdateMutex) { clientRectangle = bounds; /*surf.Dispose (); surf = new Cairo.XlibSurface (xHandle, xwinHnd, xDefaultVisual, clientRectangle.Width, clientRectangle.Height);*/ (surf as XlibSurface).SetSize (clientRectangle.Width, clientRectangle.Height); foreach (GraphicObject g in GraphicTree) g.RegisterForLayouting (LayoutingType.All); RegisterClip (clientRectangle); } } #region Mouse and Keyboard Handling XCursor cursor = XCursor.Default; public MouseState Mouse; public KeyboardState Keyboard; public XCursor MouseCursor { set { if (value == cursor) return; cursor = value; MouseCursorChanged.Raise (this,new MouseCursorChangedEventArgs(cursor)); } } /// Processes mouse move events from the root container, this function /// should be called by the host on mouse move event to forward events to crow interfaces /// true if mouse is in the interface public virtual bool ProcessMouseMove(int x, int y) { if (armedClickSender != null) { armedClickSender.onMouseClick (armedClickSender, armedClickEvtArgs); armedClickSender = null; } int deltaX = x - Mouse.X; int deltaY = y - Mouse.Y; Mouse.X = x; Mouse.Y = y; MouseMoveEventArgs e = new MouseMoveEventArgs (x, y, deltaX, deltaY); e.Mouse = Mouse; if (ActiveWidget != null && DragAndDropOperation == null) { //TODO, ensure object is still in the graphic tree //send move evt even if mouse move outside bounds ActiveWidget.onMouseMove (this, e); return true; } if (DragAndDropOperation != null)//drag source cant have hover event, so move has to be handle here DragAndDropOperation.DragSource.onMouseMove (this, e); if (HoverWidget != null) { resetTooltip (); //check topmost graphicobject first GraphicObject tmp = HoverWidget; GraphicObject topc = null; while (tmp is GraphicObject) { topc = tmp; tmp = tmp.focusParent; } int idxhw = GraphicTree.IndexOf (topc); if (idxhw != 0) { int i = 0; while (i < idxhw) { if (!GraphicTree [i].isPopup) { if (GraphicTree [i].MouseIsIn (e.Position)) { while (HoverWidget != null) { HoverWidget.onMouseLeave (HoverWidget, e); HoverWidget = HoverWidget.focusParent; } GraphicTree [i].checkHoverWidget (e); HoverWidget.onMouseMove (this, e); return true; } } i++; } } if (HoverWidget.MouseIsIn (e.Position)) { HoverWidget.checkHoverWidget (e); HoverWidget.onMouseMove (this, e); return true; } else { HoverWidget.onMouseLeave (HoverWidget, e); //seek upward from last focused graph obj's while (HoverWidget.focusParent != null) { HoverWidget = HoverWidget.focusParent; if (HoverWidget.MouseIsIn (e.Position)) { HoverWidget.checkHoverWidget (e); HoverWidget.onMouseMove (this, e); return true; } else HoverWidget.onMouseLeave (HoverWidget, e); } } } //top level graphic obj's parsing lock (GraphicTree) { for (int i = 0; i < GraphicTree.Count; i++) { GraphicObject g = GraphicTree [i]; if (g.MouseIsIn (e.Position)) { g.checkHoverWidget (e); if (g is Window) PutOnTop (g); HoverWidget.onMouseMove (this, e); return true; } } } HoverWidget = null; return false; } /// /// Forward the mouse up event from the host to the crow interface /// /// return true, if interface handled the event, false otherwise. /// Button index public bool ProcessMouseButtonUp(int button) { Mouse.DisableBit (button); MouseButtonEventArgs e = new MouseButtonEventArgs ((Crow.MouseButton)button) { Mouse = Mouse }; if (_activeWidget == null) return false; if (mouseRepeatThread != null) { mouseRepeatOn = false; mouseRepeatThread.Abort(); mouseRepeatThread.Join (); } if (!mouseRepeatTriggeredAtLeastOnce) { if (_activeWidget.MouseIsIn (e.Position)) armeClick (_activeWidget, e); } mouseRepeatTriggeredAtLeastOnce = false; _activeWidget.onMouseUp (_activeWidget, e); // GraphicObject lastActive = _activeWidget; ActiveWidget = null; // if (!lastActive.MouseIsIn (Mouse.Position)) { // ProcessMouseMove (Mouse.X, Mouse.Y); // } return true; } /// /// Forward the mouse down event from the host to the crow interface /// /// return true, if interface handled the event, false otherwise. /// Button index public bool ProcessMouseButtonDown(int button) { Mouse.EnableBit (button); MouseButtonEventArgs e = new MouseButtonEventArgs ((Crow.MouseButton)button) { Mouse = Mouse }; if (HoverWidget == null) return false; if (HoverWidget == armedClickSender) { if (clickTimer.ElapsedMilliseconds < Interface.DoubleClick) { armedClickSender.onMouseDoubleClick (armedClickSender, e); armedClickSender = null; return true; } } if (armedClickSender!=null) armedClickSender.onMouseClick (armedClickSender, armedClickEvtArgs); armedClickSender = null; HoverWidget.onMouseDown(HoverWidget,new BubblingMouseButtonEventArg(e)); if (FocusedWidget == null) return true; if (!FocusedWidget.MouseRepeat) return true; mouseRepeatThread = new Thread (mouseRepeatThreadFunc); mouseRepeatThread.IsBackground = true; mouseRepeatThread.Start (); return true; } /// /// Forward the mouse wheel event from the host to the crow interface /// /// return true, if interface handled the event, false otherwise. /// Button index public bool ProcessMouseWheelChanged(float delta) { Mouse.SetScrollRelative (0, delta); MouseWheelEventArgs e = new MouseWheelEventArgs () { Mouse = Mouse, DeltaPrecise = delta }; if (HoverWidget == null) return false; HoverWidget.onMouseWheel (this, e); return true; } /// /// Forward key down event from the host to the crow interface /// /// return true, if interface handled the event, false otherwise. /// Button index public bool ProcessKeyDown(int Key){ //Keyboard.SetKeyState((Crow.Key)Key,true); KeyboardKeyEventArgs e = lastKeyDownEvt = new KeyboardKeyEventArgs((Crow.Key)Key, false, Keyboard); lastKeyDownEvt.IsRepeat = true; KeyboardKeyDown.Raise (this, e); if (_focusedWidget == null) return false; _focusedWidget.onKeyDown (this, e); // keyboardRepeatThread = new Thread (keyboardRepeatThreadFunc); // keyboardRepeatThread.IsBackground = true; // keyboardRepeatThread.Start (); return true; } /// /// Forward key up event from the host to the crow interface /// /// return true, if interface handled the event, false otherwise. /// Button index public bool ProcessKeyUp(int Key){ //Keyboard.SetKeyState((Crow.Key)Key,false); if (_focusedWidget == null) return false; KeyboardKeyEventArgs e = new KeyboardKeyEventArgs((Crow.Key)Key, false, Keyboard); _focusedWidget.onKeyUp (this, e); KeyboardKeyUp.Raise (this, e); // if (keyboardRepeatThread != null) { // keyboardRepeatOn = false; // keyboardRepeatThread.Abort(); // keyboardRepeatThread.Join (); // } return true; } /// /// Forward a translated key press event from the host to the crow interface /// /// return true, if interface handled the event, false otherwise. /// Button index public bool ProcessKeyPress(char Key){ if (_focusedWidget == null) return false; KeyPressEventArgs e = new KeyPressEventArgs(Key); _focusedWidget.onKeyPress (this, e); return true; } #endregion #region Tooltip handling Stopwatch tooltipTimer = new Stopwatch(); GraphicObject ToolTipContainer = null; volatile bool tooltipVisible = false; protected void initTooltip () { ToolTipContainer = Load ("#Crow.Tooltip.template"); Thread t = new Thread (toolTipThreadFunc); t.IsBackground = true; t.Start (); } void toolTipThreadFunc () { while(true) { if (tooltipTimer.ElapsedMilliseconds > ToolTipDelay) { if (!tooltipVisible) { GraphicObject g = _hoverWidget; while (g != null) { if (!string.IsNullOrEmpty (g.Tooltip)) { AddWidget (ToolTipContainer); ToolTipContainer.DataSource = g; ToolTipContainer.Top = Mouse.Y + 10; ToolTipContainer.Left = Mouse.X + 10; tooltipVisible = true; break; } g = g.LogicalParent as GraphicObject; } } } Thread.Sleep (200); } } void resetTooltip () { if (tooltipVisible) { //ToolTipContainer.DataSource = null; RemoveWidget (ToolTipContainer); tooltipVisible = false; } tooltipTimer.Restart (); } #endregion #region Contextual menu MenuItem ctxMenuContainer; protected void initContextMenus (){ ctxMenuContainer = Load ("#Crow.ContextMenu.template") as MenuItem; ctxMenuContainer.LayoutChanged += CtxMenuContainer_LayoutChanged; } void CtxMenuContainer_LayoutChanged (object sender, LayoutingEventArgs e) { Rectangle r = ctxMenuContainer.ScreenCoordinates (ctxMenuContainer.Slot); if (e.LayoutType == LayoutingType.Width || e.LayoutType == LayoutingType.X) { if (r.Right > this.clientRectangle.Right) ctxMenuContainer.Left = this.clientRectangle.Right - ctxMenuContainer.Slot.Width; }else if (e.LayoutType == LayoutingType.Width || e.LayoutType == LayoutingType.Y) { if (r.Bottom > this.clientRectangle.Bottom) ctxMenuContainer.Top = this.clientRectangle.Bottom - ctxMenuContainer.Slot.Height; } } public void ShowContextMenu (GraphicObject go) { lock (UpdateMutex) { if (ctxMenuContainer.Parent == null) this.AddWidget (ctxMenuContainer); else ctxMenuContainer.IsOpened = true; ctxMenuContainer.isPopup = true; ctxMenuContainer.LogicalParent = go; ctxMenuContainer.DataSource = go; PutOnTop (ctxMenuContainer, true); } ctxMenuContainer.Left = Mouse.X - 5; ctxMenuContainer.Top = Mouse.Y - 5; HoverWidget = ctxMenuContainer; ctxMenuContainer.onMouseEnter (ctxMenuContainer, new MouseMoveEventArgs (Mouse.X, Mouse.Y, 0, 0)); } #endregion #region Device Repeat Events volatile bool mouseRepeatOn, keyboardRepeatOn, mouseRepeatTriggeredAtLeastOnce = false; volatile int mouseRepeatCount, keyboardRepeatCount; Thread mouseRepeatThread, keyboardRepeatThread; KeyboardKeyEventArgs lastKeyDownEvt; void mouseRepeatThreadFunc() { mouseRepeatOn = true; mouseRepeatTriggeredAtLeastOnce = false; Thread.Sleep (Interface.DeviceRepeatDelay); while (mouseRepeatOn) { mouseRepeatCount++; Thread.Sleep (Interface.DeviceRepeatInterval); } mouseRepeatCount = 0; } void keyboardRepeatThreadFunc() { keyboardRepeatOn = true; Thread.Sleep (Interface.DeviceRepeatDelay); while (keyboardRepeatOn) { keyboardRepeatCount++; Thread.Sleep (Interface.DeviceRepeatInterval); } keyboardRepeatCount = 0; } #endregion #region ILayoutable implementation public virtual bool PointIsIn(ref Point m) { return true; } public void RegisterClip(Rectangle r){ clipping.UnionRectangle (r); } public bool ArrangeChildren { get { return false; }} public int LayoutingTries { get { throw new NotImplementedException (); } set { throw new NotImplementedException (); } } public LayoutingType RegisteredLayoutings { get { return LayoutingType.None; } set { throw new NotImplementedException (); } } public void RegisterForLayouting (LayoutingType layoutType) { throw new NotImplementedException (); } public bool UpdateLayout (LayoutingType layoutType) { throw new NotImplementedException (); } public Rectangle ContextCoordinates (Rectangle r) { return r;} public Rectangle ScreenCoordinates (Rectangle r) { return r; } public ILayoutable Parent { get { return null; } set { throw new NotImplementedException (); } } public ILayoutable LogicalParent { get { return null; } set { throw new NotImplementedException (); } } public Rectangle ClientRectangle { get { return clientRectangle; } } public Interface HostContainer { get { return this; } } public Rectangle getSlot () { return ClientRectangle; } #endregion #if MEASURE_TIME public PerformanceMeasure clippingMeasure = new PerformanceMeasure("Clipping", 1); public PerformanceMeasure layoutingMeasure = new PerformanceMeasure("Layouting", 1); public PerformanceMeasure updateMeasure = new PerformanceMeasure("Update", 1); public PerformanceMeasure drawingMeasure = new PerformanceMeasure("Drawing", 1); public List PerfMeasures = new List(); #endif #if DEBUG_LAYOUTING public List LQIsTries = new List(); public LQIList curLQIsTries = new LQIList(); public List LQIs = new List(); public LQIList curLQIs = new LQIList(); // public static LayoutingQueueItem[] MultipleRunsLQIs { // get { return curUpdateLQIs.Where(l=>l.LayoutingTries>2 || l.DiscardCount > 0).ToArray(); } // } public LayoutingQueueItem currentLQI; #else public List LQIs = null;//still create the var for CrowIDE #endif } }