2 * Copyright (C) 2012-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
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"
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;
34 //! macOS requires the calls the NSCursor hide/unhide to be balanced
35 enum class NSCursorVisibilityBalancer
41 NSCursorVisibilityBalancer m_lastAppCursorVisibilityAction;
50 m_lastAppCursorVisibilityAction = NSCursorVisibilityBalancer::NONE;
51 [self enableInputEvents];
55 - (void)MessagePush:(XBMC_Event*)newEvent
57 std::unique_lock<CCriticalSection> lock(m_inputlock);
58 events.emplace(*newEvent);
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)
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 = {};
74 std::unique_lock<CCriticalSection> lock(m_inputlock);
75 if (events.size() == 0)
77 pumpEvent = events.front();
80 std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
82 ret |= appPort->OnEvent(pumpEvent);
87 - (size_t)GetQueueSize
89 std::unique_lock<CCriticalSection> lock(m_inputlock);
93 - (unichar)OsxKey2XbmcKey:(unichar)character
97 case NSLeftArrowFunctionKey:
99 case NSRightArrowFunctionKey:
101 case NSUpArrowFunctionKey:
103 case NSDownArrowFunctionKey:
105 case NSBackspaceCharacter:
106 case NSDeleteCharacter:
107 return XBMCK_BACKSPACE;
108 case NSCarriageReturnCharacter:
109 case NSEnterCharacter:
111 case NSF1FunctionKey:
113 case NSF2FunctionKey:
115 case NSF3FunctionKey:
117 case NSF4FunctionKey:
119 case NSF5FunctionKey:
121 case NSF6FunctionKey:
123 case NSF7FunctionKey:
125 case NSF8FunctionKey:
127 case NSF9FunctionKey:
129 case NSF10FunctionKey:
131 case NSF11FunctionKey:
133 case NSF12FunctionKey:
135 case NSF13FunctionKey:
137 case NSF14FunctionKey:
139 case NSF15FunctionKey:
141 case NSHomeFunctionKey:
143 case NSEndFunctionKey:
145 case NSPageDownFunctionKey:
146 return XBMCK_PAGEDOWN;
147 case NSPageUpFunctionKey:
149 case NSPauseFunctionKey:
151 case NSInsertCharFunctionKey:
158 - (XBMCMod)OsxMod2XbmcMod:(NSEventModifierFlags)appleModifier
160 unsigned int xbmcModifier = XBMCKMOD_NONE;
162 if (appleModifier & NSEventModifierFlagShift)
163 xbmcModifier |= XBMCKMOD_SHIFT;
165 if (appleModifier & NSEventModifierFlagControl)
166 xbmcModifier |= XBMCKMOD_CTRL;
168 if (appleModifier & NSEventModifierFlagOption)
169 xbmcModifier |= XBMCKMOD_ALT;
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)
182 switch (event.key.keysym.sym)
184 case XBMCK_s: // CMD-s to take a screenshot
186 CAction* action = new CAction(ACTION_TAKE_SCREENSHOT);
187 CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
188 static_cast<void*>(action));
200 - (void)enableInputEvents
202 m_inputEnabled = true;
205 - (void)disableInputEvents
207 m_inputEnabled = false;
210 - (void)signalMouseEntered
212 if (m_lastAppCursorVisibilityAction != NSCursorVisibilityBalancer::HIDE)
214 m_lastAppCursorVisibilityAction = NSCursorVisibilityBalancer::HIDE;
219 - (void)signalMouseExited
221 if (m_lastAppCursorVisibilityAction != NSCursorVisibilityBalancer::UNHIDE)
223 m_lastAppCursorVisibilityAction = NSCursorVisibilityBalancer::UNHIDE;
228 - (void)ProcessInputEvent:(NSEvent*)nsEvent
232 [self InputEventHandler:nsEvent];
236 - (NSEvent*)InputEventHandler:(NSEvent*)nsEvent
238 bool passEvent = true;
239 XBMC_Event newEvent = {};
241 switch (nsEvent.type)
243 // handle mouse events and transform them into the xbmc event world
244 case NSEventTypeLeftMouseUp:
246 [self SendXBMCMouseButtonEvent:nsEvent
248 mouseEventType:XBMC_MOUSEBUTTONUP
249 buttonCode:XBMC_BUTTON_LEFT];
252 case NSEventTypeLeftMouseDown:
254 [self SendXBMCMouseButtonEvent:nsEvent
256 mouseEventType:XBMC_MOUSEBUTTONDOWN
257 buttonCode:XBMC_BUTTON_LEFT];
260 case NSEventTypeRightMouseUp:
262 [self SendXBMCMouseButtonEvent:nsEvent
264 mouseEventType:XBMC_MOUSEBUTTONUP
265 buttonCode:XBMC_BUTTON_RIGHT];
268 case NSEventTypeRightMouseDown:
270 [self SendXBMCMouseButtonEvent:nsEvent
272 mouseEventType:XBMC_MOUSEBUTTONDOWN
273 buttonCode:XBMC_BUTTON_RIGHT];
276 case NSEventTypeOtherMouseUp:
278 [self SendXBMCMouseButtonEvent:nsEvent
280 mouseEventType:XBMC_MOUSEBUTTONUP
281 buttonCode:XBMC_BUTTON_MIDDLE];
284 case NSEventTypeOtherMouseDown:
286 [self SendXBMCMouseButtonEvent:nsEvent
288 mouseEventType:XBMC_MOUSEBUTTONDOWN
289 buttonCode:XBMC_BUTTON_MIDDLE];
292 case NSEventTypeMouseMoved:
293 case NSEventTypeLeftMouseDragged:
294 case NSEventTypeRightMouseDragged:
295 case NSEventTypeOtherMouseDragged:
297 auto location = [self TranslateMouseLocation:nsEvent];
298 if (location.has_value())
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];
308 case NSEventTypeScrollWheel:
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)
314 auto button = nsEvent.scrollingDeltaY > 0 ? XBMC_BUTTON_WHEELUP : XBMC_BUTTON_WHEELDOWN;
315 if ([self SendXBMCMouseButtonEvent:nsEvent
317 mouseEventType:XBMC_MOUSEBUTTONDOWN
320 // scrollwhell need a subsquent button press with no button code
321 [self SendXBMCMouseButtonEvent:nsEvent
323 mouseEventType:XBMC_MOUSEBUTTONUP
324 buttonCode:std::nullopt];
330 // handle keyboard events and transform them into the xbmc event world
331 case NSEventTypeKeyUp:
333 newEvent = [self keyPressEvent:nsEvent];
334 newEvent.type = XBMC_KEYUP;
336 [self MessagePush:&newEvent];
340 case NSEventTypeKeyDown:
342 newEvent = [self keyPressEvent:nsEvent];
343 newEvent.type = XBMC_KEYDOWN;
345 if (![self ProcessOSXShortcuts:newEvent])
346 [self MessagePush:&newEvent];
354 // We must return the event for it to be useful if not already handled
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)
379 else if (unicode.length > 1)
381 CLog::Log(LOGERROR, "CWinEventsOSXImpl::keyPressEvent - event string > 1 - size: {}",
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];
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())
403 NSPoint locationCoordinates = location.value();
404 xbmcEvent.type = mouseEventType;
405 if (buttonCode.has_value())
407 xbmcEvent.button.button = buttonCode.value();
409 xbmcEvent.button.x = locationCoordinates.x;
410 xbmcEvent.button.y = locationCoordinates.y;
411 [self MessagePush:&xbmcEvent];
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))
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;