diff options
author | Aaron Bockover <abock@microsoft.com> | 2019-03-06 23:19:20 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-03-06 23:19:20 +0300 |
commit | debfa3d9a52108e87dcf74e4ee6cf73ac010a97d (patch) | |
tree | adddd3c8caed2455770bdac88258d295c3c973b6 /main | |
parent | c60106f9c9b5d39b54659cd84e67be399f2ac9ae (diff) | |
parent | c864ceb44db3ca2a0c1712ab56e2726cca973bf3 (diff) |
Merge pull request #282 from xamarin/pr-abock-slightly-better-gtk
GtkNSViewHost: replace the native GtkNSView with a managed one
Diffstat (limited to 'main')
2 files changed, 393 insertions, 12 deletions
diff --git a/main/src/addins/MonoDevelop.TextEditor/MonoDevelop.TextEditor.Cocoa/CocoaTextViewContent.cs b/main/src/addins/MonoDevelop.TextEditor/MonoDevelop.TextEditor.Cocoa/CocoaTextViewContent.cs index a3d3b632c5..d1c4af7971 100644 --- a/main/src/addins/MonoDevelop.TextEditor/MonoDevelop.TextEditor.Cocoa/CocoaTextViewContent.cs +++ b/main/src/addins/MonoDevelop.TextEditor/MonoDevelop.TextEditor.Cocoa/CocoaTextViewContent.cs @@ -31,11 +31,10 @@ using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities; using MonoDevelop.Components; -using MonoDevelop.Components.Commands; using MonoDevelop.Core; +using MonoDevelop.Core.FeatureConfiguration; using MonoDevelop.Ide.Commands; using MonoDevelop.Projects; -using Microsoft.VisualStudio.Text.Editor.Implementation; namespace MonoDevelop.TextEditor { @@ -73,20 +72,47 @@ namespace MonoDevelop.TextEditor { ICocoaTextViewHost textViewHost; NSView textViewHostControl; - EmbeddedNSViewControl embeddedControl; + GtkNSViewHostControl embeddedControl; - sealed class EmbeddedNSViewControl : Control + static readonly Lazy<bool> useManagedGtkNSViewHost = new Lazy<bool> ( + () => FeatureSwitchService.IsFeatureEnabled ("ManagedGtkNSViewHost").GetValueOrDefault ()); + + abstract class GtkNSViewHostControl : Control + { + public Gtk.Widget GtkView { get; protected set; } + } + + sealed class ManagedGtkNSViewHostControl : GtkNSViewHostControl + { + public ManagedGtkNSViewHostControl (ICocoaTextViewHost textViewHost) + { + if (textViewHost == null) + throw new ArgumentNullException (nameof (textViewHost)); + + GtkView = new Gtk.GtkNSViewHost (textViewHost.HostControl); + GtkView.Show (); + } + + protected override object CreateNativeWidget<T> () + => GtkView; + + public override bool HasFocus + => GtkView.HasFocus; + + public override void GrabFocus () + => GtkView.GrabFocus (); + } + + sealed class LegacyGtkNSViewHostControl : GtkNSViewHostControl { readonly ICocoaTextViewHost textViewHost; readonly NSView nsView; bool nativeViewNeedsFocus; - public Gtk.Widget GtkView { get; } - public bool IsGrabbingFocus { get; private set; } - public EmbeddedNSViewControl (ICocoaTextViewHost textViewHost) + public LegacyGtkNSViewHostControl (ICocoaTextViewHost textViewHost) { this.textViewHost = textViewHost ?? throw new ArgumentNullException (nameof (textViewHost)); this.nsView = textViewHost.HostControl; @@ -164,11 +190,18 @@ namespace MonoDevelop.TextEditor textViewHost = Imports.TextEditorFactoryService.CreateTextViewHost (TextView, setFocus: true); textViewHostControl = textViewHost.HostControl; - embeddedControl = new EmbeddedNSViewControl (textViewHost); - TextView.GotAggregateFocus += (sender, e) => { - if (!embeddedControl.IsGrabbingFocus) - embeddedControl.GtkView.GrabFocus (); - }; + if (useManagedGtkNSViewHost.Value) { + embeddedControl = new ManagedGtkNSViewHostControl (textViewHost); + } else { + var legacyEmbeddedControl = new LegacyGtkNSViewHostControl (textViewHost); + embeddedControl = legacyEmbeddedControl; + + TextView.GotAggregateFocus += (sender, e) => { + if (!legacyEmbeddedControl.IsGrabbingFocus) + embeddedControl.GtkView.GrabFocus (); + }; + } + TextView.Properties.AddProperty (typeof (Gtk.Widget), embeddedControl.GtkView); return embeddedControl; diff --git a/main/src/addins/MonoDevelop.TextEditor/MonoDevelop.TextEditor.Cocoa/GtkNSViewHost.cs b/main/src/addins/MonoDevelop.TextEditor/MonoDevelop.TextEditor.Cocoa/GtkNSViewHost.cs new file mode 100644 index 0000000000..edc46e22e7 --- /dev/null +++ b/main/src/addins/MonoDevelop.TextEditor/MonoDevelop.TextEditor.Cocoa/GtkNSViewHost.cs @@ -0,0 +1,348 @@ +// +// Copyright (c) Microsoft Corp. (https://www.microsoft.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.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using AppKit; +using CoreGraphics; +using ObjCRuntime; + +namespace Gtk +{ + sealed class GtkNSViewHost : Widget + { + const string LIBGTKQUARTZ = "libgtk-quartz-2.0.dylib"; + + [DllImport (LIBGTKQUARTZ)] + static extern IntPtr gdk_quartz_window_get_nsview (IntPtr window); + + [DllImport (LIBGTKQUARTZ)] + static extern IntPtr gdk_quartz_event_get_nsevent (IntPtr evnt); + + [DllImport (LIBGTKQUARTZ)] + static extern void gdk_window_coords_to_parent ( + IntPtr window, + double x, + double y, + out double parent_x, + out double parent_y); + + [DllImport (LIBGTKQUARTZ)] + static extern bool gdk_window_has_native (IntPtr window); + + static NSEvent GetNSEvent (Gdk.Event evnt) + { + if (evnt == null || evnt.Handle == IntPtr.Zero) + return null; + + var nsEventHandle = gdk_quartz_event_get_nsevent (evnt.Handle); + if (nsEventHandle == IntPtr.Zero) + return null; + + return Runtime.GetNSObject<NSEvent> (nsEventHandle); + } + + NSView view; + NSView superview; + + public GtkNSViewHost (NSView view) + { + this.view = view ?? throw new ArgumentNullException (nameof (view)); + + WidgetFlags |= WidgetFlags.NoWindow; + } + + void UpdateViewFrame () + { + LogEnter (); + try { + if (view == null) + return; + + var window = GdkWindow; + var allocation = Allocation; + double x = allocation.X; + double y = allocation.Y; + + while (window != null && !gdk_window_has_native (window.Handle)) { + gdk_window_coords_to_parent (window.Handle, x, y, out var nx, out var ny); + Log ($"({x},{y}) -> ({nx},{ny})"); + x = nx; + y = ny; + window = window.Parent; + } + + view.Frame = new CGRect (x, y, allocation.Width, allocation.Height); + Log ($"Frame: {view.Frame}"); + } finally { + LogExit (); + } + } + + static NSView RecursivelyFindSubviewForPredicate (NSView view, Predicate<NSView> predicate) + { + if (predicate (view)) + return view; + + var subviews = view.Subviews; + if (subviews != null && subviews.Length > 0) { + foreach (var subview in subviews) { + if (subview != null) { + var foundView = RecursivelyFindSubviewForPredicate (subview, predicate); + if (foundView != null) + return foundView; + } + } + } + + return null; + } + + NSView GetAcceptsFirstResponderView () + => RecursivelyFindSubviewForPredicate (view, v => v.AcceptsFirstResponder ()); + + protected override void OnDestroyed () + { + LogEnter (); + + view?.RemoveFromSuperview (); + view = null; + superview = null; + + base.OnDestroyed (); + + LogExit (); + } + + protected override void OnRealized () + { + LogEnter (); + + GdkWindow = Parent.GdkWindow; + + if (GdkWindow != null && GdkWindow.Handle != IntPtr.Zero) { + var superviewHandle = gdk_quartz_window_get_nsview (GdkWindow.Handle); + if (superviewHandle != IntPtr.Zero) + superview = Runtime.GetNSObject<NSView> (superviewHandle); + } + + if (superview != null && view != null) { + view.Hidden = true; + superview.AddSubview (view); + UpdateViewFrame (); + + CanFocus = GetAcceptsFirstResponderView () != null; + } + + base.OnRealized (); + + LogExit (); + } + + protected override void OnUnrealized () + { + LogEnter (); + + if (IsMapped) + Unmap (); + + view?.RemoveFromSuperview (); + superview = null; + + base.OnUnrealized (); + LogExit (); + } + + protected override void OnMapped () + { + LogEnter (); + + view.Hidden = false; + + base.OnMapped (); + + if (IsMapped) + UpdateViewFrame (); + + LogExit (); + } + + protected override void OnUnmapped () + { + LogEnter (); + + view.Hidden = true; + + base.OnUnmapped (); + + LogExit (); + } + + protected override bool OnConfigureEvent (Gdk.EventConfigure evnt) + { + LogEnter (); + try { + return base.OnConfigureEvent (evnt); + } finally { + LogExit (); + } + } + + protected override void OnSizeRequested (ref Requisition requisition) + { + LogEnter (); + try { + if (view == null) { + Log ("Calling base. 'view' is null"); + base.OnSizeRequested (ref requisition); + return; + } + + var fittingSize = view.FittingSize; + requisition.Width = (int)fittingSize.Width; + requisition.Height = (int)fittingSize.Height; + Log ($"Setting requisition to {requisition.Width}x{requisition.Height}"); + } finally { + LogExit (); + } + } + + protected override void OnSizeAllocated (Gdk.Rectangle allocation) + { + LogEnter (); + + base.OnSizeAllocated (allocation); + + UpdateViewFrame (); + + LogExit (); + } + + protected override bool OnFocusInEvent (Gdk.EventFocus evnt) + { + LogEnter (); + try { + var acceptsFirstResponderView = GetAcceptsFirstResponderView (); + if (acceptsFirstResponderView?.Window == null) + return false; + + acceptsFirstResponderView.Window.MakeFirstResponder (acceptsFirstResponderView); + + return base.OnFocusInEvent (evnt); + } finally { + LogExit (); + } + } + + protected override bool OnFocusOutEvent (Gdk.EventFocus evnt) + { + LogEnter (); + try { + var firstResponder = view?.Window.FirstResponder as NSView; + if (firstResponder != null && view?.AncestorSharedWithView (firstResponder) == view) + firstResponder.Window.MakeFirstResponder (null); + return base.OnFocusOutEvent (evnt); + } finally { + LogExit (); + } + } + + bool ForwardEvent<TEvent> ( + TEvent evnt, + Action<NSView, NSEvent> forwardCall, + Func<TEvent, bool> baseCall) where TEvent : Gdk.Event + { + var acceptsFirstResponderView = GetAcceptsFirstResponderView (); + if (acceptsFirstResponderView == null) + return false; + + var nsEvent = GetNSEvent (evnt); + if (nsEvent == null) + return false; + + forwardCall (acceptsFirstResponderView, nsEvent); + + return baseCall (evnt); + } + + protected override bool OnKeyPressEvent (Gdk.EventKey evnt) + { + LogEnter (); + try { + return ForwardEvent ( + evnt, + (v, e) => v.KeyDown (e), + base.OnKeyReleaseEvent); + } finally { + LogExit (); + } + } + + protected override bool OnKeyReleaseEvent (Gdk.EventKey evnt) + { + LogEnter (); + try { + return ForwardEvent ( + evnt, + (v, e) => v.KeyUp (e), + base.OnKeyReleaseEvent); + } finally { + LogExit (); + } + } + + #region Tracing + + int traceDepth = 0; + + [Conditional ("DEBUG")] + void LogIndent () + => Debug.Write (new string (' ', traceDepth * 2)); + + [Conditional ("DEBUG")] + void Log (string message, [CallerMemberName] string memberName = null) + { + LogIndent (); + Debug.WriteLine ($"{memberName}: {message}"); + } + + [Conditional ("DEBUG")] + void LogEnter ([CallerMemberName] string memberName = null) + { + LogIndent (); + Debug.WriteLine ($"Enter: {memberName}"); + traceDepth++; + } + + [Conditional ("DEBUG")] + void LogExit ([CallerMemberName] string memberName = null) + { + traceDepth--; + LogIndent (); + Debug.WriteLine ($"Exit: {memberName}"); + } + + #endregion + } +}
\ No newline at end of file |