/main/src/addins/MacPlatform/MacPlatform.cs

http://github.com/mono/monodevelop · C# · 1624 lines · 1231 code · 246 blank · 147 comment · 224 complexity · 19db2685b6dbfadce835b2e516e0e031 MD5 · raw file

Large files are truncated click here to view the full file

  1. //
  2. // MacPlatformService.cs
  3. //
  4. // Author:
  5. // Geoff Norton <gnorton@novell.com>
  6. // Michael Hutchinson <m.j.hutchinson@gmail.com>
  7. //
  8. // Copyright (C) 2007-2011 Novell, Inc (http://www.novell.com)
  9. //
  10. // Permission is hereby granted, free of charge, to any person obtaining
  11. // a copy of this software and associated documentation files (the
  12. // "Software"), to deal in the Software without restriction, including
  13. // without limitation the rights to use, copy, modify, merge, publish,
  14. // distribute, sublicense, and/or sell copies of the Software, and to
  15. // permit persons to whom the Software is furnished to do so, subject to
  16. // the following conditions:
  17. //
  18. // The above copyright notice and this permission notice shall be
  19. // included in all copies or substantial portions of the Software.
  20. //
  21. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  22. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  23. // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  24. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  25. // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  26. // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  27. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  28. //
  29. using System;
  30. using System.Collections.Generic;
  31. using System.IO;
  32. using System.Linq;
  33. using System.Text.RegularExpressions;
  34. using AppKit;
  35. using CoreFoundation;
  36. using Foundation;
  37. using CoreGraphics;
  38. using MonoDevelop.Core;
  39. using MonoDevelop.Core.Execution;
  40. using MonoDevelop.Core.Instrumentation;
  41. using MonoDevelop.Components.Commands;
  42. using MonoDevelop.Ide;
  43. using MonoDevelop.Ide.Gui;
  44. using MonoDevelop.Ide.Commands;
  45. using MonoDevelop.Ide.Desktop;
  46. using MonoDevelop.MacInterop;
  47. using MonoDevelop.Components;
  48. using MonoDevelop.Components.MainToolbar;
  49. using MonoDevelop.Components.Extensions;
  50. using System.Runtime.InteropServices;
  51. using ObjCRuntime;
  52. using System.Diagnostics;
  53. using Xwt.Mac;
  54. using MonoDevelop.Components.Mac;
  55. using System.Reflection;
  56. using MacPlatform;
  57. using MonoDevelop.Projects;
  58. using System.Threading.Tasks;
  59. namespace MonoDevelop.MacIntegration
  60. {
  61. public class MacPlatformService : PlatformService
  62. {
  63. const string monoDownloadUrl = "http://www.mono-project.com/download/";
  64. static readonly TimerCounter timer = InstrumentationService.CreateTimerCounter ("Mac Platform Initialization", "Platform Service");
  65. static readonly TimerCounter mimeTimer = InstrumentationService.CreateTimerCounter ("Mac Mime Database", "Platform Service");
  66. readonly ITimeTracker initTracker;
  67. static bool initedGlobal;
  68. bool setupFail, initedApp;
  69. // hold a reference on all observer objects generated by the notification center
  70. // NOTE: these objects should not be actively disposed on macOS 10.11 and later, unless removed manually
  71. // not keeping a reference might cause a runtime crash when observers are added:
  72. // KVO_IS_RETAINING_ALL_OBSERVERS_OF_THIS_OBJECT_IF_IT_CRASHES_AN_OBSERVER_WAS_OVERRELEASED_OR_SMASHED
  73. List<IDisposable> notificationObservers = new List<IDisposable> ();
  74. readonly Lazy<Dictionary<string, string>> mimemap = new Lazy<Dictionary<string, string>> (LoadMimeMap);
  75. static string applicationMenuName;
  76. public static string ApplicationMenuName {
  77. get {
  78. return applicationMenuName ?? BrandingService.ApplicationName;
  79. }
  80. set {
  81. if (applicationMenuName != value) {
  82. applicationMenuName = value;
  83. }
  84. }
  85. }
  86. [DllImport(Constants.ObjectiveCLibrary)]
  87. private static extern IntPtr class_getInstanceMethod(IntPtr classHandle, IntPtr Selector);
  88. [DllImport(Constants.ObjectiveCLibrary)]
  89. private static extern IntPtr method_getImplementation(IntPtr method);
  90. [DllImport(Constants.ObjectiveCLibrary)]
  91. private static extern IntPtr imp_implementationWithBlock(ref BlockLiteral block);
  92. [DllImport(Constants.ObjectiveCLibrary)]
  93. private static extern void method_setImplementation(IntPtr method, IntPtr imp);
  94. [MonoNativeFunctionWrapper]
  95. delegate void AccessibilitySetValueForAttributeDelegate (IntPtr self, IntPtr selector, IntPtr valueHandle, IntPtr attributeHandle);
  96. delegate void SwizzledAccessibilitySetValueForAttributeDelegate (IntPtr block, IntPtr self, IntPtr valueHandle, IntPtr attributeHandle);
  97. static IntPtr originalAccessibilitySetValueForAttributeMethod;
  98. void SwizzleNSApplication ()
  99. {
  100. // Swizzle accessibilitySetValue:forAttribute: so that we can detect when VoiceOver gets enabled
  101. var nsApplicationClassHandle = Class.GetHandle ("NSApplication");
  102. var accessibilitySetValueForAttributeSelector = Selector.GetHandle ("accessibilitySetValue:forAttribute:");
  103. var accessibilitySetValueForAttributeMethod = class_getInstanceMethod (nsApplicationClassHandle, accessibilitySetValueForAttributeSelector);
  104. originalAccessibilitySetValueForAttributeMethod = method_getImplementation (accessibilitySetValueForAttributeMethod);
  105. var block = new BlockLiteral ();
  106. SwizzledAccessibilitySetValueForAttributeDelegate d = accessibilitySetValueForAttribute;
  107. block.SetupBlock (d, null);
  108. var imp = imp_implementationWithBlock (ref block);
  109. method_setImplementation (accessibilitySetValueForAttributeMethod, imp);
  110. }
  111. [MonoPInvokeCallback (typeof (SwizzledAccessibilitySetValueForAttributeDelegate))]
  112. static void accessibilitySetValueForAttribute (IntPtr block, IntPtr self, IntPtr valueHandle, IntPtr attributeHandle)
  113. {
  114. var d = Marshal.GetDelegateForFunctionPointer<AccessibilitySetValueForAttributeDelegate> (originalAccessibilitySetValueForAttributeMethod);
  115. d (self, Selector.GetHandle ("accessibilitySetValue:forAttribute:"), valueHandle, attributeHandle);
  116. NSString attrString = (NSString)ObjCRuntime.Runtime.GetNSObject (attributeHandle);
  117. var val = (NSNumber)ObjCRuntime.Runtime.GetNSObject (valueHandle);
  118. if (attrString == "AXEnhancedUserInterface" && !IdeTheme.AccessibilityEnabled) {
  119. if (val.BoolValue) {
  120. ShowVoiceOverNotice ();
  121. }
  122. }
  123. AccessibilityInUse = val.BoolValue;
  124. }
  125. MacIdeAppleEvents ideAppleEvents;
  126. public MacPlatformService ()
  127. {
  128. if (initedGlobal)
  129. throw new Exception ("Only one MacPlatformService instance allowed");
  130. initedGlobal = true;
  131. initTracker = timer.BeginTiming ();
  132. var dir = Path.GetDirectoryName (typeof(MacPlatformService).Assembly.Location);
  133. if (ObjCRuntime.Dlfcn.dlopen (Path.Combine (dir, "libxammac.dylib"), 0) == IntPtr.Zero)
  134. LoggingService.LogFatalError ("Unable to load libxammac");
  135. CheckGtkVersion (2, 24, 14);
  136. Xwt.Toolkit.CurrentEngine.RegisterBackend<IExtendedTitleBarWindowBackend,ExtendedTitleBarWindowBackend> ();
  137. Xwt.Toolkit.CurrentEngine.RegisterBackend<IExtendedTitleBarDialogBackend,ExtendedTitleBarDialogBackend> ();
  138. Xwt.Toolkit.CurrentEngine.RegisterBackend<Xwt.Backends.ISearchTextEntryBackend,AccessibleGtkSearchEntryBackend> ();
  139. var description = XamMacBuildInfo.Value;
  140. if (string.IsNullOrEmpty (description)) {
  141. LoggingService.LogWarning ("Failed to parse version of Xamarin.Mac used at runtime");
  142. } else {
  143. LoggingService.LogInfo ("Using {0}", description);
  144. }
  145. }
  146. static string GetInfoPart (string line)
  147. {
  148. return line.Split (':') [1].Trim ();
  149. }
  150. static Lazy<string> XamMacBuildInfo = new Lazy<string> (() => {
  151. const string buildInfoResource = "Xamarin.Mac.buildinfo";
  152. var asm = System.Reflection.Assembly.GetExecutingAssembly ();
  153. string version, hash, branch;
  154. try {
  155. using (var stream = asm.GetManifestResourceStream (buildInfoResource))
  156. using (var sr = new StreamReader (stream)) {
  157. // Version: 4.4.0.36
  158. // Hash: 0c7c49a6
  159. // Branch: master
  160. // Build date: 2018 - 03 - 12 15:24:46 - 0400 -- discarded
  161. version = GetInfoPart (sr.ReadLine ());
  162. hash = GetInfoPart (sr.ReadLine ());
  163. branch = GetInfoPart (sr.ReadLine ());
  164. return $"Xamarin.Mac {version} ({branch} / {hash})";
  165. }
  166. } catch {
  167. return string.Empty;
  168. }
  169. });
  170. internal override string GetNativeRuntimeDescription ()
  171. {
  172. return XamMacBuildInfo.Value;
  173. }
  174. static void CheckGtkVersion (uint major, uint minor, uint micro)
  175. {
  176. // to require exact version, also check
  177. //: || Gtk.Global.CheckVersion (major, minor, micro + 1) == null
  178. //
  179. if (Gtk.Global.CheckVersion (major, minor, micro) != null) {
  180. LoggingService.LogFatalError (
  181. "GTK+ version is incompatible with required version {0}.{1}.{2}.",
  182. major, minor, micro
  183. );
  184. var downloadButton = new AlertButton ("Download Mono Framework", null);
  185. if (downloadButton == MessageService.GenericAlert (
  186. Stock.Error,
  187. GettextCatalog.GetString ("Some dependencies need to be updated"),
  188. GettextCatalog.GetString (
  189. "{0} requires a newer version of GTK+, which is included with the Mono Framework. Please " +
  190. "download and install the latest stable Mono Framework package and restart {0}.",
  191. BrandingService.ApplicationName
  192. ),
  193. new AlertButton ("Quit", null), downloadButton))
  194. {
  195. OpenUrl (monoDownloadUrl);
  196. }
  197. Environment.Exit (1);
  198. }
  199. }
  200. const string FoundationLib = "/System/Library/Frameworks/Foundation.framework/Versions/Current/Foundation";
  201. delegate void NSUncaughtExceptionHandler (IntPtr exception);
  202. static readonly NSUncaughtExceptionHandler uncaughtHandler = HandleUncaughtException;
  203. static NSUncaughtExceptionHandler oldHandler;
  204. static void HandleUncaughtException(IntPtr exceptionPtr)
  205. {
  206. // It's non-null guaranteed by objc.
  207. Debug.Assert (exceptionPtr != IntPtr.Zero);
  208. var nsException = ObjCRuntime.Runtime.GetNSObject<NSException> (exceptionPtr);
  209. try {
  210. throw new MarshalledObjCException (nsException);
  211. } catch (MarshalledObjCException e) {
  212. LoggingService.LogFatalError ("Unhandled ObjC Exception", e);
  213. // Is there a way to figure out if it's going to crash us? Maybe check MarshalObjectiveCExceptionMode and MarshalManagedExceptionMode?
  214. }
  215. // Invoke the default xamarin.mac one, so if it bubbles up an exception, the caller receives it.
  216. oldHandler?.Invoke (exceptionPtr);
  217. }
  218. sealed class MarshalledObjCException : ObjCException
  219. {
  220. public MarshalledObjCException (NSException exception) : base (exception)
  221. {
  222. const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetField;
  223. var trace = new [] { new StackTrace (true), };
  224. //// Otherwise exception stacktrace is not gathered.
  225. typeof (Exception)
  226. .InvokeMember ("captured_traces", flags, null, this, new object [] { trace });
  227. }
  228. }
  229. [DllImport (FoundationLib)]
  230. static extern void NSSetUncaughtExceptionHandler (NSUncaughtExceptionHandler handler);
  231. [DllImport (FoundationLib)]
  232. static extern NSUncaughtExceptionHandler NSGetUncaughtExceptionHandler ();
  233. static void RegisterUncaughtExceptionHandler ()
  234. {
  235. oldHandler = NSGetUncaughtExceptionHandler ();
  236. NSSetUncaughtExceptionHandler (uncaughtHandler);
  237. ObjCRuntime.Runtime.MarshalManagedException += (sender, args) => {
  238. LoggingService.LogInternalError (args.Exception);
  239. };
  240. ObjCRuntime.Runtime.MarshalObjectiveCException += (sender, args) => {
  241. try {
  242. throw new MarshalledObjCException (args.Exception);
  243. } catch (MarshalledObjCException e) {
  244. LoggingService.LogInternalError ("MarshalObjCException", e);
  245. }
  246. };
  247. }
  248. public override Xwt.Toolkit LoadNativeToolkit ()
  249. {
  250. var loaded = NativeToolkitHelper.LoadCocoa ();
  251. loaded.RegisterBackend<Xwt.Backends.IDialogBackend, ThemedMacDialogBackend> ();
  252. loaded.RegisterBackend<Xwt.Backends.IWindowBackend, ThemedMacWindowBackend> ();
  253. loaded.RegisterBackend<Xwt.Backends.IAlertDialogBackend, ThemedMacAlertDialogBackend> ();
  254. RegisterUncaughtExceptionHandler ();
  255. // We require Xwt.Mac to initialize MonoMac before we can execute any code using MonoMac
  256. initTracker.Trace ("Installing App Event Handlers");
  257. GlobalSetup ();
  258. initTracker.End ();
  259. var appDelegate = NSApplication.SharedApplication.Delegate as Xwt.Mac.AppDelegate;
  260. if (appDelegate != null) {
  261. appDelegate.Terminating += async (object o, TerminationEventArgs e) => {
  262. if (MonoDevelop.Ide.IdeApp.IsRunning) {
  263. // If GLib the mainloop is still running that means NSApplication.Terminate() was called
  264. // before Gtk.Application.Quit(). Cancel Cocoa termination and exit the mainloop.
  265. e.Reply = NSApplicationTerminateReply.Cancel;
  266. await IdeApp.Exit();
  267. } else {
  268. // The mainloop has already exited and we've already cleaned up our application state
  269. // so it's now safe to terminate Cocoa.
  270. e.Reply = NSApplicationTerminateReply.Now;
  271. }
  272. };
  273. appDelegate.ShowDockMenu += AppDelegate_ShowDockMenu;
  274. }
  275. // Listen to the AtkCocoa notification for the presence of VoiceOver
  276. SwizzleNSApplication ();
  277. var nc = NSNotificationCenter.DefaultCenter;
  278. notificationObservers.Add (nc.AddObserver ((NSString)"AtkCocoaAccessibilityEnabled", (NSNotification) => {
  279. LoggingService.LogInfo ($"VoiceOver on {IdeTheme.AccessibilityEnabled}");
  280. if (!IdeTheme.AccessibilityEnabled) {
  281. ShowVoiceOverNotice ();
  282. }
  283. }, NSApplication.SharedApplication));
  284. // Now that Cocoa has been initialized we can check whether the keyboard focus mode is turned on
  285. // See System Preferences - Keyboard - Shortcuts - Full Keyboard Access
  286. var keyboardMode = NSUserDefaults.StandardUserDefaults.IntForKey ("AppleKeyboardUIMode");
  287. // 0 - Text boxes and lists only
  288. // 2 - All controls
  289. // 3 - All controls + keyboard access
  290. if (keyboardMode != 0) {
  291. Gtk.Rc.ParseString ("style \"default\" { engine \"xamarin\" { focusstyle = 2 } }");
  292. Gtk.Rc.ParseString ("style \"radio-or-check-box\" { engine \"xamarin\" { focusstyle = 2 } } ");
  293. }
  294. AccessibilityKeyboardFocusInUse = (keyboardMode != 0);
  295. // Disallow window tabbing globally
  296. if (MacSystemInformation.OsVersion >= MacSystemInformation.Sierra)
  297. NSWindow.AllowsAutomaticWindowTabbing = false;
  298. // At this point, Cocoa should have been initialized; it is initialized along with Gtk+ at the beginning of IdeStartup.Run
  299. // If LaunchReason is still Unknown at this point, it means we have missed the NSApplicationDidLaunch notification for some reason and
  300. // we fall back to the AppDelegate's version to unblock anything waiting for that notification.
  301. if (IdeApp.LaunchReason == IdeApp.LaunchType.Unknown) {
  302. if (appDelegate.LaunchReason != AppDelegate.LaunchType.Unknown) {
  303. IdeApp.LaunchReason = appDelegate.LaunchReason == AppDelegate.LaunchType.Normal ? IdeApp.LaunchType.Normal : IdeApp.LaunchType.LaunchedFromFileManager;
  304. } else {
  305. // Fall back to Normal if even the app delegate doesn't know
  306. LoggingService.LogWarning ("Unknown startup reason, assuming Normal");
  307. IdeApp.LaunchReason = IdeApp.LaunchType.Normal;
  308. }
  309. }
  310. ideAppleEvents = new MacIdeAppleEvents ();
  311. return loaded;
  312. }
  313. void AppDelegate_ShowDockMenu (object sender, ShowDockMenuArgs e)
  314. {
  315. if (((FilePath)NSBundle.MainBundle.BundlePath).Extension != ".app")
  316. return;
  317. var menu = new NSMenu ();
  318. var newInstanceMenuItem = new NSMenuItem ();
  319. newInstanceMenuItem.Title = GettextCatalog.GetString ("New Instance");
  320. newInstanceMenuItem.Activated += NewInstanceMenuItem_Activated;
  321. menu.AddItem (newInstanceMenuItem);
  322. e.DockMenu = menu;
  323. }
  324. static void NewInstanceMenuItem_Activated (object sender, EventArgs e)
  325. {
  326. var bundlePath = NSBundle.MainBundle.BundlePath;
  327. NSWorkspace.SharedWorkspace.LaunchApplication (NSUrl.FromFilename (bundlePath), NSWorkspaceLaunchOptions.NewInstance, new NSDictionary (), out NSError error);
  328. if (error != null)
  329. LoggingService.LogError ($"Failed to start new instance: {error.LocalizedDescription}");
  330. }
  331. // The Enabled key needs to be controlled through NSUserDefaults so that it can be
  332. // set from the command line. This lets users who require accessibility features to
  333. // enabled to control it through the commandline, if it has been disabled from inside the software
  334. const string EnabledKey = "com.monodevelop.AccessibilityEnabled";
  335. const string VoiceOverNoticeShownKey = "MonoDevelop.VoiceOver.Show";
  336. static void ShowVoiceOverNotice ()
  337. {
  338. if (PropertyService.Get<bool> (VoiceOverNoticeShownKey, false)) {
  339. return;
  340. }
  341. // Show the VoiceOver notice once
  342. PropertyService.Set (VoiceOverNoticeShownKey, true);
  343. var alert = new NSAlert ();
  344. alert.MessageText = GettextCatalog.GetString ("Assistive Technology Detected");
  345. 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);
  346. alert.AddButton (GettextCatalog.GetString ("Restart and enable"));
  347. alert.AddButton (GettextCatalog.GetString ("No"));
  348. var result = alert.RunModal ();
  349. switch (result) {
  350. case 1000:
  351. NSUserDefaults defaults = NSUserDefaults.StandardUserDefaults;
  352. defaults.SetBool (true, EnabledKey);
  353. defaults.Synchronize ();
  354. IdeApp.Restart ();
  355. break;
  356. default:
  357. break;
  358. }
  359. }
  360. internal override void MakeAccessibilityAnnouncement (string text)
  361. {
  362. using var message = new NSString (text);
  363. using var dictionary = new NSDictionary (NSAccessibilityNotificationUserInfoKeys.AnnouncementKey, message,
  364. NSAccessibilityNotificationUserInfoKeys.PriorityKey, NSAccessibilityPriorityLevel.High);
  365. NSAccessibility.PostNotification (NSApplication.SharedApplication.AccessibilityMainWindow, NSAccessibilityNotifications.AnnouncementRequestedNotification, dictionary);
  366. }
  367. protected override string OnGetMimeTypeForUri (string uri)
  368. {
  369. var ext = Path.GetExtension (uri);
  370. string mime;
  371. if (ext != null && mimemap.Value.TryGetValue (ext, out mime))
  372. return mime;
  373. return null;
  374. }
  375. public override void ShowUrl (string url)
  376. {
  377. OpenUrl (url);
  378. }
  379. internal static void OpenUrl (string url)
  380. {
  381. Gtk.Application.Invoke ((o, args) => {
  382. NSWorkspace.SharedWorkspace.OpenUrl (new NSUrl (url));
  383. });
  384. }
  385. public override void OpenFile (string filename)
  386. {
  387. Gtk.Application.Invoke ((o, args) => {
  388. NSWorkspace.SharedWorkspace.OpenFile (filename);
  389. });
  390. }
  391. public override string DefaultMonospaceFont {
  392. get { return "Menlo 12"; }
  393. }
  394. public override string Name {
  395. get { return "OSX"; }
  396. }
  397. static Dictionary<string, string> LoadMimeMap ()
  398. {
  399. var map = new Dictionary<string, string> ();
  400. // All recent Macs should have this file; if not we'll just die silently
  401. if (!File.Exists ("/etc/apache2/mime.types")) {
  402. LoggingService.LogError ("Apache mime database is missing");
  403. return map;
  404. }
  405. using var time = mimeTimer.BeginTiming ();
  406. try {
  407. var loader = new MimeMapLoader (map);
  408. loader.LoadMimeMap ("/etc/apache2/mime.types");
  409. } catch (Exception ex){
  410. LoggingService.LogError ("Could not load Apache mime database", ex);
  411. }
  412. return map;
  413. }
  414. string currentCommandMenuPath, currentAppMenuPath;
  415. public override bool SetGlobalMenu (CommandManager commandManager, string commandMenuAddinPath, string appMenuAddinPath)
  416. {
  417. if (setupFail)
  418. return false;
  419. // avoid reinitialization of the same menu structure
  420. if (initedApp == true && currentCommandMenuPath == commandMenuAddinPath && currentAppMenuPath == appMenuAddinPath)
  421. return true;
  422. try {
  423. InitApp (commandManager);
  424. NSApplication.SharedApplication.HelpMenu = null;
  425. var rootMenu = NSApplication.SharedApplication.MainMenu;
  426. if (rootMenu == null) {
  427. rootMenu = new NSMenu ();
  428. } else {
  429. rootMenu.RemoveAllItems ();
  430. }
  431. CommandEntrySet appCes = commandManager.CreateCommandEntrySet (appMenuAddinPath);
  432. rootMenu.AddItem (new MDSubMenuItem (commandManager, appCes));
  433. CommandEntrySet ces = commandManager.CreateCommandEntrySet (commandMenuAddinPath);
  434. foreach (CommandEntry ce in ces) {
  435. var item = new MDSubMenuItem (commandManager, (CommandEntrySet)ce);
  436. rootMenu.AddItem (item);
  437. if (ce.CommandId as string == "Help" && item.HasSubmenu && NSApplication.SharedApplication.HelpMenu == null)
  438. NSApplication.SharedApplication.HelpMenu = item.Submenu;
  439. }
  440. // Assign the main menu after loading the items. Otherwise a weird application menu appears.
  441. if (NSApplication.SharedApplication.MainMenu == null)
  442. NSApplication.SharedApplication.MainMenu = rootMenu;
  443. currentCommandMenuPath = commandMenuAddinPath;
  444. currentAppMenuPath = appMenuAddinPath;
  445. } catch (Exception ex) {
  446. try {
  447. var m = NSApplication.SharedApplication.MainMenu;
  448. if (m != null) {
  449. m.Dispose ();
  450. }
  451. NSApplication.SharedApplication.MainMenu = null;
  452. m = NSApplication.SharedApplication.HelpMenu;
  453. if (m != null) {
  454. m.Dispose ();
  455. }
  456. NSApplication.SharedApplication.HelpMenu = null;
  457. } catch (Exception e2) {
  458. LoggingService.LogError ("Could not uninstall global menu", e2);
  459. }
  460. LoggingService.LogError ("Could not install global menu", ex);
  461. setupFail = true;
  462. return false;
  463. }
  464. return true;
  465. }
  466. static void OnCommandActivating (object sender, CommandActivationEventArgs args)
  467. {
  468. if (args.Source != CommandSource.Keybinding)
  469. return;
  470. var m = NSApplication.SharedApplication.MainMenu;
  471. if (m != null) {
  472. foreach (NSMenuItem item in m.Items) {
  473. var submenu = item.Submenu as MDMenu;
  474. if (submenu != null && submenu.FlashIfContainsCommand (args.CommandId))
  475. return;
  476. }
  477. }
  478. }
  479. void InitApp (CommandManager commandManager)
  480. {
  481. if (initedApp)
  482. return;
  483. commandManager.CommandActivating += OnCommandActivating;
  484. //mac-ify these command names
  485. commandManager.GetCommand (EditCommands.MonodevelopPreferences).Text = GettextCatalog.GetString ("Preferences...");
  486. commandManager.GetCommand (EditCommands.DefaultPolicies).Text = GettextCatalog.GetString ("Policies...");
  487. commandManager.GetCommand (HelpCommands.About).Text = GetAboutCommandText ();
  488. commandManager.GetCommand (MacIntegrationCommands.HideWindow).Text = GetHideWindowCommandText ();
  489. commandManager.GetCommand (ToolCommands.AddinManager).Text = GettextCatalog.GetString ("Extensions...");
  490. initedApp = true;
  491. IdeApp.Initialized += (s, e) => {
  492. IdeApp.Workbench.RootWindow.DeleteEvent += HandleDeleteEvent;
  493. if (MacSystemInformation.OsVersion >= MacSystemInformation.Lion) {
  494. IdeApp.Workbench.RootWindow.Realized += (sender, args) => {
  495. var win = GtkQuartz.GetWindow ((Gtk.Window)sender);
  496. win.CollectionBehavior |= NSWindowCollectionBehavior.FullScreenPrimary;
  497. if (MacSystemInformation.OsVersion >= MacSystemInformation.Sierra)
  498. win.TabbingMode = NSWindowTabbingMode.Disallowed;
  499. };
  500. }
  501. };
  502. PatchGtkTheme ();
  503. if (MacSystemInformation.OsVersion >= MacSystemInformation.Mojave) {
  504. IdeTheme.HighContrastThemeEnabled = GetIsHighContrastActive ();
  505. notificationObservers.Add (NSApplication.SharedApplication.AddObserver ("effectiveAppearance", NSKeyValueObservingOptions.New, notif =>
  506. Core.Runtime.RunInMainThread (() => {
  507. IdeTheme.HighContrastThemeEnabled = GetIsHighContrastActive ();
  508. PatchGtkTheme ();
  509. })
  510. ));
  511. } else {
  512. IdeTheme.HighContrastThemeEnabled = false;
  513. notificationObservers.Add (NSNotificationCenter.DefaultCenter.AddObserver (NSCell.ControlTintChangedNotification, notif => Core.Runtime.RunInMainThread (
  514. delegate {
  515. Styles.LoadStyle ();
  516. PatchGtkTheme ();
  517. })));
  518. }
  519. if (MacSystemInformation.OsVersion < MacSystemInformation.Mojave) { // the shared color panel has full automatic theme support on Mojave
  520. Styles.Changed += (s, a) => {
  521. var colorPanel = NSColorPanel.SharedColorPanel;
  522. if (colorPanel.ContentView?.Superview?.Window == null)
  523. LoggingService.LogWarning ("Updating shared color panel appearance failed, no valid window.");
  524. IdeTheme.ApplyTheme (colorPanel.ContentView.Superview.Window);
  525. var appearance = colorPanel.ContentView.Superview.Window.Appearance;
  526. if (appearance == null)
  527. appearance = IdeTheme.GetAppearance ();
  528. // The subviews of the shared NSColorPanel do not inherit the appearance of the main panel window
  529. // and need to be updated recursively.
  530. UpdateColorPanelSubviewsAppearance (colorPanel.ContentView.Superview, appearance);
  531. };
  532. }
  533. // FIXME: Immediate theme switching disabled, until NSAppearance issues are fixed
  534. //IdeApp.Preferences.UserInterfaceTheme.Changed += (s,a) => PatchGtkTheme ();
  535. }
  536. static bool GetIsHighContrastActive ()
  537. {
  538. var highContrastAppearances = new string [] {
  539. NSAppearance.NameAccessibilityHighContrastAqua,
  540. NSAppearance.NameAccessibilityHighContrastDarkAqua,
  541. NSAppearance.NameAccessibilityHighContrastVibrantDark,
  542. NSAppearance.NameAccessibilityHighContrastVibrantLight,
  543. };
  544. // FindBestMatch will return the best matching a11y appearance or null if no high contrast appearance is in use
  545. return NSApplication.SharedApplication.EffectiveAppearance.FindBestMatch (highContrastAppearances) != null;
  546. }
  547. static void UpdateColorPanelSubviewsAppearance (NSView view, NSAppearance appearance)
  548. {
  549. if (view.Class.Name == "NSPageableTableView")
  550. ((NSTableView)view).BackgroundColor = Xwt.Mac.Util.ToNSColor (Styles.BackgroundColor);
  551. view.Appearance = appearance;
  552. foreach (var subview in view.Subviews)
  553. UpdateColorPanelSubviewsAppearance (subview, appearance);
  554. }
  555. static string GetAboutCommandText ()
  556. {
  557. return GettextCatalog.GetString ("About {0}", ApplicationMenuName);
  558. }
  559. static string GetHideWindowCommandText ()
  560. {
  561. return GettextCatalog.GetString ("Hide {0}", ApplicationMenuName);
  562. }
  563. // VV/VK: Disable tint based color generation
  564. // This will dynamically generate a gtkrc for certain widgets using system control colors.
  565. void PatchGtkTheme ()
  566. {
  567. // string color_hex, text_hex;
  568. //
  569. // if (MonoDevelop.Core.Platform.OSVersion >= MonoDevelop.Core.MacSystemInformation.Yosemite) {
  570. // NSControlTint tint = NSColor.CurrentControlTint;
  571. // NSColor text = NSColor.SelectedMenuItemText.UsingColorSpace (NSColorSpace.GenericRGBColorSpace);
  572. // NSColor color = tint == NSControlTint.Blue ? NSColor.SelectedMenuItem.UsingColorSpace (NSColorSpace.GenericRGBColorSpace) : NSColor.SelectedMenuItem.UsingColorSpace (NSColorSpace.DeviceWhite);
  573. //
  574. // color_hex = ConvertColorToHex (color);
  575. // text_hex = ConvertColorToHex (text);
  576. // } else {
  577. // color_hex = "#c5d4e0";
  578. // text_hex = "#000";
  579. // }
  580. //
  581. // string gtkrc = String.Format (@"
  582. // style ""treeview"" = ""default"" {{
  583. // base[SELECTED] = ""{0}""
  584. // base[ACTIVE] = ""{0}""
  585. // text[SELECTED] = ""{1}""
  586. // text[ACTIVE] = ""{1}""
  587. // engine ""xamarin"" {{
  588. // roundness = 0
  589. // gradient_shades = {{ 1.01, 1.01, 1.01, 1.01 }}
  590. // glazestyle = 1
  591. // }}
  592. // }}
  593. //
  594. // widget_class ""*.<GtkTreeView>*"" style ""treeview""
  595. // ",
  596. // color_hex,
  597. // text_hex
  598. // );
  599. //
  600. // Gtk.Rc.ParseString (gtkrc);
  601. }
  602. static TimeToCodeMetadata.DocumentType GetDocumentTypeFromFilename (string filename)
  603. {
  604. if (Projects.Services.ProjectService.IsWorkspaceItemFile (filename) || Projects.Services.ProjectService.IsSolutionItemFile (filename)) {
  605. return TimeToCodeMetadata.DocumentType.Solution;
  606. }
  607. return TimeToCodeMetadata.DocumentType.File;
  608. }
  609. void GlobalSetup ()
  610. {
  611. //FIXME: should we remove these when finalizing?
  612. try {
  613. ApplicationEvents.Reopen += delegate (object sender, ApplicationEventArgs e) {
  614. e.Handled = true;
  615. IdeApp.BringToFront ();
  616. };
  617. ApplicationEvents.OpenDocuments += delegate (object sender, ApplicationDocumentEventArgs e) {
  618. //OpenFiles may pump the mainloop, but can't do that from an AppleEvent
  619. GLib.Idle.Add (delegate {
  620. Ide.WelcomePage.WelcomePageService.HideWelcomePageOrWindow ();
  621. var trackTTC = IdeStartupTracker.StartupTracker.StartTimeToCodeLoadTimer ();
  622. IdeApp.OpenFilesAsync (e.Documents.Select (
  623. doc => new FileOpenInformation (doc.Key, null, doc.Value, 1, OpenDocumentOptions.DefaultInternal)),
  624. null
  625. ).ContinueWith ((result) => {
  626. if (!trackTTC) {
  627. return;
  628. }
  629. var firstFile = e.Documents.First ().Key;
  630. IdeStartupTracker.StartupTracker.TrackTimeToCode (GetDocumentTypeFromFilename (firstFile));
  631. });
  632. return false;
  633. });
  634. e.Handled = true;
  635. };
  636. ApplicationEvents.OpenUrls += delegate (object sender, ApplicationUrlEventArgs e) {
  637. GLib.Idle.Add (delegate {
  638. var trackTTC = IdeStartupTracker.StartupTracker.StartTimeToCodeLoadTimer ();
  639. // Open files via the monodevelop:// URI scheme, compatible with the
  640. // common TextMate scheme: http://blog.macromates.com/2007/the-textmate-url-scheme/
  641. IdeApp.OpenFilesAsync (e.Urls.Select (url => {
  642. try {
  643. var uri = new Uri (url);
  644. if (uri.Host != "open")
  645. return null;
  646. var qs = System.Web.HttpUtility.ParseQueryString (uri.Query);
  647. var fileUri = new Uri (qs ["file"]);
  648. int line, column;
  649. if (!Int32.TryParse (qs ["line"], out line))
  650. line = 1;
  651. if (!Int32.TryParse (qs ["column"], out column))
  652. column = 1;
  653. return new FileOpenInformation (Uri.UnescapeDataString(fileUri.AbsolutePath), null,
  654. line, column, OpenDocumentOptions.DefaultInternal);
  655. } catch (Exception ex) {
  656. LoggingService.LogError ("Invalid TextMate URI: " + url, ex);
  657. return null;
  658. }
  659. }).Where (foi => foi != null), null).ContinueWith ((result) => {
  660. if (!trackTTC) {
  661. return;
  662. }
  663. var firstFile = e.Urls.First ();
  664. IdeStartupTracker.StartupTracker.TrackTimeToCode (GetDocumentTypeFromFilename (firstFile));
  665. });
  666. return false;
  667. });
  668. };
  669. //if not running inside an app bundle (at dev time), need to do some additional setup
  670. if (NSBundle.MainBundle.InfoDictionary ["CFBundleIdentifier"] == null) {
  671. SetupWithoutBundle ();
  672. } else {
  673. SetupDockIcon ();
  674. }
  675. } catch (Exception ex) {
  676. LoggingService.LogError ("Could not install app event handlers", ex);
  677. setupFail = true;
  678. }
  679. }
  680. private static NSImage applicationIcon;
  681. internal static NSImage ApplicationIcon {
  682. get {
  683. if (applicationIcon == null) {
  684. // use the bundle icon by default
  685. // if run from a bundle, this will be the icon from the bundle icon file.
  686. // if not run from a bundle, this will be the default file icon of the mono framework folder.
  687. applicationIcon = NSWorkspace.SharedWorkspace.IconForFile (NSBundle.MainBundle.BundlePath);
  688. }
  689. return applicationIcon;
  690. }
  691. private set {
  692. NSApplication.SharedApplication.ApplicationIconImage = applicationIcon = value;
  693. }
  694. }
  695. static void SetupDockIcon ()
  696. {
  697. NSObject initialBundleIconFileValue;
  698. // Don't do anything if we're inside an app bundle.
  699. if (NSBundle.MainBundle.InfoDictionary.TryGetValue (new NSString ("CFBundleIconFile"), out initialBundleIconFileValue)) {
  700. return;
  701. }
  702. // Setup without bundle.
  703. FilePath exePath = System.Reflection.Assembly.GetExecutingAssembly ().Location;
  704. string iconName = BrandingService.GetString ("ApplicationIcon");
  705. string iconFile = null;
  706. if (iconName != null) {
  707. iconFile = BrandingService.GetFile (iconName);
  708. } else {
  709. // assume running from build directory
  710. var mdSrcMain = exePath.ParentDirectory.ParentDirectory.ParentDirectory;
  711. iconFile = mdSrcMain.Combine ("theme-icons", "Mac", "monodevelop.icns");
  712. }
  713. if (File.Exists (iconFile)) {
  714. ApplicationIcon = new NSImage (iconFile, lazy: true);
  715. }
  716. }
  717. static void SetupWithoutBundle ()
  718. {
  719. // set a bundle IDE to prevent NSProgress crash
  720. // https://bugzilla.xamarin.com/show_bug.cgi?id=8850
  721. NSBundle.MainBundle.InfoDictionary ["CFBundleIdentifier"] = new NSString ("com.xamarin.monodevelop");
  722. SetupDockIcon ();
  723. }
  724. static FilePath GetAppBundleRoot (FilePath path)
  725. {
  726. do {
  727. if (path.Extension == ".app")
  728. return path;
  729. } while ((path = path.ParentDirectory).IsNotNull);
  730. return null;
  731. }
  732. [GLib.ConnectBefore]
  733. static async void HandleDeleteEvent (object o, Gtk.DeleteEventArgs args)
  734. {
  735. args.RetVal = true;
  736. if (await IdeApp.Workspace.Close ()) {
  737. IdeApp.Workbench.Hide ();
  738. }
  739. }
  740. public static Gdk.Pixbuf GetPixbufFromNSImageRep (NSImageRep rep, int width, int height)
  741. {
  742. var rect = new CGRect (0, 0, width, height);
  743. var bitmap = rep as NSBitmapImageRep;
  744. try {
  745. if (bitmap == null) {
  746. using (var cgi = rep.AsCGImage (ref rect, null, null)) {
  747. if (cgi == null)
  748. return null;
  749. bitmap = new NSBitmapImageRep (cgi);
  750. }
  751. }
  752. return GetPixbufFromNSBitmapImageRep (bitmap, width, height);
  753. } finally {
  754. if (bitmap != null)
  755. bitmap.Dispose ();
  756. }
  757. }
  758. public static Gdk.Pixbuf GetPixbufFromNSImage (NSImage icon, int width, int height)
  759. {
  760. var rect = new CGRect (0, 0, width, height);
  761. var rep = icon.BestRepresentation (rect, null, null);
  762. var bitmap = rep as NSBitmapImageRep;
  763. try {
  764. if (bitmap == null) {
  765. if (rep != null)
  766. rep.Dispose ();
  767. using (var cgi = icon.AsCGImage (ref rect, null, null)) {
  768. if (cgi == null)
  769. return null;
  770. bitmap = new NSBitmapImageRep (cgi);
  771. }
  772. }
  773. return GetPixbufFromNSBitmapImageRep (bitmap, width, height);
  774. } finally {
  775. if (bitmap != null)
  776. bitmap.Dispose ();
  777. }
  778. }
  779. static Gdk.Pixbuf GetPixbufFromNSBitmapImageRep (NSBitmapImageRep bitmap, int width, int height)
  780. {
  781. byte[] data;
  782. using (var tiff = bitmap.TiffRepresentation) {
  783. data = new byte[tiff.Length];
  784. System.Runtime.InteropServices.Marshal.Copy (tiff.Bytes, data, 0, data.Length);
  785. }
  786. int pw = (int)bitmap.PixelsWide, ph = (int)bitmap.PixelsHigh;
  787. var pixbuf = new Gdk.Pixbuf (data, pw, ph);
  788. // if one dimension matches, and the other is same or smaller, use as-is
  789. if ((pw == width && ph <= height) || (ph == height && pw <= width))
  790. return pixbuf;
  791. // otherwise scale proportionally such that the largest dimension matches the desired size
  792. if (pw == ph) {
  793. pw = width;
  794. ph = height;
  795. } else if (pw > ph) {
  796. ph = (int) (width * ((float) ph / pw));
  797. pw = width;
  798. } else {
  799. pw = (int) (height * ((float) pw / ph));
  800. ph = height;
  801. }
  802. var scaled = pixbuf.ScaleSimple (pw, ph, Gdk.InterpType.Bilinear);
  803. pixbuf.Dispose ();
  804. return scaled;
  805. }
  806. protected override Xwt.Drawing.Image OnGetIconForFile (string filename)
  807. {
  808. //this only works on MacOS 10.6.0 and greater
  809. if (MacSystemInformation.OsVersion < MacSystemInformation.SnowLeopard)
  810. return base.OnGetIconForFile (filename);
  811. NSImage icon = null;
  812. if (Path.IsPathRooted (filename) && File.Exists (filename)) {
  813. icon = NSWorkspace.SharedWorkspace.IconForFile (filename);
  814. } else {
  815. string extension = Path.GetExtension (filename);
  816. if (!string.IsNullOrEmpty (extension))
  817. icon = NSWorkspace.SharedWorkspace.IconForFileType (extension);
  818. }
  819. if (icon == null) {
  820. return base.OnGetIconForFile (filename);
  821. }
  822. int w, h;
  823. if (!Gtk.Icon.SizeLookup (Gtk.IconSize.Menu, out w, out h)) {
  824. w = h = 22;
  825. }
  826. var res = GetPixbufFromNSImage (icon, w, h);
  827. return res != null ? res.ToXwtImage () : base.OnGetIconForFile (filename);
  828. }
  829. public override ProcessAsyncOperation StartConsoleProcess (string command, string arguments, string workingDirectory,
  830. IDictionary<string, string> environmentVariables,
  831. string title, bool pauseWhenFinished)
  832. {
  833. return new MacExternalConsoleProcess (command, arguments, workingDirectory, environmentVariables,
  834. title, pauseWhenFinished);
  835. }
  836. public override bool CanOpenTerminal {
  837. get {
  838. return true;
  839. }
  840. }
  841. public override void OpenTerminal (FilePath directory, IDictionary<string, string> environmentVariables, string title)
  842. {
  843. string tabId, windowId;
  844. MacExternalConsoleProcess.RunTerminal (
  845. null, null, directory, environmentVariables, title, false, out tabId, out windowId
  846. );
  847. }
  848. public override IEnumerable<DesktopApplication> GetApplications (string filename)
  849. {
  850. return GetApplications (filename, DesktopApplicationRole.All);
  851. }
  852. public override IEnumerable<DesktopApplication> GetApplications (string filename, DesktopApplicationRole role)
  853. {
  854. static global::CoreServices.LSRoles ToLSRoles (DesktopApplicationRole role)
  855. {
  856. var lsRole = global::CoreServices.LSRoles.None;
  857. if (role == DesktopApplicationRole.All)
  858. return global::CoreServices.LSRoles.All;
  859. if (role.HasFlag (DesktopApplicationRole.Viewer))
  860. lsRole |= global::CoreServices.LSRoles.Viewer;
  861. if (role.HasFlag (DesktopApplicationRole.Editor))
  862. lsRole |= global::CoreServices.LSRoles.Editor;
  863. if (role.HasFlag (DesktopApplicationRole.Shell))
  864. lsRole |= global::CoreServices.LSRoles.Shell;
  865. return lsRole;
  866. }
  867. //FIXME: we should disambiguate dupliacte apps in different locations and display both
  868. //for now, just filter out the duplicates
  869. var checkUniqueName = new HashSet<string> ();
  870. var checkUniquePath = new HashSet<string> ();
  871. //FIXME: bundle path is wrong because of how MD is built into an app
  872. //var thisPath = NSBundle.MainBundle.BundleUrl.Path;
  873. //checkUniquePath.Add (thisPath);
  874. checkUniqueName.Add ("MonoDevelop");
  875. checkUniqueName.Add (BrandingService.ApplicationName);
  876. NSUrl url = CreateNSUrl (filename);
  877. var def = global::CoreServices.LaunchServices.GetDefaultApplicationUrlForUrl (url);
  878. var apps = new List<DesktopApplication> ();
  879. var retrievedApps = global::CoreServices.LaunchServices.GetApplicationUrlsForUrl (url, ToLSRoles (role));
  880. if (retrievedApps != null) {
  881. foreach (var app in retrievedApps) {
  882. if (string.IsNullOrEmpty (app.Path) || !checkUniquePath.Add (app.Path))
  883. continue;
  884. if (checkUniqueName.Add (app.LastPathComponent)) {
  885. apps.Add (new MacDesktopApplication (app.Path, Path.GetFileNameWithoutExtension (app.LastPathComponent), def != null && def == app));
  886. }
  887. }
  888. }
  889. apps.Sort ((DesktopApplication a, DesktopApplication b) => {
  890. int r = a.IsDefault.CompareTo (b.IsDefault);
  891. if (r != 0)
  892. return -r;
  893. return a.DisplayName.CompareTo (b.DisplayName);
  894. });
  895. return apps;
  896. }
  897. static NSUrl CreateNSUrl (string filename)
  898. {
  899. if (Uri.TryCreate (filename, UriKind.Absolute, out Uri hyperlink) && !hyperlink.IsFile)
  900. return NSUrl.FromString (filename);
  901. return NSUrl.FromFilename (filename);
  902. }
  903. class MacDesktopApplication : DesktopApplication
  904. {
  905. public MacDesktopApplication (string app, string name, bool isDefault) : base (app, name, isDefault)
  906. {
  907. }
  908. public override void Launch (params string[] files)
  909. {
  910. NSWorkspace.SharedWorkspace.OpenUrls (
  911. Array.ConvertAll (files, file => CreateNSUrl (file)),
  912. NSBundle.FromPath (Id).BundleIdentifier,
  913. NSWorkspaceLaunchOptions.Default,
  914. NSAppleEventDescriptor.DescriptorWithBoolean (true));
  915. }
  916. }
  917. public override Xwt.Rectangle GetUsableMonitorGeometry (int screenNumber, int monitorNumber)
  918. {
  919. var screen = Gdk.Display.Default.GetScreen (screenNumber);
  920. Gdk.Rectangle ygeometry = screen.GetMonitorGeometry (monitorNumber);
  921. Gdk.Rectangle xgeometry = screen.GetMonitorGeometry (0);
  922. NSScreen nss = NSScreen.Screens[monitorNumber];
  923. var visible = nss.VisibleFrame;
  924. var frame = nss.Frame;
  925. // Note: Frame and VisibleFrame rectangles are relative to monitor 0, but we need absolute
  926. // coordinates.
  927. visible.X += xgeometry.X;
  928. frame.X += xgeometry.X;
  929. // VisibleFrame.Y is the height of the Dock if it is at the bottom of the screen, so in order
  930. // to get the menu height, we just figure out the difference between the visibleFrame height
  931. // and the actual frame height, then subtract the Dock height.
  932. //
  933. // We need to swap the Y offset with the menu height because our callers expect the Y offset
  934. // to be from the top of the screen, not from the bottom of the screen.
  935. nfloat x, y, width, height;
  936. if (visible.Height <= frame.Height) {
  937. var dockHeight = visible.Y - frame.Y;
  938. var menubarHeight = (frame.Height - visible.Height) - dockHeight;
  939. height = frame.Height - menubarHeight - dockHeight;
  940. y = ygeometry.Y + menubarHeight;
  941. } else {
  942. height = frame.Height;
  943. y = ygeometry.Y;
  944. }
  945. // Takes care of the possibility of the Dock being positioned on the left or right edge of the screen.
  946. width = NMath.Min (visible.Width, frame.Width);
  947. x = NMath.Max (visible.X, frame.X);
  948. return new Xwt.Rectangle ((int) x, (int) y, (int) width, (int) height);
  949. }
  950. internal override void GrabDesktopFocus (Gtk.Window window)
  951. {
  952. window.Present ();
  953. NSApplication.SharedApplication.ActivateIgnoringOtherApps (true);
  954. }
  955. public override Window GetParentForModalWindow ()
  956. {
  957. try {
  958. var window = NSApplication.SharedApplication.ModalWindow;
  959. if (window != null)
  960. return window;
  961. } catch (Exception e) {
  962. LoggingService.LogInternalError ("Getting SharedApplication.ModalWindow failed", e);
  963. }
  964. try {
  965. var window = NSApplication.SharedApplication.KeyWindow;
  966. if (window != null)
  967. return window;
  968. } catch (Exception e) {
  969. LoggingService.LogInternalError ("Getting SharedApplication.KeyWindow failed", e);
  970. }
  971. try {
  972. var window = NSApplication.SharedApplication.MainWindow;
  973. if (window != null)
  974. return window;
  975. } catch (Exception e) {
  976. LoggingService.LogInternalError ("Getting SharedApplication.MainWindow failed", e);
  977. }
  978. return null;
  979. }
  980. bool HasAnyDockWindowFocused ()
  981. {
  982. foreach (var window in Gtk.Window.ListToplevels ()) {
  983. // Gtk.Window.HasToplevelFocus may return false for a window that embeds a Cocoa view
  984. if (!window.HasToplevelFocus && GtkQuartz.GetWindow (window) != NSApplication.SharedApplication.KeyWindow) {
  985. continue;
  986. }
  987. if (window is Components.Docking.DockFloatingWindow floatingWindow) {
  988. return true;
  989. }
  990. if (window is IdeWindow ideWindow && ideWindow.Child is Components.Docking.AutoHideBox) {
  991. return true;
  992. }
  993. }
  994. return false;
  995. }
  996. bool CheckIfTopWindowIsWorkbench ()
  997. {
  998. foreach (var window in Gtk.Window.ListToplevels ()) {
  999. if (!window.HasToplevelFocus) {
  1000. continue;
  1001. }
  1002. if (window is DefaultWorkbench) {
  1003. return true;
  1004. }
  1005. }
  1006. return false;
  1007. }
  1008. public override Window GetFocusedTopLevelWindow ()
  1009. {
  1010. if (NSApplication.SharedApplication.KeyWindow != null) {
  1011. if (IdeApp.Workbench?.RootWindow?.Visible == true) {
  1012. //if is a docking window then return the current root window
  1013. if (CheckIfTopWindowIsWorkbench () || HasAnyDockWindowFocused ()) {
  1014. return MessageService.RootWindow;
  1015. }
  1016. }
  1017. return NSApplication.SharedApplication.KeyWindow;
  1018. }
  1019. return null;
  1020. }
  1021. public override void FocusWindow (Window window)
  1022. {
  1023. try {
  1024. NSWindow nswindow = window; // will also get an NSWindow from a Gtk.Window
  1025. if (nswindow != null) {
  1026. nswindow.MakeKeyAndOrderFront (nswindow);
  1027. return;
  1028. }
  1029. } catch (Exception ex) {
  1030. LoggingService.LogError ("Focusing window failed: not an NSWindow", ex);
  1031. }
  1032. base.FocusWindow (window);
  1033. }
  1034. static Cairo.Color ConvertColor (NSColor color)
  1035. {
  1036. nfloat r, g, b, a;
  1037. if (color.ColorSpaceName == NSColorSpace.DeviceWhite) {
  1038. a = 1.0f;
  1039. r = g = b = color.WhiteComponent;
  1040. } else {
  1041. color.GetRgba (out r, out g, out b, out a);
  1042. }
  1043. return new Cairo.Color (r, g, b, a);
  1044. }
  1045. static string ConvertColorToHex (NSColor color)
  1046. {
  1047. nfloat r, g, b, a;
  1048. if (color.ColorSpaceName == NSColorSpace.DeviceWhite) {
  1049. a = 1.0f;
  1050. r = g = b = color.WhiteComponent;
  1051. } else {
  1052. color.GetRgba (out r, out g, out b, out a);
  1053. }
  1054. return String.Format ("#{0}{1}{2}",
  1055. ((int)(r * 255)).ToString ("x2"),
  1056. ((int)(g * 255)).ToString ("x2"),
  1057. ((int)(b * 255)).ToString ("x2")
  1058. );
  1059. }
  1060. internal static int GetTitleBarHeight ()
  1061. {
  1062. var frame = new CGRect (0, 0, 100, 100);
  1063. var rect = NSWindow.ContentRectFor (frame, NSWindowStyle.Titled);
  1064. return (int)(frame.Height - rect.Height);
  1065. }
  1066. internal static int GetTitleBarHeight (NSWindow w)
  1067. {
  1068. int height = 0;
  1069. if (w.StyleMask.HasFlag (NSWindowStyle.Titled))
  1070. height += GetTitleBarHeight ();
  1071. if (w.Toolbar != null) {
  1072. var rect = NSWindow.ContentRectFor (w.Frame, w.StyleMask);
  1073. height += (int)(rect.Height - w.ContentView.Frame.Height);
  1074. }
  1075. return height;
  1076. }
  1077. internal static NSImage LoadImage (string resource)
  1078. {
  1079. using (var stream = typeof (MacPlatformService).Assembly.GetManifestResourceStream (resource))
  1080. using (NSData data = NSData.FromStream (stream)) {
  1081. return new NSImage (data);
  1082. }
  1083. }
  1084. internal override void SetMainWindowDecorations (Gtk.Window window)
  1085. {
  1086. NSWindow w = GtkQuartz.GetWindow (window);
  1087. w.IsOpaque = true;
  1088. w.StyleMask |= NSWindowStyle.UnifiedTitleAndToolbar;
  1089. IdeTheme.ApplyTheme (w);
  1090. }
  1091. internal override void RemoveWindowShadow (Gtk.Window window)
  1092. {
  1093. if (window == null)
  1094. throw new ArgumentNullException (nameof(window));
  1095. var w = GtkQuartz.GetWindow (window);
  1096. if (w != null)
  1097. w.HasShadow = false;
  1098. }
  1099. internal override IMainToolbarView CreateMainToolbar (Gtk.Window window)
  1100. {
  1101. return new MonoDevelop.MacIntegration.MainToolbar.MainToolbar (window);
  1102. }
  1103. internal override void AttachMainToolbar (Gtk.VBox parent, IMainToolbarView toolbar)
  1104. {
  1105. var nativeToolbar = (MonoDevelop.MacIntegration.MainToolbar.MainToolbar)toolbar;
  1106. NSWindow w = GtkQuartz.GetWindow (nativeToolbar.gtkWindow);
  1107. if (MacSystemInformation.OsVersion >= MacSystemInformation.Yosemite)
  1108. w.TitleVisibility = NSWindowTitleVisibility.Hidden;
  1109. w.Toolbar = nativeToolbar.widget;
  1110. nativeToolbar.Initialize ();
  1111. }
  1112. protected override RecentFiles CreateRecentFilesProvider ()
  1113. {
  1114. return new FdoRecentFiles (UserProfile.Current.LocalConfigDir.Combine ("RecentlyUsed.xml"));
  1115. }
  1116. public override bool GetIsFullscreen (Window window)
  1117. {
  1118. if (MacSystemInformation.OsVersion < MacSystemInformation.Lion) {
  1119. return base.GetIsFullscreen (window);
  1120. }
  1121. NSWindow nswin = window;
  1122. return (nswin.StyleMask & NSWindowStyle.FullScreenWindow) != 0;
  1123. }
  1124. public override void SetIsFullscreen (Window window, bool isFullscreen)
  1125. {
  1126. if (MacSystemInformation.OsVersion < MacSystemInformation.Lion) {
  1127. base.SetIsFullscreen (window, isFullscreen);
  1128. return;
  1129. }
  1130. NSWindow nswin = GtkQuartz.GetWindow (window);
  1131. if (isFullscreen != ((nswin.StyleMask & NSWindowStyle.FullScreenWindow) != 0))
  1132. nswin.ToggleFullScreen (null);
  1133. }
  1134. public override bool IsModalDialogRunning ()
  1135. {
  1136. if (NSApplication.SharedApplication.ModalWindow != null)
  1137. return true;
  1138. var toplevels = Gtk.Window.ListToplevels ();
  1139. var ret = toplevels.Any (w => w.Modal && w.Visible && GtkQuartz.GetWindow (w)?.DebugDescription.StartsWith ("<_NSFullScreenTileDividerWindow", StringComparison.Ordinal) != true);
  1140. return ret;
  1141. }
  1142. internal override void AddChildWindow (Gtk.Window parent, Gtk.Window child)
  1143. {
  1144. NSWindow w = GtkQuartz.GetWindow (parent);
  1145. child.Realize ();
  1146. NSWindow overlay = GtkQuartz.GetWindow (child);
  1147. overlay.SetExcludedFromWindowsMenu (true);
  1148. w.AddChildWindow (overlay, NSWindowOrderingMode.Above);
  1149. }
  1150. internal override void PlaceWindow (Gtk.Window window, int x, int y, int width, int height)
  1151. {
  1152. if (window.GdkWindow == null)
  1153. return; // Not yet realized
  1154. NSWindow w = GtkQuartz.GetWindow (window);
  1155. y += GetTitleBarHeight (w);
  1156. var dr = FromDesktopRect (new Gdk.Rectangle (x, y, width, height));
  1157. var r = w.FrameRectFor (dr);
  1158. w.SetFrame (r, true);
  1159. base.PlaceWindow (window, x, y, width, height);
  1160. }
  1161. static CGRect FromDesktopRect (Gdk.Rectangle r)
  1162. {
  1163. var desktopBounds = CalcDesktopBounds ();
  1164. r.Y = desktopBounds.Height - r.Y - r.Height;
  1165. if (desktopBounds.Y < 0)
  1166. r.Y += desktopBounds.Y;
  1167. return new CGRect (desktopBounds.X + r.X, r.Y, r.Width, r.Height);
  1168. }
  1169. static Gdk.Rectangle CalcDesktopBounds ()
  1170. {
  1171. var desktopBounds = new Gdk.Rectangle ();
  1172. foreach (var s in NSScreen.Screens) {
  1173. var r = s.Frame;
  1174. desktopBounds = desktopBounds.Union (new Gdk.Rectangle ((int)r.X, (int)r.Y, (int)r.Width, (int)r.Height));
  1175. }
  1176. return desktopBounds;
  1177. }
  1178. public override void OpenFolder (FilePath folderPath, FilePath[] selectFiles)
  1179. {
  1180. if (selectFiles.Length == 0) {
  1181. System.Diagnostics.Process.Start (folderPath);
  1182. } else {
  1183. NSWorkspace.SharedWorkspace.ActivateFileViewer (selectFiles.Select ((f) => NSUrl.FromFilename (f)).ToArray ());
  1184. }
  1185. }
  1186. internal override void RestartIde (bool reopenWorkspace)
  1187. {
  1188. FilePath bundlePath = NSBundle.MainBundle.BundlePath;
  1189. if (bundlePath.Extension != ".app") {
  1190. base.RestartIde (reopenWorkspace);
  1191. return;
  1192. }
  1193. var reopen = reopenWorkspace && IdeApp.Workspace != null && IdeApp.Workspace.Items.Count > 0;
  1194. var proc = new Process ();
  1195. var path = bundlePath.Combine ("Contents", "MacOS");
  1196. //assume renames of mdtool end with "tool"
  1197. var mdtool = Directory.EnumerateFiles (path, "*tool").Single();
  1198. var psi = new ProcessStartInfo (mdtool) {
  1199. CreateNoWindow = true,
  1200. UseShellExecute = false,
  1201. WorkingDirectory = path,
  1202. Arguments = "--start-app-bundle",
  1203. };
  1204. var recentWorkspace = reopen ? IdeServices.DesktopService.RecentFiles.GetProjects ().FirstOrDefault ()?.FileName : string.Empty;
  1205. if (!string.IsNullOrEmpty (recentWorkspace))
  1206. psi.Arguments += " " + recentWorkspace;
  1207. proc.StartInfo = psi;
  1208. proc.Start ();
  1209. }
  1210. internal override IPlatformTelemetryDetails CreatePlatformTelemetryDetails ()
  1211. {
  1212. return MacTelemetryDetails.CreateTelemetryDet