using System; using MonoMac.CoreFoundation; using MonoMac.Foundation; using MonoMac.AppKit; using BizHawk.Client.EmuHawk; using System.Windows.Forms; using System.Reflection; using BizHawk.Common; using BizHawk.Client.Common; namespace MonoMacWrapper { [MonoMac.Foundation.Register("AppDelegate")] public class AppDelegate : NSApplicationDelegate { private System.Collections.Generic.Dictionary _menuLookup; private NSTimer _masterTimer; private MainForm _mainWinForm; private Action _queuedAction; public AppDelegate(){} public override void FinishedLaunching(NSObject notification) { NSApplication.SharedApplication.BeginInvokeOnMainThread(()=> { StartApplication(); }); } public override void WillTerminate (NSNotification notification) { //Doesn't seem to be called anymore, so I override the quit option myself. _mainWinForm.Close(); } public override void DidResignActive (NSNotification notification) { GlobalWin.IsApplicationActive = false; //Note: These events that are supposed to notify us when entering or leaving the background sometimes //don't fire because the run loop is hogging most of the time on the main thread which is supposed to fire them. //Unfortunately, the value I could pull it from manually, NSApplication.SharedApplication.Active, is not updated either. //So this works about 80% of the time, but sometimes it does not. Work-around is to click away and click back again. } public override void DidBecomeActive (NSNotification notification) { GlobalWin.IsApplicationActive = true; } private void StartApplication() { NSUrl[] urls = NSFileManager.DefaultManager.GetUrls(NSSearchPathDirectory.ApplicationSupportDirectory, NSSearchPathDomain.User); if (urls.Length > 0) { string bizhawkSupport = System.IO.Path.Combine(urls[0].Path, "BizHawk/"); if(!System.IO.Directory.Exists(bizhawkSupport)) { System.IO.Directory.CreateDirectory(bizhawkSupport); } string iniPath = System.IO.Path.Combine(bizhawkSupport, "config.ini"); BizHawk.Client.Common.PathManager.DefaultIniPath = iniPath; } BizHawk.Client.EmuHawk.HawkDialogFactory.OpenDialogClass = typeof(MacOpenFileDialog); BizHawk.Client.EmuHawk.HawkDialogFactory.SaveDialogClass = typeof(MacSaveFileDialog); BizHawk.Client.EmuHawk.HawkDialogFactory.FolderBrowserClass = typeof(MacFolderBrowserDialog); Global.Config = ConfigService.Load(PathManager.DefaultIniPath); GlobalWin.IGL_GL = new BizHawk.Bizware.BizwareGL.Drivers.OpenTK.IGL_TK(2,0,false); //setup the GL context manager, needed for coping with multiple opengl cores vs opengl display method GLManager.CreateInstance(GlobalWin.IGL_GL); GlobalWin.GLManager = GLManager.Instance; GlobalWin.GL = GlobalWin.IGL_GL; BizHawk.Common.HawkFile.ArchiveHandlerFactory = new SevenZipSharpArchiveHandler(); try { _mainWinForm = new BizHawk.Client.EmuHawk.MainForm(new string[0]); var title = _mainWinForm.Text; _mainWinForm.Show(); DoMenuExtraction(); var appMenuItems = NSApplication.SharedApplication.MainMenu.ItemAt(0).Submenu.ItemArray(); appMenuItems[appMenuItems.Length-1].Action = new MonoMac.ObjCRuntime.Selector("OnAppQuit"); _mainWinForm.MainMenuStrip.Visible = false; //Hide the real one, since it's been extracted _mainWinForm.Text = title; //Timer assumes 60hz display. Note that timers are not very accurate, and I should really be doing this a different way. //Can't use CVDisplayLink, because that just notifies us of when we need to push video to the display. //Both macOS and WinForms need to share the run loop, and this solution produces the most responsive compromise allowing //them both to coexist. There seem to be frames occasionally dropped that the WinForms UI doesn't know about because //I can set Frameskip to 0 with VSync enabled and the game execution is still full speed. _masterTimer = NSTimer.CreateRepeatingTimer(1.0/60.0, MacRunLoop); NSRunLoop.Current.AddTimer(_masterTimer, NSRunLoopMode.Common); } catch (Exception e) { NSAlert nsa = new NSAlert(); nsa.MessageText = e.ToString(); nsa.RunModal(); } } private void MacRunLoop(){ bool runLoopVal = true; for (int i = 0; i < 1; i++) { runLoopVal &= _mainWinForm.RunLoopCore(); if(!runLoopVal) break; } if (runLoopVal) { if (_queuedAction != null) { _queuedAction.Invoke (); //Needs to happen in the same context as the RunLoop, otherwise we'll get weird behavior. _queuedAction = null; RefreshAllMenus(); } } else { _masterTimer.Invalidate(); NSApplication.SharedApplication.Terminate(this); } } private void DoMenuExtraction() { _menuLookup = new System.Collections.Generic.Dictionary(); ExtractMenus(_mainWinForm.MainMenuStrip); } private void ExtractMenus(System.Windows.Forms.MenuStrip menus) { for(int i=0; i 0) { NSMenu dropDown = new NSMenu(CleanMenuString(item.Text)); translated.Submenu = dropDown; ExecuteDropDownOpened(menuItem); ExtractSubmenu(menuItem.DropDownItems, dropDown, false); } } else if(item is ToolStripSeparator) { destMenu.AddItem(NSMenuItem.SeparatorItem); } } } private void ExecuteDropDownOpened(ToolStripMenuItem item) { var dropDownOpeningKey = typeof(ToolStripDropDownItem).GetField("DropDownOpenedEvent", BindingFlags.Static | BindingFlags.NonPublic); var eventProp = typeof(ToolStripDropDownItem).GetProperty("Events", BindingFlags.Instance | BindingFlags.NonPublic); if (eventProp != null && dropDownOpeningKey != null) { var dropDownOpeningValue = dropDownOpeningKey.GetValue(item); var eventList = eventProp.GetValue(item, null) as System.ComponentModel.EventHandlerList; if(eventList != null) { Delegate ddd = eventList[dropDownOpeningValue]; try{ if(ddd!=null) ddd.DynamicInvoke(null, EventArgs.Empty); } catch(Exception ex){ //throw ex; } } } } private void HandleItemVisibleChanged(object sender, EventArgs e) { if(sender is ToolStripMenuItem && _menuLookup.ContainsKey((ToolStripMenuItem)sender)) { MenuItemAdapter translated = _menuLookup[(ToolStripMenuItem)sender]; translated.Hidden = !translated.Hidden; //Can't actually look at Visible property because the entire menubar is hidden. //Since the event only gets called when Visible is changed, we can assume it got flipped. if(((ToolStripMenuItem)sender).Text.Equals("&NES")){ //Hack to rebuild menu contents due to changing FDS sub-menu. //At some point, I might want to figure out a better way to do this. RemoveMenuItems(translated); ExtractSubmenu(translated.HostMenu.DropDownItems, translated.Submenu, false); } } } private void RemoveMenuItems(MenuItemAdapter menu) { if(menu.HasSubmenu) { for(int i=menu.Submenu.Count-1; i>=0; i--) { MenuItemAdapter item = menu.Submenu.ItemAt(i) as MenuItemAdapter; if(item != null) //It will be null if it's a separator { RemoveMenuItems(item); if(_menuLookup.ContainsKey(item.HostMenu)) { _menuLookup.Remove(item.HostMenu); } item.HostMenu.CheckedChanged -= HandleMenuItemCheckedChanged; item.HostMenu.EnabledChanged -= HandleMenuItemEnabledChanged; } menu.Submenu.RemoveItemAt(i); } } } private void HandleMenuItemEnabledChanged(object sender, EventArgs e) { if(sender is ToolStripMenuItem && _menuLookup.ContainsKey((ToolStripMenuItem)sender)) { MenuItemAdapter translated = _menuLookup[(ToolStripMenuItem)sender]; translated.Enabled = translated.HostMenu.Enabled; } } private void HandleMenuItemCheckedChanged(object sender, EventArgs e) { if(sender is ToolStripMenuItem && _menuLookup.ContainsKey((ToolStripMenuItem)sender)) { MenuItemAdapter translated = _menuLookup[(ToolStripMenuItem)sender]; translated.State = translated.HostMenu.Checked ? NSCellStateValue.On : NSCellStateValue.Off; } } private void RefreshAllMenus(){ for (int i = 0; i < _mainWinForm.MainMenuStrip.Items.Count; i++) { ToolStripMenuItem item = _mainWinForm.MainMenuStrip.Items[i] as ToolStripMenuItem; MenuItemAdapter mia = _menuLookup[item]; if (mia != null) { RemoveMenuItems(mia); ExtractSubmenu(mia.HostMenu.DropDownItems, mia.Submenu, i==0); } } } private static NSImage ImageToCocoa(System.Drawing.Image input) { System.IO.MemoryStream ms = new System.IO.MemoryStream(); input.Save(ms,System.Drawing.Imaging.ImageFormat.Png); ms.Position = 0; NSImage img = NSImage.FromStream(ms); img.Size = new System.Drawing.SizeF(16f, 16f); //Some of BizHawk's menu icons are larger, even though WinForms only does 16x16. return img; } private static string CleanMenuString(string text) { return text.Replace("&",string.Empty); } private class MenuItemAdapter : NSMenuItem { public MenuItemAdapter(ToolStripMenuItem host) : base(CleanMenuString(host.Text)) { HostMenu = host; } public ToolStripMenuItem HostMenu { get;set; } } [Export("HandleMenu:")] private void HandleMenu(MenuItemAdapter item) { _queuedAction = new Action(item.HostMenu.PerformClick); } [Export("OnAppQuit")] private void OnQuit () { _mainWinForm.Close(); } } }