// // MacPlatformService.cs // // Author: // Geoff Norton // Michael Hutchinson // // Copyright (C) 2007-2011 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.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using AppKit; using CoreFoundation; using Foundation; using CoreGraphics; using MonoDevelop.Core; using MonoDevelop.Core.Execution; using MonoDevelop.Core.Instrumentation; using MonoDevelop.Components.Commands; using MonoDevelop.Ide; using MonoDevelop.Ide.Gui; using MonoDevelop.Ide.Commands; using MonoDevelop.Ide.Desktop; using MonoDevelop.MacInterop; using MonoDevelop.Components; using MonoDevelop.Components.MainToolbar; using MonoDevelop.Components.Extensions; using System.Runtime.InteropServices; using ObjCRuntime; using System.Diagnostics; using Xwt.Mac; using MonoDevelop.Components.Mac; using System.Reflection; using MacPlatform; using MonoDevelop.Projects; using System.Threading.Tasks; namespace MonoDevelop.MacIntegration { public class MacPlatformService : PlatformService { const string monoDownloadUrl = "http://www.mono-project.com/download/"; static readonly TimerCounter timer = InstrumentationService.CreateTimerCounter ("Mac Platform Initialization", "Platform Service"); static readonly TimerCounter mimeTimer = InstrumentationService.CreateTimerCounter ("Mac Mime Database", "Platform Service"); readonly ITimeTracker initTracker; static bool initedGlobal; bool setupFail, initedApp; // hold a reference on all observer objects generated by the notification center // NOTE: these objects should not be actively disposed on macOS 10.11 and later, unless removed manually // not keeping a reference might cause a runtime crash when observers are added: // KVO_IS_RETAINING_ALL_OBSERVERS_OF_THIS_OBJECT_IF_IT_CRASHES_AN_OBSERVER_WAS_OVERRELEASED_OR_SMASHED List notificationObservers = new List (); readonly Lazy> mimemap = new Lazy> (LoadMimeMap); static string applicationMenuName; public static string ApplicationMenuName { get { return applicationMenuName ?? BrandingService.ApplicationName; } set { if (applicationMenuName != value) { applicationMenuName = value; } } } [DllImport(Constants.ObjectiveCLibrary)] private static extern IntPtr class_getInstanceMethod(IntPtr classHandle, IntPtr Selector); [DllImport(Constants.ObjectiveCLibrary)] private static extern IntPtr method_getImplementation(IntPtr method); [DllImport(Constants.ObjectiveCLibrary)] private static extern IntPtr imp_implementationWithBlock(ref BlockLiteral block); [DllImport(Constants.ObjectiveCLibrary)] private static extern void method_setImplementation(IntPtr method, IntPtr imp); [MonoNativeFunctionWrapper] delegate void AccessibilitySetValueForAttributeDelegate (IntPtr self, IntPtr selector, IntPtr valueHandle, IntPtr attributeHandle); delegate void SwizzledAccessibilitySetValueForAttributeDelegate (IntPtr block, IntPtr self, IntPtr valueHandle, IntPtr attributeHandle); static IntPtr originalAccessibilitySetValueForAttributeMethod; void SwizzleNSApplication () { // Swizzle accessibilitySetValue:forAttribute: so that we can detect when VoiceOver gets enabled var nsApplicationClassHandle = Class.GetHandle ("NSApplication"); var accessibilitySetValueForAttributeSelector = Selector.GetHandle ("accessibilitySetValue:forAttribute:"); var accessibilitySetValueForAttributeMethod = class_getInstanceMethod (nsApplicationClassHandle, accessibilitySetValueForAttributeSelector); originalAccessibilitySetValueForAttributeMethod = method_getImplementation (accessibilitySetValueForAttributeMethod); var block = new BlockLiteral (); SwizzledAccessibilitySetValueForAttributeDelegate d = accessibilitySetValueForAttribute; block.SetupBlock (d, null); var imp = imp_implementationWithBlock (ref block); method_setImplementation (accessibilitySetValueForAttributeMethod, imp); } [MonoPInvokeCallback (typeof (SwizzledAccessibilitySetValueForAttributeDelegate))] static void accessibilitySetValueForAttribute (IntPtr block, IntPtr self, IntPtr valueHandle, IntPtr attributeHandle) { var d = Marshal.GetDelegateForFunctionPointer (originalAccessibilitySetValueForAttributeMethod); d (self, Selector.GetHandle ("accessibilitySetValue:forAttribute:"), valueHandle, attributeHandle); NSString attrString = (NSString)ObjCRuntime.Runtime.GetNSObject (attributeHandle); var val = (NSNumber)ObjCRuntime.Runtime.GetNSObject (valueHandle); if (attrString == "AXEnhancedUserInterface" && !IdeTheme.AccessibilityEnabled) { if (val.BoolValue) { ShowVoiceOverNotice (); } } AccessibilityInUse = val.BoolValue; } MacIdeAppleEvents ideAppleEvents; public MacPlatformService () { if (initedGlobal) throw new Exception ("Only one MacPlatformService instance allowed"); initedGlobal = true; initTracker = timer.BeginTiming (); var dir = Path.GetDirectoryName (typeof(MacPlatformService).Assembly.Location); if (ObjCRuntime.Dlfcn.dlopen (Path.Combine (dir, "libxammac.dylib"), 0) == IntPtr.Zero) LoggingService.LogFatalError ("Unable to load libxammac"); CheckGtkVersion (2, 24, 14); Xwt.Toolkit.CurrentEngine.RegisterBackend (); Xwt.Toolkit.CurrentEngine.RegisterBackend (); Xwt.Toolkit.CurrentEngine.RegisterBackend (); var description = XamMacBuildInfo.Value; if (string.IsNullOrEmpty (description)) { LoggingService.LogWarning ("Failed to parse version of Xamarin.Mac used at runtime"); } else { LoggingService.LogInfo ("Using {0}", description); } } static string GetInfoPart (string line) { return line.Split (':') [1].Trim (); } static Lazy XamMacBuildInfo = new Lazy (() => { const string buildInfoResource = "Xamarin.Mac.buildinfo"; var asm = System.Reflection.Assembly.GetExecutingAssembly (); string version, hash, branch; try { using (var stream = asm.GetManifestResourceStream (buildInfoResource)) using (var sr = new StreamReader (stream)) { // Version: 4.4.0.36 // Hash: 0c7c49a6 // Branch: master // Build date: 2018 - 03 - 12 15:24:46 - 0400 -- discarded version = GetInfoPart (sr.ReadLine ()); hash = GetInfoPart (sr.ReadLine ()); branch = GetInfoPart (sr.ReadLine ()); return $"Xamarin.Mac {version} ({branch} / {hash})"; } } catch { return string.Empty; } }); internal override string GetNativeRuntimeDescription () { return XamMacBuildInfo.Value; } static void CheckGtkVersion (uint major, uint minor, uint micro) { // to require exact version, also check //: || Gtk.Global.CheckVersion (major, minor, micro + 1) == null // if (Gtk.Global.CheckVersion (major, minor, micro) != null) { LoggingService.LogFatalError ( "GTK+ version is incompatible with required version {0}.{1}.{2}.", major, minor, micro ); var downloadButton = new AlertButton ("Download Mono Framework", null); if (downloadButton == MessageService.GenericAlert ( Stock.Error, GettextCatalog.GetString ("Some dependencies need to be updated"), GettextCatalog.GetString ( "{0} requires a newer version of GTK+, which is included with the Mono Framework. Please " + "download and install the latest stable Mono Framework package and restart {0}.", BrandingService.ApplicationName ), new AlertButton ("Quit", null), downloadButton)) { OpenUrl (monoDownloadUrl); } Environment.Exit (1); } } const string FoundationLib = "/System/Library/Frameworks/Foundation.framework/Versions/Current/Foundation"; delegate void NSUncaughtExceptionHandler (IntPtr exception); static readonly NSUncaughtExceptionHandler uncaughtHandler = HandleUncaughtException; static NSUncaughtExceptionHandler oldHandler; static void HandleUncaughtException(IntPtr exceptionPtr) { // It's non-null guaranteed by objc. Debug.Assert (exceptionPtr != IntPtr.Zero); var nsException = ObjCRuntime.Runtime.GetNSObject (exceptionPtr); try { throw new MarshalledObjCException (nsException); } catch (MarshalledObjCException e) { LoggingService.LogFatalError ("Unhandled ObjC Exception", e); // Is there a way to figure out if it's going to crash us? Maybe check MarshalObjectiveCExceptionMode and MarshalManagedExceptionMode? } // Invoke the default xamarin.mac one, so if it bubbles up an exception, the caller receives it. oldHandler?.Invoke (exceptionPtr); } sealed class MarshalledObjCException : ObjCException { public MarshalledObjCException (NSException exception) : base (exception) { const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetField; var trace = new [] { new StackTrace (true), }; //// Otherwise exception stacktrace is not gathered. typeof (Exception) .InvokeMember ("captured_traces", flags, null, this, new object [] { trace }); } } [DllImport (FoundationLib)] static extern void NSSetUncaughtExceptionHandler (NSUncaughtExceptionHandler handler); [DllImport (FoundationLib)] static extern NSUncaughtExceptionHandler NSGetUncaughtExceptionHandler (); static void RegisterUncaughtExceptionHandler () { oldHandler = NSGetUncaughtExceptionHandler (); NSSetUncaughtExceptionHandler (uncaughtHandler); ObjCRuntime.Runtime.MarshalManagedException += (sender, args) => { LoggingService.LogInternalError (args.Exception); }; ObjCRuntime.Runtime.MarshalObjectiveCException += (sender, args) => { try { throw new MarshalledObjCException (args.Exception); } catch (MarshalledObjCException e) { LoggingService.LogInternalError ("MarshalObjCException", e); } }; } public override Xwt.Toolkit LoadNativeToolkit () { var loaded = NativeToolkitHelper.LoadCocoa (); loaded.RegisterBackend (); loaded.RegisterBackend (); loaded.RegisterBackend (); RegisterUncaughtExceptionHandler (); // We require Xwt.Mac to initialize MonoMac before we can execute any code using MonoMac initTracker.Trace ("Installing App Event Handlers"); GlobalSetup (); initTracker.End (); var appDelegate = NSApplication.SharedApplication.Delegate as Xwt.Mac.AppDelegate; if (appDelegate != null) { appDelegate.Terminating += async (object o, TerminationEventArgs e) => { if (MonoDevelop.Ide.IdeApp.IsRunning) { // If GLib the mainloop is still running that means NSApplication.Terminate() was called // before Gtk.Application.Quit(). Cancel Cocoa termination and exit the mainloop. e.Reply = NSApplicationTerminateReply.Cancel; await IdeApp.Exit(); } else { // The mainloop has already exited and we've already cleaned up our application state // so it's now safe to terminate Cocoa. e.Reply = NSApplicationTerminateReply.Now; } }; appDelegate.ShowDockMenu += AppDelegate_ShowDockMenu; } // Listen to the AtkCocoa notification for the presence of VoiceOver SwizzleNSApplication (); var nc = NSNotificationCenter.DefaultCenter; notificationObservers.Add (nc.AddObserver ((NSString)"AtkCocoaAccessibilityEnabled", (NSNotification) => { LoggingService.LogInfo ($"VoiceOver on {IdeTheme.AccessibilityEnabled}"); if (!IdeTheme.AccessibilityEnabled) { ShowVoiceOverNotice (); } }, NSApplication.SharedApplication)); // Now that Cocoa has been initialized we can check whether the keyboard focus mode is turned on // See System Preferences - Keyboard - Shortcuts - Full Keyboard Access var keyboardMode = NSUserDefaults.StandardUserDefaults.IntForKey ("AppleKeyboardUIMode"); // 0 - Text boxes and lists only // 2 - All controls // 3 - All controls + keyboard access if (keyboardMode != 0) { Gtk.Rc.ParseString ("style \"default\" { engine \"xamarin\" { focusstyle = 2 } }"); Gtk.Rc.ParseString ("style \"radio-or-check-box\" { engine \"xamarin\" { focusstyle = 2 } } "); } AccessibilityKeyboardFocusInUse = (keyboardMode != 0); // Disallow window tabbing globally if (MacSystemInformation.OsVersion >= MacSystemInformation.Sierra) NSWindow.AllowsAutomaticWindowTabbing = false; // At this point, Cocoa should have been initialized; it is initialized along with Gtk+ at the beginning of IdeStartup.Run // If LaunchReason is still Unknown at this point, it means we have missed the NSApplicationDidLaunch notification for some reason and // we fall back to the AppDelegate's version to unblock anything waiting for that notification. if (IdeApp.LaunchReason == IdeApp.LaunchType.Unknown) { if (appDelegate.LaunchReason != AppDelegate.LaunchType.Unknown) { IdeApp.LaunchReason = appDelegate.LaunchReason == AppDelegate.LaunchType.Normal ? IdeApp.LaunchType.Normal : IdeApp.LaunchType.LaunchedFromFileManager; } else { // Fall back to Normal if even the app delegate doesn't know LoggingService.LogWarning ("Unknown startup reason, assuming Normal"); IdeApp.LaunchReason = IdeApp.LaunchType.Normal; } } ideAppleEvents = new MacIdeAppleEvents (); return loaded; } void AppDelegate_ShowDockMenu (object sender, ShowDockMenuArgs e) { if (((FilePath)NSBundle.MainBundle.BundlePath).Extension != ".app") return; var menu = new NSMenu (); var newInstanceMenuItem = new NSMenuItem (); newInstanceMenuItem.Title = GettextCatalog.GetString ("New Instance"); newInstanceMenuItem.Activated += NewInstanceMenuItem_Activated; menu.AddItem (newInstanceMenuItem); e.DockMenu = menu; } static void NewInstanceMenuItem_Activated (object sender, EventArgs e) { var bundlePath = NSBundle.MainBundle.BundlePath; NSWorkspace.SharedWorkspace.LaunchApplication (NSUrl.FromFilename (bundlePath), NSWorkspaceLaunchOptions.NewInstance, new NSDictionary (), out NSError error); if (error != null) LoggingService.LogError ($"Failed to start new instance: {error.LocalizedDescription}"); } // The Enabled key needs to be controlled through NSUserDefaults so that it can be // set from the command line. This lets users who require accessibility features to // enabled to control it through the commandline, if it has been disabled from inside the software const string EnabledKey = "com.monodevelop.AccessibilityEnabled"; const string VoiceOverNoticeShownKey = "MonoDevelop.VoiceOver.Show"; static void ShowVoiceOverNotice () { if (PropertyService.Get (VoiceOverNoticeShownKey, false)) { return; } // Show the VoiceOver notice once PropertyService.Set (VoiceOverNoticeShownKey, true); var alert = new NSAlert (); alert.MessageText = GettextCatalog.GetString ("Assistive Technology Detected"); alert.InformativeText = GettextCatalog.GetString ("{0} has detected an assistive technology (such as VoiceOver) is running. Do you want to restart {0} and enable the accessibility features?", BrandingService.ApplicationName); alert.AddButton (GettextCatalog.GetString ("Restart and enable")); alert.AddButton (GettextCatalog.GetString ("No")); var result = alert.RunModal (); switch (result) { case 1000: NSUserDefaults defaults = NSUserDefaults.StandardUserDefaults; defaults.SetBool (true, EnabledKey); defaults.Synchronize (); IdeApp.Restart (); break; default: break; } } internal override void MakeAccessibilityAnnouncement (string text) { using var message = new NSString (text); using var dictionary = new NSDictionary (NSAccessibilityNotificationUserInfoKeys.AnnouncementKey, message, NSAccessibilityNotificationUserInfoKeys.PriorityKey, NSAccessibilityPriorityLevel.High); NSAccessibility.PostNotification (NSApplication.SharedApplication.AccessibilityMainWindow, NSAccessibilityNotifications.AnnouncementRequestedNotification, dictionary); } protected override string OnGetMimeTypeForUri (string uri) { var ext = Path.GetExtension (uri); string mime; if (ext != null && mimemap.Value.TryGetValue (ext, out mime)) return mime; return null; } public override void ShowUrl (string url) { OpenUrl (url); } internal static void OpenUrl (string url) { Gtk.Application.Invoke ((o, args) => { NSWorkspace.SharedWorkspace.OpenUrl (new NSUrl (url)); }); } public override void OpenFile (string filename) { Gtk.Application.Invoke ((o, args) => { NSWorkspace.SharedWorkspace.OpenFile (filename); }); } public override string DefaultMonospaceFont { get { return "Menlo 12"; } } public override string Name { get { return "OSX"; } } static Dictionary LoadMimeMap () { var map = new Dictionary (); // All recent Macs should have this file; if not we'll just die silently if (!File.Exists ("/etc/apache2/mime.types")) { LoggingService.LogError ("Apache mime database is missing"); return map; } using var time = mimeTimer.BeginTiming (); try { var loader = new MimeMapLoader (map); loader.LoadMimeMap ("/etc/apache2/mime.types"); } catch (Exception ex){ LoggingService.LogError ("Could not load Apache mime database", ex); } return map; } string currentCommandMenuPath, currentAppMenuPath; public override bool SetGlobalMenu (CommandManager commandManager, string commandMenuAddinPath, string appMenuAddinPath) { if (setupFail) return false; // avoid reinitialization of the same menu structure if (initedApp == true && currentCommandMenuPath == commandMenuAddinPath && currentAppMenuPath == appMenuAddinPath) return true; try { InitApp (commandManager); NSApplication.SharedApplication.HelpMenu = null; var rootMenu = NSApplication.SharedApplication.MainMenu; if (rootMenu == null) { rootMenu = new NSMenu (); } else { rootMenu.RemoveAllItems (); } CommandEntrySet appCes = commandManager.CreateCommandEntrySet (appMenuAddinPath); rootMenu.AddItem (new MDSubMenuItem (commandManager, appCes)); CommandEntrySet ces = commandManager.CreateCommandEntrySet (commandMenuAddinPath); foreach (CommandEntry ce in ces) { var item = new MDSubMenuItem (commandManager, (CommandEntrySet)ce); rootMenu.AddItem (item); if (ce.CommandId as string == "Help" && item.HasSubmenu && NSApplication.SharedApplication.HelpMenu == null) NSApplication.SharedApplication.HelpMenu = item.Submenu; } // Assign the main menu after loading the items. Otherwise a weird application menu appears. if (NSApplication.SharedApplication.MainMenu == null) NSApplication.SharedApplication.MainMenu = rootMenu; currentCommandMenuPath = commandMenuAddinPath; currentAppMenuPath = appMenuAddinPath; } catch (Exception ex) { try { var m = NSApplication.SharedApplication.MainMenu; if (m != null) { m.Dispose (); } NSApplication.SharedApplication.MainMenu = null; m = NSApplication.SharedApplication.HelpMenu; if (m != null) { m.Dispose (); } NSApplication.SharedApplication.HelpMenu = null; } catch (Exception e2) { LoggingService.LogError ("Could not uninstall global menu", e2); } LoggingService.LogError ("Could not install global menu", ex); setupFail = true; return false; } return true; } static void OnCommandActivating (object sender, CommandActivationEventArgs args) { if (args.Source != CommandSource.Keybinding) return; var m = NSApplication.SharedApplication.MainMenu; if (m != null) { foreach (NSMenuItem item in m.Items) { var submenu = item.Submenu as MDMenu; if (submenu != null && submenu.FlashIfContainsCommand (args.CommandId)) return; } } } void InitApp (CommandManager commandManager) { if (initedApp) return; commandManager.CommandActivating += OnCommandActivating; //mac-ify these command names commandManager.GetCommand (EditCommands.MonodevelopPreferences).Text = GettextCatalog.GetString ("Preferences..."); commandManager.GetCommand (EditCommands.DefaultPolicies).Text = GettextCatalog.GetString ("Policies..."); commandManager.GetCommand (HelpCommands.About).Text = GetAboutCommandText (); commandManager.GetCommand (MacIntegrationCommands.HideWindow).Text = GetHideWindowCommandText (); commandManager.GetCommand (ToolCommands.AddinManager).Text = GettextCatalog.GetString ("Extensions..."); initedApp = true; IdeApp.Initialized += (s, e) => { IdeApp.Workbench.RootWindow.DeleteEvent += HandleDeleteEvent; if (MacSystemInformation.OsVersion >= MacSystemInformation.Lion) { IdeApp.Workbench.RootWindow.Realized += (sender, args) => { var win = GtkQuartz.GetWindow ((Gtk.Window)sender); win.CollectionBehavior |= NSWindowCollectionBehavior.FullScreenPrimary; if (MacSystemInformation.OsVersion >= MacSystemInformation.Sierra) win.TabbingMode = NSWindowTabbingMode.Disallowed; }; } }; PatchGtkTheme (); if (MacSystemInformation.OsVersion >= MacSystemInformation.Mojave) { IdeTheme.HighContrastThemeEnabled = GetIsHighContrastActive (); notificationObservers.Add (NSApplication.SharedApplication.AddObserver ("effectiveAppearance", NSKeyValueObservingOptions.New, notif => Core.Runtime.RunInMainThread (() => { IdeTheme.HighContrastThemeEnabled = GetIsHighContrastActive (); PatchGtkTheme (); }) )); } else { IdeTheme.HighContrastThemeEnabled = false; notificationObservers.Add (NSNotificationCenter.DefaultCenter.AddObserver (NSCell.ControlTintChangedNotification, notif => Core.Runtime.RunInMainThread ( delegate { Styles.LoadStyle (); PatchGtkTheme (); }))); } if (MacSystemInformation.OsVersion < MacSystemInformation.Mojave) { // the shared color panel has full automatic theme support on Mojave Styles.Changed += (s, a) => { var colorPanel = NSColorPanel.SharedColorPanel; if (colorPanel.ContentView?.Superview?.Window == null) LoggingService.LogWarning ("Updating shared color panel appearance failed, no valid window."); IdeTheme.ApplyTheme (colorPanel.ContentView.Superview.Window); var appearance = colorPanel.ContentView.Superview.Window.Appearance; if (appearance == null) appearance = IdeTheme.GetAppearance (); // The subviews of the shared NSColorPanel do not inherit the appearance of the main panel window // and need to be updated recursively. UpdateColorPanelSubviewsAppearance (colorPanel.ContentView.Superview, appearance); }; } // FIXME: Immediate theme switching disabled, until NSAppearance issues are fixed //IdeApp.Preferences.UserInterfaceTheme.Changed += (s,a) => PatchGtkTheme (); } static bool GetIsHighContrastActive () { var highContrastAppearances = new string [] { NSAppearance.NameAccessibilityHighContrastAqua, NSAppearance.NameAccessibilityHighContrastDarkAqua, NSAppearance.NameAccessibilityHighContrastVibrantDark, NSAppearance.NameAccessibilityHighContrastVibrantLight, }; // FindBestMatch will return the best matching a11y appearance or null if no high contrast appearance is in use return NSApplication.SharedApplication.EffectiveAppearance.FindBestMatch (highContrastAppearances) != null; } static void UpdateColorPanelSubviewsAppearance (NSView view, NSAppearance appearance) { if (view.Class.Name == "NSPageableTableView") ((NSTableView)view).BackgroundColor = Xwt.Mac.Util.ToNSColor (Styles.BackgroundColor); view.Appearance = appearance; foreach (var subview in view.Subviews) UpdateColorPanelSubviewsAppearance (subview, appearance); } static string GetAboutCommandText () { return GettextCatalog.GetString ("About {0}", ApplicationMenuName); } static string GetHideWindowCommandText () { return GettextCatalog.GetString ("Hide {0}", ApplicationMenuName); } // VV/VK: Disable tint based color generation // This will dynamically generate a gtkrc for certain widgets using system control colors. void PatchGtkTheme () { // string color_hex, text_hex; // // if (MonoDevelop.Core.Platform.OSVersion >= MonoDevelop.Core.MacSystemInformation.Yosemite) { // NSControlTint tint = NSColor.CurrentControlTint; // NSColor text = NSColor.SelectedMenuItemText.UsingColorSpace (NSColorSpace.GenericRGBColorSpace); // NSColor color = tint == NSControlTint.Blue ? NSColor.SelectedMenuItem.UsingColorSpace (NSColorSpace.GenericRGBColorSpace) : NSColor.SelectedMenuItem.UsingColorSpace (NSColorSpace.DeviceWhite); // // color_hex = ConvertColorToHex (color); // text_hex = ConvertColorToHex (text); // } else { // color_hex = "#c5d4e0"; // text_hex = "#000"; // } // // string gtkrc = String.Format (@" // style ""treeview"" = ""default"" {{ // base[SELECTED] = ""{0}"" // base[ACTIVE] = ""{0}"" // text[SELECTED] = ""{1}"" // text[ACTIVE] = ""{1}"" // engine ""xamarin"" {{ // roundness = 0 // gradient_shades = {{ 1.01, 1.01, 1.01, 1.01 }} // glazestyle = 1 // }} // }} // // widget_class ""*.*"" style ""treeview"" // ", // color_hex, // text_hex // ); // // Gtk.Rc.ParseString (gtkrc); } static TimeToCodeMetadata.DocumentType GetDocumentTypeFromFilename (string filename) { if (Projects.Services.ProjectService.IsWorkspaceItemFile (filename) || Projects.Services.ProjectService.IsSolutionItemFile (filename)) { return TimeToCodeMetadata.DocumentType.Solution; } return TimeToCodeMetadata.DocumentType.File; } void GlobalSetup () { //FIXME: should we remove these when finalizing? try { ApplicationEvents.Reopen += delegate (object sender, ApplicationEventArgs e) { e.Handled = true; IdeApp.BringToFront (); }; ApplicationEvents.OpenDocuments += delegate (object sender, ApplicationDocumentEventArgs e) { //OpenFiles may pump the mainloop, but can't do that from an AppleEvent GLib.Idle.Add (delegate { Ide.WelcomePage.WelcomePageService.HideWelcomePageOrWindow (); var trackTTC = IdeStartupTracker.StartupTracker.StartTimeToCodeLoadTimer (); IdeApp.OpenFilesAsync (e.Documents.Select ( doc => new FileOpenInformation (doc.Key, null, doc.Value, 1, OpenDocumentOptions.DefaultInternal)), null ).ContinueWith ((result) => { if (!trackTTC) { return; } var firstFile = e.Documents.First ().Key; IdeStartupTracker.StartupTracker.TrackTimeToCode (GetDocumentTypeFromFilename (firstFile)); }); return false; }); e.Handled = true; }; ApplicationEvents.OpenUrls += delegate (object sender, ApplicationUrlEventArgs e) { GLib.Idle.Add (delegate { var trackTTC = IdeStartupTracker.StartupTracker.StartTimeToCodeLoadTimer (); // Open files via the monodevelop:// URI scheme, compatible with the // common TextMate scheme: http://blog.macromates.com/2007/the-textmate-url-scheme/ IdeApp.OpenFilesAsync (e.Urls.Select (url => { try { var uri = new Uri (url); if (uri.Host != "open") return null; var qs = System.Web.HttpUtility.ParseQueryString (uri.Query); var fileUri = new Uri (qs ["file"]); int line, column; if (!Int32.TryParse (qs ["line"], out line)) line = 1; if (!Int32.TryParse (qs ["column"], out column)) column = 1; return new FileOpenInformation (Uri.UnescapeDataString(fileUri.AbsolutePath), null, line, column, OpenDocumentOptions.DefaultInternal); } catch (Exception ex) { LoggingService.LogError ("Invalid TextMate URI: " + url, ex); return null; } }).Where (foi => foi != null), null).ContinueWith ((result) => { if (!trackTTC) { return; } var firstFile = e.Urls.First (); IdeStartupTracker.StartupTracker.TrackTimeToCode (GetDocumentTypeFromFilename (firstFile)); }); return false; }); }; //if not running inside an app bundle (at dev time), need to do some additional setup if (NSBundle.MainBundle.InfoDictionary ["CFBundleIdentifier"] == null) { SetupWithoutBundle (); } else { SetupDockIcon (); } } catch (Exception ex) { LoggingService.LogError ("Could not install app event handlers", ex); setupFail = true; } } private static NSImage applicationIcon; internal static NSImage ApplicationIcon { get { if (applicationIcon == null) { // use the bundle icon by default // if run from a bundle, this will be the icon from the bundle icon file. // if not run from a bundle, this will be the default file icon of the mono framework folder. applicationIcon = NSWorkspace.SharedWorkspace.IconForFile (NSBundle.MainBundle.BundlePath); } return applicationIcon; } private set { NSApplication.SharedApplication.ApplicationIconImage = applicationIcon = value; } } static void SetupDockIcon () { NSObject initialBundleIconFileValue; // Don't do anything if we're inside an app bundle. if (NSBundle.MainBundle.InfoDictionary.TryGetValue (new NSString ("CFBundleIconFile"), out initialBundleIconFileValue)) { return; } // Setup without bundle. FilePath exePath = System.Reflection.Assembly.GetExecutingAssembly ().Location; string iconName = BrandingService.GetString ("ApplicationIcon"); string iconFile = null; if (iconName != null) { iconFile = BrandingService.GetFile (iconName); } else { // assume running from build directory var mdSrcMain = exePath.ParentDirectory.ParentDirectory.ParentDirectory; iconFile = mdSrcMain.Combine ("theme-icons", "Mac", "monodevelop.icns"); } if (File.Exists (iconFile)) { ApplicationIcon = new NSImage (iconFile, lazy: true); } } static void SetupWithoutBundle () { // set a bundle IDE to prevent NSProgress crash // https://bugzilla.xamarin.com/show_bug.cgi?id=8850 NSBundle.MainBundle.InfoDictionary ["CFBundleIdentifier"] = new NSString ("com.xamarin.monodevelop"); SetupDockIcon (); } static FilePath GetAppBundleRoot (FilePath path) { do { if (path.Extension == ".app") return path; } while ((path = path.ParentDirectory).IsNotNull); return null; } [GLib.ConnectBefore] static async void HandleDeleteEvent (object o, Gtk.DeleteEventArgs args) { args.RetVal = true; if (await IdeApp.Workspace.Close ()) { IdeApp.Workbench.Hide (); } } public static Gdk.Pixbuf GetPixbufFromNSImageRep (NSImageRep rep, int width, int height) { var rect = new CGRect (0, 0, width, height); var bitmap = rep as NSBitmapImageRep; try { if (bitmap == null) { using (var cgi = rep.AsCGImage (ref rect, null, null)) { if (cgi == null) return null; bitmap = new NSBitmapImageRep (cgi); } } return GetPixbufFromNSBitmapImageRep (bitmap, width, height); } finally { if (bitmap != null) bitmap.Dispose (); } } public static Gdk.Pixbuf GetPixbufFromNSImage (NSImage icon, int width, int height) { var rect = new CGRect (0, 0, width, height); var rep = icon.BestRepresentation (rect, null, null); var bitmap = rep as NSBitmapImageRep; try { if (bitmap == null) { if (rep != null) rep.Dispose (); using (var cgi = icon.AsCGImage (ref rect, null, null)) { if (cgi == null) return null; bitmap = new NSBitmapImageRep (cgi); } } return GetPixbufFromNSBitmapImageRep (bitmap, width, height); } finally { if (bitmap != null) bitmap.Dispose (); } } static Gdk.Pixbuf GetPixbufFromNSBitmapImageRep (NSBitmapImageRep bitmap, int width, int height) { byte[] data; using (var tiff = bitmap.TiffRepresentation) { data = new byte[tiff.Length]; System.Runtime.InteropServices.Marshal.Copy (tiff.Bytes, data, 0, data.Length); } int pw = (int)bitmap.PixelsWide, ph = (int)bitmap.PixelsHigh; var pixbuf = new Gdk.Pixbuf (data, pw, ph); // if one dimension matches, and the other is same or smaller, use as-is if ((pw == width && ph <= height) || (ph == height && pw <= width)) return pixbuf; // otherwise scale proportionally such that the largest dimension matches the desired size if (pw == ph) { pw = width; ph = height; } else if (pw > ph) { ph = (int) (width * ((float) ph / pw)); pw = width; } else { pw = (int) (height * ((float) pw / ph)); ph = height; } var scaled = pixbuf.ScaleSimple (pw, ph, Gdk.InterpType.Bilinear); pixbuf.Dispose (); return scaled; } protected override Xwt.Drawing.Image OnGetIconForFile (string filename) { //this only works on MacOS 10.6.0 and greater if (MacSystemInformation.OsVersion < MacSystemInformation.SnowLeopard) return base.OnGetIconForFile (filename); NSImage icon = null; if (Path.IsPathRooted (filename) && File.Exists (filename)) { icon = NSWorkspace.SharedWorkspace.IconForFile (filename); } else { string extension = Path.GetExtension (filename); if (!string.IsNullOrEmpty (extension)) icon = NSWorkspace.SharedWorkspace.IconForFileType (extension); } if (icon == null) { return base.OnGetIconForFile (filename); } int w, h; if (!Gtk.Icon.SizeLookup (Gtk.IconSize.Menu, out w, out h)) { w = h = 22; } var res = GetPixbufFromNSImage (icon, w, h); return res != null ? res.ToXwtImage () : base.OnGetIconForFile (filename); } public override ProcessAsyncOperation StartConsoleProcess (string command, string arguments, string workingDirectory, IDictionary environmentVariables, string title, bool pauseWhenFinished) { return new MacExternalConsoleProcess (command, arguments, workingDirectory, environmentVariables, title, pauseWhenFinished); } public override bool CanOpenTerminal { get { return true; } } public override void OpenTerminal (FilePath directory, IDictionary environmentVariables, string title) { string tabId, windowId; MacExternalConsoleProcess.RunTerminal ( null, null, directory, environmentVariables, title, false, out tabId, out windowId ); } public override IEnumerable GetApplications (string filename) { return GetApplications (filename, DesktopApplicationRole.All); } public override IEnumerable GetApplications (string filename, DesktopApplicationRole role) { static global::CoreServices.LSRoles ToLSRoles (DesktopApplicationRole role) { var lsRole = global::CoreServices.LSRoles.None; if (role == DesktopApplicationRole.All) return global::CoreServices.LSRoles.All; if (role.HasFlag (DesktopApplicationRole.Viewer)) lsRole |= global::CoreServices.LSRoles.Viewer; if (role.HasFlag (DesktopApplicationRole.Editor)) lsRole |= global::CoreServices.LSRoles.Editor; if (role.HasFlag (DesktopApplicationRole.Shell)) lsRole |= global::CoreServices.LSRoles.Shell; return lsRole; } //FIXME: we should disambiguate dupliacte apps in different locations and display both //for now, just filter out the duplicates var checkUniqueName = new HashSet (); var checkUniquePath = new HashSet (); //FIXME: bundle path is wrong because of how MD is built into an app //var thisPath = NSBundle.MainBundle.BundleUrl.Path; //checkUniquePath.Add (thisPath); checkUniqueName.Add ("MonoDevelop"); checkUniqueName.Add (BrandingService.ApplicationName); NSUrl url = CreateNSUrl (filename); var def = global::CoreServices.LaunchServices.GetDefaultApplicationUrlForUrl (url); var apps = new List (); var retrievedApps = global::CoreServices.LaunchServices.GetApplicationUrlsForUrl (url, ToLSRoles (role)); if (retrievedApps != null) { foreach (var app in retrievedApps) { if (string.IsNullOrEmpty (app.Path) || !checkUniquePath.Add (app.Path)) continue; if (checkUniqueName.Add (app.LastPathComponent)) { apps.Add (new MacDesktopApplication (app.Path, Path.GetFileNameWithoutExtension (app.LastPathComponent), def != null && def == app)); } } } apps.Sort ((DesktopApplication a, DesktopApplication b) => { int r = a.IsDefault.CompareTo (b.IsDefault); if (r != 0) return -r; return a.DisplayName.CompareTo (b.DisplayName); }); return apps; } static NSUrl CreateNSUrl (string filename) { if (Uri.TryCreate (filename, UriKind.Absolute, out Uri hyperlink) && !hyperlink.IsFile) return NSUrl.FromString (filename); return NSUrl.FromFilename (filename); } class MacDesktopApplication : DesktopApplication { public MacDesktopApplication (string app, string name, bool isDefault) : base (app, name, isDefault) { } public override void Launch (params string[] files) { NSWorkspace.SharedWorkspace.OpenUrls ( Array.ConvertAll (files, file => CreateNSUrl (file)), NSBundle.FromPath (Id).BundleIdentifier, NSWorkspaceLaunchOptions.Default, NSAppleEventDescriptor.DescriptorWithBoolean (true)); } } public override Xwt.Rectangle GetUsableMonitorGeometry (int screenNumber, int monitorNumber) { var screen = Gdk.Display.Default.GetScreen (screenNumber); Gdk.Rectangle ygeometry = screen.GetMonitorGeometry (monitorNumber); Gdk.Rectangle xgeometry = screen.GetMonitorGeometry (0); NSScreen nss = NSScreen.Screens[monitorNumber]; var visible = nss.VisibleFrame; var frame = nss.Frame; // Note: Frame and VisibleFrame rectangles are relative to monitor 0, but we need absolute // coordinates. visible.X += xgeometry.X; frame.X += xgeometry.X; // VisibleFrame.Y is the height of the Dock if it is at the bottom of the screen, so in order // to get the menu height, we just figure out the difference between the visibleFrame height // and the actual frame height, then subtract the Dock height. // // We need to swap the Y offset with the menu height because our callers expect the Y offset // to be from the top of the screen, not from the bottom of the screen. nfloat x, y, width, height; if (visible.Height <= frame.Height) { var dockHeight = visible.Y - frame.Y; var menubarHeight = (frame.Height - visible.Height) - dockHeight; height = frame.Height - menubarHeight - dockHeight; y = ygeometry.Y + menubarHeight; } else { height = frame.Height; y = ygeometry.Y; } // Takes care of the possibility of the Dock being positioned on the left or right edge of the screen. width = NMath.Min (visible.Width, frame.Width); x = NMath.Max (visible.X, frame.X); return new Xwt.Rectangle ((int) x, (int) y, (int) width, (int) height); } internal override void GrabDesktopFocus (Gtk.Window window) { window.Present (); NSApplication.SharedApplication.ActivateIgnoringOtherApps (true); } public override Window GetParentForModalWindow () { try { var window = NSApplication.SharedApplication.ModalWindow; if (window != null) return window; } catch (Exception e) { LoggingService.LogInternalError ("Getting SharedApplication.ModalWindow failed", e); } try { var window = NSApplication.SharedApplication.KeyWindow; if (window != null) return window; } catch (Exception e) { LoggingService.LogInternalError ("Getting SharedApplication.KeyWindow failed", e); } try { var window = NSApplication.SharedApplication.MainWindow; if (window != null) return window; } catch (Exception e) { LoggingService.LogInternalError ("Getting SharedApplication.MainWindow failed", e); } return null; } bool HasAnyDockWindowFocused () { foreach (var window in Gtk.Window.ListToplevels ()) { // Gtk.Window.HasToplevelFocus may return false for a window that embeds a Cocoa view if (!window.HasToplevelFocus && GtkQuartz.GetWindow (window) != NSApplication.SharedApplication.KeyWindow) { continue; } if (window is Components.Docking.DockFloatingWindow floatingWindow) { return true; } if (window is IdeWindow ideWindow && ideWindow.Child is Components.Docking.AutoHideBox) { return true; } } return false; } bool CheckIfTopWindowIsWorkbench () { foreach (var window in Gtk.Window.ListToplevels ()) { if (!window.HasToplevelFocus) { continue; } if (window is DefaultWorkbench) { return true; } } return false; } public override Window GetFocusedTopLevelWindow () { if (NSApplication.SharedApplication.KeyWindow != null) { if (IdeApp.Workbench?.RootWindow?.Visible == true) { //if is a docking window then return the current root window if (CheckIfTopWindowIsWorkbench () || HasAnyDockWindowFocused ()) { return MessageService.RootWindow; } } return NSApplication.SharedApplication.KeyWindow; } return null; } public override void FocusWindow (Window window) { try { NSWindow nswindow = window; // will also get an NSWindow from a Gtk.Window if (nswindow != null) { nswindow.MakeKeyAndOrderFront (nswindow); return; } } catch (Exception ex) { LoggingService.LogError ("Focusing window failed: not an NSWindow", ex); } base.FocusWindow (window); } static Cairo.Color ConvertColor (NSColor color) { nfloat r, g, b, a; if (color.ColorSpaceName == NSColorSpace.DeviceWhite) { a = 1.0f; r = g = b = color.WhiteComponent; } else { color.GetRgba (out r, out g, out b, out a); } return new Cairo.Color (r, g, b, a); } static string ConvertColorToHex (NSColor color) { nfloat r, g, b, a; if (color.ColorSpaceName == NSColorSpace.DeviceWhite) { a = 1.0f; r = g = b = color.WhiteComponent; } else { color.GetRgba (out r, out g, out b, out a); } return String.Format ("#{0}{1}{2}", ((int)(r * 255)).ToString ("x2"), ((int)(g * 255)).ToString ("x2"), ((int)(b * 255)).ToString ("x2") ); } internal static int GetTitleBarHeight () { var frame = new CGRect (0, 0, 100, 100); var rect = NSWindow.ContentRectFor (frame, NSWindowStyle.Titled); return (int)(frame.Height - rect.Height); } internal static int GetTitleBarHeight (NSWindow w) { int height = 0; if (w.StyleMask.HasFlag (NSWindowStyle.Titled)) height += GetTitleBarHeight (); if (w.Toolbar != null) { var rect = NSWindow.ContentRectFor (w.Frame, w.StyleMask); height += (int)(rect.Height - w.ContentView.Frame.Height); } return height; } internal static NSImage LoadImage (string resource) { using (var stream = typeof (MacPlatformService).Assembly.GetManifestResourceStream (resource)) using (NSData data = NSData.FromStream (stream)) { return new NSImage (data); } } internal override void SetMainWindowDecorations (Gtk.Window window) { NSWindow w = GtkQuartz.GetWindow (window); w.IsOpaque = true; w.StyleMask |= NSWindowStyle.UnifiedTitleAndToolbar; IdeTheme.ApplyTheme (w); } internal override void RemoveWindowShadow (Gtk.Window window) { if (window == null) throw new ArgumentNullException (nameof(window)); var w = GtkQuartz.GetWindow (window); if (w != null) w.HasShadow = false; } internal override IMainToolbarView CreateMainToolbar (Gtk.Window window) { return new MonoDevelop.MacIntegration.MainToolbar.MainToolbar (window); } internal override void AttachMainToolbar (Gtk.VBox parent, IMainToolbarView toolbar) { var nativeToolbar = (MonoDevelop.MacIntegration.MainToolbar.MainToolbar)toolbar; NSWindow w = GtkQuartz.GetWindow (nativeToolbar.gtkWindow); if (MacSystemInformation.OsVersion >= MacSystemInformation.Yosemite) w.TitleVisibility = NSWindowTitleVisibility.Hidden; w.Toolbar = nativeToolbar.widget; nativeToolbar.Initialize (); } protected override RecentFiles CreateRecentFilesProvider () { return new FdoRecentFiles (UserProfile.Current.LocalConfigDir.Combine ("RecentlyUsed.xml")); } public override bool GetIsFullscreen (Window window) { if (MacSystemInformation.OsVersion < MacSystemInformation.Lion) { return base.GetIsFullscreen (window); } NSWindow nswin = window; return (nswin.StyleMask & NSWindowStyle.FullScreenWindow) != 0; } public override void SetIsFullscreen (Window window, bool isFullscreen) { if (MacSystemInformation.OsVersion < MacSystemInformation.Lion) { base.SetIsFullscreen (window, isFullscreen); return; } NSWindow nswin = GtkQuartz.GetWindow (window); if (isFullscreen != ((nswin.StyleMask & NSWindowStyle.FullScreenWindow) != 0)) nswin.ToggleFullScreen (null); } public override bool IsModalDialogRunning () { if (NSApplication.SharedApplication.ModalWindow != null) return true; var toplevels = Gtk.Window.ListToplevels (); var ret = toplevels.Any (w => w.Modal && w.Visible && GtkQuartz.GetWindow (w)?.DebugDescription.StartsWith ("<_NSFullScreenTileDividerWindow", StringComparison.Ordinal) != true); return ret; } internal override void AddChildWindow (Gtk.Window parent, Gtk.Window child) { NSWindow w = GtkQuartz.GetWindow (parent); child.Realize (); NSWindow overlay = GtkQuartz.GetWindow (child); overlay.SetExcludedFromWindowsMenu (true); w.AddChildWindow (overlay, NSWindowOrderingMode.Above); } internal override void PlaceWindow (Gtk.Window window, int x, int y, int width, int height) { if (window.GdkWindow == null) return; // Not yet realized NSWindow w = GtkQuartz.GetWindow (window); y += GetTitleBarHeight (w); var dr = FromDesktopRect (new Gdk.Rectangle (x, y, width, height)); var r = w.FrameRectFor (dr); w.SetFrame (r, true); base.PlaceWindow (window, x, y, width, height); } static CGRect FromDesktopRect (Gdk.Rectangle r) { var desktopBounds = CalcDesktopBounds (); r.Y = desktopBounds.Height - r.Y - r.Height; if (desktopBounds.Y < 0) r.Y += desktopBounds.Y; return new CGRect (desktopBounds.X + r.X, r.Y, r.Width, r.Height); } static Gdk.Rectangle CalcDesktopBounds () { var desktopBounds = new Gdk.Rectangle (); foreach (var s in NSScreen.Screens) { var r = s.Frame; desktopBounds = desktopBounds.Union (new Gdk.Rectangle ((int)r.X, (int)r.Y, (int)r.Width, (int)r.Height)); } return desktopBounds; } public override void OpenFolder (FilePath folderPath, FilePath[] selectFiles) { if (selectFiles.Length == 0) { System.Diagnostics.Process.Start (folderPath); } else { NSWorkspace.SharedWorkspace.ActivateFileViewer (selectFiles.Select ((f) => NSUrl.FromFilename (f)).ToArray ()); } } internal override void RestartIde (bool reopenWorkspace) { FilePath bundlePath = NSBundle.MainBundle.BundlePath; if (bundlePath.Extension != ".app") { base.RestartIde (reopenWorkspace); return; } var reopen = reopenWorkspace && IdeApp.Workspace != null && IdeApp.Workspace.Items.Count > 0; var proc = new Process (); var path = bundlePath.Combine ("Contents", "MacOS"); //assume renames of mdtool end with "tool" var mdtool = Directory.EnumerateFiles (path, "*tool").Single(); var psi = new ProcessStartInfo (mdtool) { CreateNoWindow = true, UseShellExecute = false, WorkingDirectory = path, Arguments = "--start-app-bundle", }; var recentWorkspace = reopen ? IdeServices.DesktopService.RecentFiles.GetProjects ().FirstOrDefault ()?.FileName : string.Empty; if (!string.IsNullOrEmpty (recentWorkspace)) psi.Arguments += " " + recentWorkspace; proc.StartInfo = psi; proc.Start (); } internal override IPlatformTelemetryDetails CreatePlatformTelemetryDetails () { return MacTelemetryDetails.CreateTelemetryDetails (); } internal override MemoryMonitor CreateMemoryMonitor () => new MacMemoryMonitor (); internal class MacMemoryMonitor : MemoryMonitor, IDisposable { const MemoryPressureFlags notificationFlags = MemoryPressureFlags.Critical | MemoryPressureFlags.Warn | MemoryPressureFlags.Normal; internal DispatchSource.MemoryPressure DispatchSource { get; private set; } public MacMemoryMonitor () { DispatchSource = new DispatchSource.MemoryPressure (notificationFlags, DispatchQueue.MainQueue); DispatchSource.SetEventHandler (() => { var metadata = CreateMemoryMetadata (DispatchSource.PressureFlags); var args = new PlatformMemoryStatusEventArgs (metadata); OnStatusChanged (args); }); DispatchSource.Resume (); } static MacPlatformMemoryMetadata CreateMemoryMetadata (MemoryPressureFlags flags) { var platformMemoryStatus = GetPlatformMemoryStatus (flags); Interop.SysCtl ("vm.compressor_bytes_used", out long osCompressedMemory); Interop.SysCtl ("vm.vm_page_free_target", out long osFreePagesTarget); Interop.SysCtl ("vm.page_free_count", out long osFreePages); Interop.SysCtl ("vm.pagesize", out long pagesize); KernelInterop.GetCompressedMemoryInfo (out ulong appCompressedMemory, out ulong appVirtualMemory); return new MacPlatformMemoryMetadata { MemoryStatus = platformMemoryStatus, OSVirtualMemoryFreeTarget = osFreePagesTarget * pagesize, OSTotalFreeVirtualMemory = osFreePages * pagesize, OSTotalCompressedMemory = osCompressedMemory, ApplicationVirtualMemory = appVirtualMemory, ApplicationCompressedMemory = appCompressedMemory, }; } static PlatformMemoryStatus GetPlatformMemoryStatus (MemoryPressureFlags flags) { switch (flags) { case MemoryPressureFlags.Critical: return PlatformMemoryStatus.Critical; case MemoryPressureFlags.Warn: return PlatformMemoryStatus.Low; case MemoryPressureFlags.Normal: return PlatformMemoryStatus.Normal; default: LoggingService.LogError ("Unknown MemoryPressureFlags value {0}", flags.ToString ()); return PlatformMemoryStatus.Normal; } } public void Dispose () { if (DispatchSource != null) { DispatchSource.Cancel (); DispatchSource.Dispose (); DispatchSource = null; } } class MacPlatformMemoryMetadata : PlatformMemoryMetadata { // sysctl - vm.vm_page_free_target public long OSVirtualMemoryFreeTarget { get => GetProperty (); set => SetProperty (value); } // sysctl - vm.compressor_bytes_used public long OSTotalCompressedMemory { get => GetProperty (); set => SetProperty (value); } // sysctl - vm.page_free_count public long OSTotalFreeVirtualMemory { get => GetProperty (); set => SetProperty (value); } // task_vm_info_t.compressed public ulong ApplicationCompressedMemory { get => GetProperty (); set => SetProperty (value); } // task_vm_info_t.virtual_size public ulong ApplicationVirtualMemory { get => GetProperty (); set => SetProperty (value); } } } internal override ThermalMonitor CreateThermalMonitor () { if (MacSystemInformation.OsVersion < new Version (10, 10, 3)) return base.CreateThermalMonitor (); return new MacThermalMonitor (); } internal class MacThermalMonitor : ThermalMonitor, IDisposable { NSObject observer; public MacThermalMonitor () { observer = NSProcessInfo.Notifications.ObserveThermalStateDidChange ((o, args) => { var metadata = new PlatformThermalMetadata { ThermalStatus = ToPlatform (NSProcessInfo.ProcessInfo.ThermalState), }; var thermalArgs = new PlatformThermalStatusEventArgs (metadata); OnStatusChanged (thermalArgs); }); } static PlatformThermalStatus ToPlatform (NSProcessInfoThermalState status) { switch (status) { case NSProcessInfoThermalState.Nominal: return PlatformThermalStatus.Normal; case NSProcessInfoThermalState.Fair: return PlatformThermalStatus.Fair; case NSProcessInfoThermalState.Critical: return PlatformThermalStatus.Critical; case NSProcessInfoThermalState.Serious: return PlatformThermalStatus.Serious; default: LoggingService.LogError ("Unknown NSProcessInfoThermalState value {0}", status.ToString ()); return PlatformThermalStatus.Normal; } } public void Dispose () { if (observer != null) { NSNotificationCenter.DefaultCenter.RemoveObserver (observer); observer = null; } } } } public class ThemedMacWindowBackend : Xwt.Mac.WindowBackend { public override void InitializeBackend (object frontend, Xwt.Backends.ApplicationContext context) { base.InitializeBackend (frontend, context); IdeTheme.ApplyTheme (this); } } public class ThemedMacDialogBackend : Xwt.Mac.DialogBackend { public ThemedMacDialogBackend () { } public ThemedMacDialogBackend (IntPtr ptr) : base (ptr) { } public override void InitializeBackend (object frontend, Xwt.Backends.ApplicationContext context) { base.InitializeBackend (frontend, context); IdeTheme.ApplyTheme (this); } } public class ThemedMacAlertDialogBackend : Xwt.Mac.AlertDialogBackend { public ThemedMacAlertDialogBackend () { } public ThemedMacAlertDialogBackend (IntPtr ptr) : base (ptr) { } public override void Initialize (Xwt.Backends.ApplicationContext actx) { base.Initialize (actx); IdeTheme.ApplyTheme (this.Window); } } }