Search code examples
c#macosxamarinnspanel

How do I get keyboard events in the topmost NSPanel?


I have created an app using Xamarin to help watching movies online. It shows the subtitles on top of all other windows. This has been done using the NSPanel, as it was the only way to make it work on MacOS Mojave.

The app works well. Now I want to improve the app by making NSPanel respond to the keyboard events, so I can control the app by using the keyboard for pausing, playing, going backward or going forward.

How do I get keyboard events in the topmost NSPanel?

I tried to use this code:

NSEvent.AddLocalMonitorForEventsMatchingMask(NSEventMask.KeyDown, KeyboardEventHandler);

private static NSEvent KeyboardEventHandler(NSEvent keyEvent)
{
    // handle key down events here
    return (keyEvent);
}

But it only works when the app is not in the full-screen mode.

The full SubtitlesViewer-MACOS project can be found here.

Here is the part of the code that creates the panel:

public override void ViewWillAppear()
{
    base.ViewWillAppear();
    SetupView();
}

private void SetupView()
{ 
    var screenRes = screenResolution();
    int PANEL_HEIGHT = 200;
    subtitlesPanel = new NSPanel
    (
        new CoreGraphics.CGRect(40, 50, screenRes.Width - 80, PANEL_HEIGHT),
        NSWindowStyle.Titled | NSWindowStyle.Closable | NSWindowStyle.Resizable | NSWindowStyle.Miniaturizable | NSWindowStyle.DocModal,
        NSBackingStore.Buffered, true
    )
    {
        BackgroundColor = NSColor.FromCalibratedRgba(0, 0, 0, 0.0f),
        ReleasedWhenClosed = true,
        HidesOnDeactivate = false,
        FloatingPanel = true,
        StyleMask = NSWindowStyle.NonactivatingPanel,
        Level = NSWindowLevel.MainMenu - 1,
        IsMovable = true,
        CollectionBehavior = NSWindowCollectionBehavior.CanJoinAllSpaces |
        NSWindowCollectionBehavior.FullScreenAuxiliary
    };

    subtitlesPanel.OrderFront(null);

    subtitleTextButton = new NSButton(new CoreGraphics.CGRect(40, 0, screenRes.Width - 120, PANEL_HEIGHT-30))
    {
        Title = "",
        WantsLayer = true
    };

    subtitleTextButton.Layer.BackgroundColor = NSColor.Clear.CGColor;

    subtitleTextField = new NSTextField(new CoreGraphics.CGRect(40, 0, screenRes.Width - 120, PANEL_HEIGHT-30))
    {
        Alignment = NSTextAlignment.Center
    };
    subtitleTextField.Cell.Alignment = NSTextAlignment.Center;

    forwardButton = new NSButton(new CoreGraphics.CGRect(0, 0, 40, 30));
    forwardButton.Title = ">>";
    forwardButton.Activated += (object sender, EventArgs e) => {
        subtitlesProvider.Forward();
    };

    backButton = new NSButton(new CoreGraphics.CGRect(0, 30, 40, 30));
    backButton.Title = "<<";
    backButton.Activated += (object sender, EventArgs e) => {
        subtitlesProvider.Back();
    };

    startStopButton = new NSButton(new CoreGraphics.CGRect(0, 60, 40, 30));
    startStopButton.Title = "Play";
    startStopButton.Activated += (object sender, EventArgs e) => {
        subtitlesProvider.StartStop(subtitlesProvider.Playing);
    };

    subtitlesPanel.ContentView.AddSubview(subtitleTextButton, NSWindowOrderingMode.Below, null);
    subtitlesPanel.ContentView.AddSubview(subtitleTextField, NSWindowOrderingMode.Below, null);

    subtitlesPanel.ContentView.AddSubview(forwardButton, NSWindowOrderingMode.Below, null);
    subtitlesPanel.ContentView.AddSubview(backButton, NSWindowOrderingMode.Below, null);
    subtitlesPanel.ContentView.AddSubview(startStopButton, NSWindowOrderingMode.Below, null);

    SetupSubtitlesProvider();
}

Please kindly advice what else should I try to make it work.


Solution

  • I have found the solution here: keyDown not being called

    This is my implementation of NSPanelExt class to handle the keys.

    public class NSPanelExt : NSPanel
    {
        public KeyPressedHandler KeyPressed;
        public delegate void KeyPressedHandler(KeyCodeEventArgs e);
    
        public NSPanelExt(CGRect contentRect, NSWindowStyle aStyle, NSBackingStore bufferingType, bool deferCreation) : base(contentRect, aStyle, bufferingType, deferCreation)
        {
        }
    
        public override bool CanBecomeMainWindow => true;
    
        public override bool CanBecomeKeyWindow => true;
    
        public override bool AcceptsFirstResponder()
        {
            return true;
        }
    
    
        public override void KeyDown(NSEvent theEvent)
        {
            // this function is never called
            KeyPressed?.Invoke(new KeyCodeEventArgs {  Key = GetKeyCode(theEvent.KeyCode) });
    
        }
    
        private KeyCode GetKeyCode(ushort keyCode)
        {
            KeyCode result = KeyCode.Unknown;
            switch (keyCode)
            {
                case 123:
                    result = KeyCode.Left;
                    break;
                case 49:
                    result = KeyCode.Space;
                    break;
                case 124:
                    result = KeyCode.Right;
                    break;
                case 53:
                    result = KeyCode.Esc;
                    break;
            }
    
            return result;
        }
    

    I have also updated the ViewController to keep NSPanel always active.

    public partial class ViewController : NSViewController
        {
            // ...
            private NSButton startStopButton;
            Timer _timer = new Timer();
    
            private void SetupView()
            { 
            // ...
        subtitlesPanel.KeyPressed += SubtitlesPanel_KeyPressed;
    
            // ...   
                IntializeKeepWindowFocusedTimer();
            }
    
            void SubtitlesPanel_KeyPressed(KeyCodeEventArgs e)
            {
                switch(e.Key)
                {
                    case KeyCode.Left:
                        backButton.PerformClick(this);
                        break;
                    case KeyCode.Right:
                        forwardButton.PerformClick(this);
                        break;
                    case KeyCode.Space:
                        startStopButton.PerformClick(this);
                            break;
                    case KeyCode.Esc:
                        _timer.Stop();
                        break;
                }
            }
    
    
            private void IntializeKeepWindowFocusedTimer()
            {
                _timer.Interval = 200;  //in milliseconds
                _timer.Elapsed += Timer_Elapsed;;
                _timer.AutoReset = true;
                _timer.Enabled = true;
            }
    
            void Timer_Elapsed(object sender, ElapsedEventArgs e)
            {
                NSApplication.SharedApplication.BeginInvokeOnMainThread(() =>
                {
                    subtitlesPanel.MakeKeyWindow();
                    if (SetSubtitleNeeded)
                    {
                        subtitlesProvider.SetSubTitle(0);
                        startStopButton.Title = "Stop";
                        SetSubtitleNeeded = false;
                        _timer.Interval = 5000;
                    }
                });
            }
    
            private bool SetSubtitleNeeded = false;
    
            partial void ClickedButton(NSObject sender)
            {
                _timer.Stop();
                var nsUrl = subtitleFileSelector.GetFile();
                if (nsUrl == null)
                    return;
    
                fileName = nsUrl.Path;
                subtitlesProvider.ReadFromFile(fileName);
                SetSubtitleNeeded = true;
                _timer.Start();
            }