Merge pull request #26350 from jjd-uk/estuary_media_align
[xbmc.git] / xbmc / windowing / osx / WinEventsOSXImpl.mm
blobe22c9219e91484b8f9691d4f482bd492b76113e4
1 /*
2  *  Copyright (C) 2012-2018 Team Kodi
3  *  This file is part of Kodi - https://kodi.tv
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *  See LICENSES/README.md for more information.
7  */
9 #include "WinEventsOSXImpl.h"
11 #include "ServiceBroker.h"
12 #include "application/AppInboundProtocol.h"
13 #include "input/actions/Action.h"
14 #include "input/actions/ActionIDs.h"
15 #include "input/mouse/MouseStat.h"
16 #include "messaging/ApplicationMessenger.h"
17 #include "utils/log.h"
19 #include <mutex>
20 #include <optional>
21 #include <queue>
23 #import <AppKit/AppKit.h>
24 #import <Foundation/Foundation.h>
26 #pragma mark - objc implementation
28 @implementation CWinEventsOSXImpl
30   std::queue<XBMC_Event> events;
31   CCriticalSection m_inputlock;
32   bool m_inputEnabled;
34   //! macOS requires the calls the NSCursor hide/unhide to be balanced
35   enum class NSCursorVisibilityBalancer
36   {
37     NONE,
38     HIDE,
39     UNHIDE
40   };
41   NSCursorVisibilityBalancer m_lastAppCursorVisibilityAction;
44 #pragma mark - init
46 - (instancetype)init
48   self = [super init];
50   m_lastAppCursorVisibilityAction = NSCursorVisibilityBalancer::NONE;
51   [self enableInputEvents];
52   return self;
55 - (void)MessagePush:(XBMC_Event*)newEvent
57   std::unique_lock<CCriticalSection> lock(m_inputlock);
58   events.emplace(*newEvent);
61 - (bool)MessagePump
64   bool ret = false;
66   // Do not always loop, only pump the initial queued count events. else if ui keep pushing
67   // events the loop won't finish then it will block xbmc main message loop.
68   for (size_t pumpEventCount = [self GetQueueSize]; pumpEventCount > 0; --pumpEventCount)
69   {
70     // Pop up only one event per time since in App::OnEvent it may init modal dialog which init
71     // deeper message loop and call the deeper MessagePump from there.
72     XBMC_Event pumpEvent = {};
73     {
74       std::unique_lock<CCriticalSection> lock(m_inputlock);
75       if (events.size() == 0)
76         return ret;
77       pumpEvent = events.front();
78       events.pop();
79     }
80     std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
81     if (appPort)
82       ret |= appPort->OnEvent(pumpEvent);
83   }
84   return ret;
87 - (size_t)GetQueueSize
89   std::unique_lock<CCriticalSection> lock(m_inputlock);
90   return events.size();
93 - (unichar)OsxKey2XbmcKey:(unichar)character
95   switch (character)
96   {
97     case NSLeftArrowFunctionKey:
98       return XBMCK_LEFT;
99     case NSRightArrowFunctionKey:
100       return XBMCK_RIGHT;
101     case NSUpArrowFunctionKey:
102       return XBMCK_UP;
103     case NSDownArrowFunctionKey:
104       return XBMCK_DOWN;
105     case NSBackspaceCharacter:
106     case NSDeleteCharacter:
107       return XBMCK_BACKSPACE;
108     case NSCarriageReturnCharacter:
109     case NSEnterCharacter:
110       return XBMCK_RETURN;
111     case NSF1FunctionKey:
112       return XBMCK_F1;
113     case NSF2FunctionKey:
114       return XBMCK_F2;
115     case NSF3FunctionKey:
116       return XBMCK_F3;
117     case NSF4FunctionKey:
118       return XBMCK_F4;
119     case NSF5FunctionKey:
120       return XBMCK_F5;
121     case NSF6FunctionKey:
122       return XBMCK_F6;
123     case NSF7FunctionKey:
124       return XBMCK_F7;
125     case NSF8FunctionKey:
126       return XBMCK_F8;
127     case NSF9FunctionKey:
128       return XBMCK_F9;
129     case NSF10FunctionKey:
130       return XBMCK_F10;
131     case NSF11FunctionKey:
132       return XBMCK_F11;
133     case NSF12FunctionKey:
134       return XBMCK_F12;
135     case NSF13FunctionKey:
136       return XBMCK_F13;
137     case NSF14FunctionKey:
138       return XBMCK_F14;
139     case NSF15FunctionKey:
140       return XBMCK_F15;
141     case NSHomeFunctionKey:
142       return XBMCK_HOME;
143     case NSEndFunctionKey:
144       return XBMCK_END;
145     case NSPageDownFunctionKey:
146       return XBMCK_PAGEDOWN;
147     case NSPageUpFunctionKey:
148       return XBMCK_PAGEUP;
149     case NSPauseFunctionKey:
150       return XBMCK_PAUSE;
151     case NSInsertCharFunctionKey:
152       return XBMCK_INSERT;
153     default:
154       return character;
155   }
158 - (XBMCMod)OsxMod2XbmcMod:(NSEventModifierFlags)appleModifier
160   unsigned int xbmcModifier = XBMCKMOD_NONE;
161   // shift
162   if (appleModifier & NSEventModifierFlagShift)
163     xbmcModifier |= XBMCKMOD_SHIFT;
164   // left ctrl
165   if (appleModifier & NSEventModifierFlagControl)
166     xbmcModifier |= XBMCKMOD_CTRL;
167   // left alt/option
168   if (appleModifier & NSEventModifierFlagOption)
169     xbmcModifier |= XBMCKMOD_ALT;
170   // left command
171   if (appleModifier & NSEventModifierFlagCommand)
172     xbmcModifier |= XBMCKMOD_LMETA;
174   return static_cast<XBMCMod>(xbmcModifier);
177 - (bool)ProcessOSXShortcuts:(XBMC_Event&)event
179   const auto cmd = (event.key.keysym.mod & (XBMCKMOD_LMETA | XBMCKMOD_RMETA)) != 0;
180   if (cmd && event.type == XBMC_KEYDOWN)
181   {
182     switch (event.key.keysym.sym)
183     {
184       case XBMCK_s: // CMD-s to take a screenshot
185       {
186         CAction* action = new CAction(ACTION_TAKE_SCREENSHOT);
187         CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
188                                                    static_cast<void*>(action));
189         return true;
190       }
192       default:
193         return false;
194     }
195   }
197   return false;
200 - (void)enableInputEvents
202   m_inputEnabled = true;
205 - (void)disableInputEvents
207   m_inputEnabled = false;
210 - (void)signalMouseEntered
212   if (m_lastAppCursorVisibilityAction != NSCursorVisibilityBalancer::HIDE)
213   {
214     m_lastAppCursorVisibilityAction = NSCursorVisibilityBalancer::HIDE;
215     [NSCursor hide];
216   }
219 - (void)signalMouseExited
221   if (m_lastAppCursorVisibilityAction != NSCursorVisibilityBalancer::UNHIDE)
222   {
223     m_lastAppCursorVisibilityAction = NSCursorVisibilityBalancer::UNHIDE;
224     [NSCursor unhide];
225   }
228 - (void)ProcessInputEvent:(NSEvent*)nsEvent
230   if (m_inputEnabled)
231   {
232     [self InputEventHandler:nsEvent];
233   }
236 - (NSEvent*)InputEventHandler:(NSEvent*)nsEvent
238   bool passEvent = true;
239   XBMC_Event newEvent = {};
241   switch (nsEvent.type)
242   {
243     // handle mouse events and transform them into the xbmc event world
244     case NSEventTypeLeftMouseUp:
245     {
246       [self SendXBMCMouseButtonEvent:nsEvent
247                            xbmcEvent:newEvent
248                       mouseEventType:XBMC_MOUSEBUTTONUP
249                           buttonCode:XBMC_BUTTON_LEFT];
250       break;
251     }
252     case NSEventTypeLeftMouseDown:
253     {
254       [self SendXBMCMouseButtonEvent:nsEvent
255                            xbmcEvent:newEvent
256                       mouseEventType:XBMC_MOUSEBUTTONDOWN
257                           buttonCode:XBMC_BUTTON_LEFT];
258       break;
259     }
260     case NSEventTypeRightMouseUp:
261     {
262       [self SendXBMCMouseButtonEvent:nsEvent
263                            xbmcEvent:newEvent
264                       mouseEventType:XBMC_MOUSEBUTTONUP
265                           buttonCode:XBMC_BUTTON_RIGHT];
266       break;
267     }
268     case NSEventTypeRightMouseDown:
269     {
270       [self SendXBMCMouseButtonEvent:nsEvent
271                            xbmcEvent:newEvent
272                       mouseEventType:XBMC_MOUSEBUTTONDOWN
273                           buttonCode:XBMC_BUTTON_RIGHT];
274       break;
275     }
276     case NSEventTypeOtherMouseUp:
277     {
278       [self SendXBMCMouseButtonEvent:nsEvent
279                            xbmcEvent:newEvent
280                       mouseEventType:XBMC_MOUSEBUTTONUP
281                           buttonCode:XBMC_BUTTON_MIDDLE];
282       break;
283     }
284     case NSEventTypeOtherMouseDown:
285     {
286       [self SendXBMCMouseButtonEvent:nsEvent
287                            xbmcEvent:newEvent
288                       mouseEventType:XBMC_MOUSEBUTTONDOWN
289                           buttonCode:XBMC_BUTTON_MIDDLE];
290       break;
291     }
292     case NSEventTypeMouseMoved:
293     case NSEventTypeLeftMouseDragged:
294     case NSEventTypeRightMouseDragged:
295     case NSEventTypeOtherMouseDragged:
296     {
297       auto location = [self TranslateMouseLocation:nsEvent];
298       if (location.has_value())
299       {
300         NSPoint locationCoordinates = location.value();
301         newEvent.type = XBMC_MOUSEMOTION;
302         newEvent.motion.x = locationCoordinates.x;
303         newEvent.motion.y = locationCoordinates.y;
304         [self MessagePush:&newEvent];
305       }
306       break;
307     }
308     case NSEventTypeScrollWheel:
309     {
310       // very strange, real scrolls have non-zero deltaY followed by same number of events
311       // with a zero deltaY. This reverses our scroll which is WTF? anoying. Trap them out here.
312       if (nsEvent.deltaY != 0.0)
313       {
314         auto button = nsEvent.scrollingDeltaY > 0 ? XBMC_BUTTON_WHEELUP : XBMC_BUTTON_WHEELDOWN;
315         if ([self SendXBMCMouseButtonEvent:nsEvent
316                                  xbmcEvent:newEvent
317                             mouseEventType:XBMC_MOUSEBUTTONDOWN
318                                 buttonCode:button])
319         {
320           // scrollwhell need a subsquent button press with no button code
321           [self SendXBMCMouseButtonEvent:nsEvent
322                                xbmcEvent:newEvent
323                           mouseEventType:XBMC_MOUSEBUTTONUP
324                               buttonCode:std::nullopt];
325         }
326       }
327       break;
328     }
330     // handle keyboard events and transform them into the xbmc event world
331     case NSEventTypeKeyUp:
332     {
333       newEvent = [self keyPressEvent:nsEvent];
334       newEvent.type = XBMC_KEYUP;
336       [self MessagePush:&newEvent];
337       passEvent = false;
338       break;
339     }
340     case NSEventTypeKeyDown:
341     {
342       newEvent = [self keyPressEvent:nsEvent];
343       newEvent.type = XBMC_KEYDOWN;
345       if (![self ProcessOSXShortcuts:newEvent])
346         [self MessagePush:&newEvent];
347       passEvent = false;
349       break;
350     }
351     default:
352       return nsEvent;
353   }
354   // We must return the event for it to be useful if not already handled
355   if (passEvent)
356     return nsEvent;
357   else
358     return nullptr;
361 - (XBMC_Event)keyPressEvent:(NSEvent*)nsEvent
363   XBMC_Event newEvent = {};
365   // use characters to propagate the actual unicode character
366   NSString* unicode = nsEvent.characters;
367   // use charactersIgnoringModifiers to get the corresponding char without modifiers. This will
368   // keep shift so, lower case it as kodi shortcuts might depend on it. modifiers are propagated
369   // anyway in keysym.mod
370   NSString* unicodeWithoutModifiers = [nsEvent.charactersIgnoringModifiers lowercaseString];
372   // May be possible for actualStringLength > 1. Havent been able to replicate anything
373   // larger than 1, but keep in mind for any regressions
374   if (!unicode || unicode.length == 0 || !unicodeWithoutModifiers ||
375       unicodeWithoutModifiers.length == 0)
376   {
377     return newEvent;
378   }
379   else if (unicode.length > 1)
380   {
381     CLog::Log(LOGERROR, "CWinEventsOSXImpl::keyPressEvent - event string > 1 - size: {}",
382               unicode.length);
383     return newEvent;
384   }
386   newEvent.key.keysym.scancode = nsEvent.keyCode;
387   newEvent.key.keysym.sym =
388       static_cast<XBMCKey>([self OsxKey2XbmcKey:[unicodeWithoutModifiers characterAtIndex:0]]);
389   newEvent.key.keysym.unicode = [unicode characterAtIndex:0];
390   newEvent.key.keysym.mod = [self OsxMod2XbmcMod:nsEvent.modifierFlags];
392   return newEvent;
395 - (BOOL)SendXBMCMouseButtonEvent:(NSEvent*)nsEvent
396                        xbmcEvent:(XBMC_Event&)xbmcEvent
397                   mouseEventType:(uint8_t)mouseEventType
398                       buttonCode:(std::optional<uint8_t>)buttonCode
400   auto location = [self TranslateMouseLocation:nsEvent];
401   if (location.has_value())
402   {
403     NSPoint locationCoordinates = location.value();
404     xbmcEvent.type = mouseEventType;
405     if (buttonCode.has_value())
406     {
407       xbmcEvent.button.button = buttonCode.value();
408     }
409     xbmcEvent.button.x = locationCoordinates.x;
410     xbmcEvent.button.y = locationCoordinates.y;
411     [self MessagePush:&xbmcEvent];
412     return true;
413   }
414   return false;
417 - (std::optional<NSPoint>)TranslateMouseLocation:(NSEvent*)nsEvent
419   NSPoint location = nsEvent.locationInWindow;
420   // ignore events if outside the view bounds
421   if (!nsEvent.window || !NSPointInRect(location, nsEvent.window.contentView.frame))
422   {
423     return std::nullopt;
424   }
425   // translate the location to backing units
426   location = [nsEvent.window convertPointToBacking:location];
427   NSRect frame = [nsEvent.window convertRectToBacking:nsEvent.window.contentView.frame];
428   // cocoa world is upside down ...
429   location.y = frame.size.height - location.y;
430   return location;
433 @end