Codechange: be consistent in naming the paint function Paint()
[openttd-github.git] / src / video / cocoa / cocoa_wnd.mm
blob78e942c92f9953098b59d41679b14648a160fc0b
1 /*
2  * This file is part of OpenTTD.
3  * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4  * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5  * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6  */
8 /******************************************************************************
9  *                             Cocoa video driver                             *
10  * Known things left to do:                                                   *
11  *  List available resolutions.                                               *
12  ******************************************************************************/
14 #ifdef WITH_COCOA
16 #include "../../stdafx.h"
17 #include "../../os/macosx/macos.h"
19 #define Rect  OTTDRect
20 #define Point OTTDPoint
21 #import <Cocoa/Cocoa.h>
22 #undef Rect
23 #undef Point
25 #include "../../openttd.h"
26 #include "../../debug.h"
27 #include "../../rev.h"
28 #include "cocoa_v.h"
29 #include "cocoa_wnd.h"
30 #include "../../settings_type.h"
31 #include "../../string_func.h"
32 #include "../../gfx_func.h"
33 #include "../../window_func.h"
34 #include "../../window_gui.h"
37 /* Table data for key mapping. */
38 #include "cocoa_keys.h"
41 /**
42  * Important notice regarding all modifications!!!!!!!
43  * There are certain limitations because the file is objective C++.
44  * gdb has limitations.
45  * C++ and objective C code can't be joined in all cases (classes stuff).
46  * Read http://developer.apple.com/releasenotes/Cocoa/Objective-C++.html for more information.
47  */
49 bool _allow_hidpi_window = true; // Referenced from table/misc_settings.ini
51 @interface OTTDMain : NSObject <NSApplicationDelegate>
52 @end
54 NSString *OTTDMainLaunchGameEngine = @"ottdmain_launch_game_engine";
56 bool _tab_is_down;
58 static bool _cocoa_video_dialog = false;
59 static OTTDMain *_ottd_main;
62 /**
63  * Count the number of UTF-16 code points in a range of an UTF-8 string.
64  * @param from Start of the range.
65  * @param to End of the range.
66  * @return Number of UTF-16 code points in the range.
67  */
68 static NSUInteger CountUtf16Units(const char *from, const char *to)
70         NSUInteger i = 0;
72         while (from < to) {
73                 WChar c;
74                 size_t len = Utf8Decode(&c, from);
75                 i += len < 4 ? 1 : 2; // Watch for surrogate pairs.
76                 from += len;
77         }
79         return i;
82 /**
83  * Advance an UTF-8 string by a number of equivalent UTF-16 code points.
84  * @param str UTF-8 string.
85  * @param count Number of UTF-16 code points to advance the string by.
86  * @return Advanced string pointer.
87  */
88 static const char *Utf8AdvanceByUtf16Units(const char *str, NSUInteger count)
90         for (NSUInteger i = 0; i < count && *str != '\0'; ) {
91                 WChar c;
92                 size_t len = Utf8Decode(&c, str);
93                 i += len < 4 ? 1 : 2; // Watch for surrogates.
94                 str += len;
95         }
97         return str;
101  * Convert a NSString to an UTF-32 encoded string.
102  * @param s String to convert.
103  * @return Vector of UTF-32 characters.
104  */
105 static std::vector<WChar> NSStringToUTF32(NSString *s)
107         std::vector<WChar> unicode_str;
109         unichar lead = 0;
110         for (NSUInteger i = 0; i < s.length; i++) {
111                 unichar c = [ s characterAtIndex:i ];
112                 if (Utf16IsLeadSurrogate(c)) {
113                         lead = c;
114                         continue;
115                 } else if (Utf16IsTrailSurrogate(c)) {
116                         if (lead != 0) unicode_str.push_back(Utf16DecodeSurrogate(lead, c));
117                 } else {
118                         unicode_str.push_back(c);
119                 }
120         }
122         return unicode_str;
127  * The main class of the application, the application's delegate.
128  */
129 @implementation OTTDMain
131  * Stop the game engine. Must be called on main thread.
132  */
133 - (void)stopEngine
135         [ NSApp stop:self ];
137         /* Send an empty event to return from the run loop. Without that, application is stuck waiting for an event. */
138         NSEvent *event = [ NSEvent otherEventWithType:NSApplicationDefined location:NSMakePoint(0, 0) modifierFlags:0 timestamp:0.0 windowNumber:0 context:nil subtype:0 data1:0 data2:0 ];
139         [ NSApp postEvent:event atStart:YES ];
143  * Start the game loop.
144  */
145 - (void)launchGameEngine: (NSNotification*) note
147         auto *drv = static_cast<VideoDriver_Cocoa *>(VideoDriver::GetInstance());
149         /* Setup cursor for the current _game_mode. */
150         NSEvent *e = [ [ NSEvent alloc ] init ];
151         [ drv->cocoaview cursorUpdate:e ];
152         [ e release ];
154         /* Hand off to main application code. */
155         drv->GameLoop();
157         /* We are done, thank you for playing. */
158         [ self performSelectorOnMainThread:@selector(stopEngine) withObject:nil waitUntilDone:FALSE ];
162  * Called when the internal event loop has just started running.
163  */
164 - (void) applicationDidFinishLaunching: (NSNotification*) note
166         /* Add a notification observer so we can restart the game loop later on if necessary. */
167         [ [ NSNotificationCenter defaultCenter ] addObserver:self selector:@selector(launchGameEngine:) name:OTTDMainLaunchGameEngine object:nil ];
169         /* Start game loop. */
170         [ [ NSNotificationCenter defaultCenter ] postNotificationName:OTTDMainLaunchGameEngine object:nil ];
174  * Display the in game quit confirmation dialog.
175  */
176 - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*) sender
178         HandleExitGameRequest();
180         return NSTerminateCancel;
184  * Remove ourself as a notification observer.
185  */
186 - (void)unregisterObserver
188         [ [ NSNotificationCenter defaultCenter ] removeObserver:self ];
190 @end
193  * Initialize the application menu shown in top bar.
194  */
195 static void setApplicationMenu()
197         NSString *appName = @"OpenTTD";
198         NSMenu *appleMenu = [ [ NSMenu alloc ] initWithTitle:appName ];
200         /* Add menu items */
201         NSString *title = [ @"About " stringByAppendingString:appName ];
202         [ appleMenu addItemWithTitle:title action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@"" ];
204         [ appleMenu addItem:[ NSMenuItem separatorItem ] ];
206         title = [ @"Hide " stringByAppendingString:appName ];
207         [ appleMenu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@"h" ];
209         NSMenuItem *menuItem = [ appleMenu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h" ];
210         [ menuItem setKeyEquivalentModifierMask:(NSAlternateKeyMask | NSCommandKeyMask) ];
212         [ appleMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@"" ];
214         [ appleMenu addItem:[ NSMenuItem separatorItem ] ];
216         title = [ @"Quit " stringByAppendingString:appName ];
217         [ appleMenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q" ];
219         /* Put menu into the menubar */
220         menuItem = [ [ NSMenuItem alloc ] initWithTitle:@"" action:nil keyEquivalent:@"" ];
221         [ menuItem setSubmenu:appleMenu ];
222         [ [ NSApp mainMenu ] addItem:menuItem ];
224         /* Tell the application object that this is now the application menu.
225          * This interesting Objective-C construct is used because not all SDK
226          * versions define this method publicly. */
227         if ([ NSApp respondsToSelector:@selector(setAppleMenu:) ]) {
228                 [ NSApp performSelector:@selector(setAppleMenu:) withObject:appleMenu ];
229         }
231         /* Finally give up our references to the objects */
232         [ appleMenu release ];
233         [ menuItem release ];
237  * Create a window menu.
238  */
239 static void setupWindowMenu()
241         NSMenu *windowMenu = [ [ NSMenu alloc ] initWithTitle:@"Window" ];
243         /* "Minimize" item */
244         [ windowMenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m" ];
246         /* Put menu into the menubar */
247         NSMenuItem *menuItem = [ [ NSMenuItem alloc ] initWithTitle:@"Window" action:nil keyEquivalent:@"" ];
248         [ menuItem setSubmenu:windowMenu ];
249         [ [ NSApp mainMenu ] addItem:menuItem ];
251         if (MacOSVersionIsAtLeast(10, 7, 0)) {
252                 /* The OS will change the name of this menu item automatically */
253                 [ windowMenu addItemWithTitle:@"Fullscreen" action:@selector(toggleFullScreen:) keyEquivalent:@"^f" ];
254         }
256         /* Tell the application object that this is now the window menu */
257         [ NSApp setWindowsMenu:windowMenu ];
259         /* Finally give up our references to the objects */
260         [ windowMenu release ];
261         [ menuItem release ];
265  * Startup the application.
266  */
267 bool CocoaSetupApplication()
269         ProcessSerialNumber psn = { 0, kCurrentProcess };
271         /* Ensure the application object is initialised */
272         [ NSApplication sharedApplication ];
274         /* Tell the dock about us */
275         OSStatus returnCode = TransformProcessType(&psn, kProcessTransformToForegroundApplication);
276         if (returnCode != 0) DEBUG(driver, 0, "Could not change to foreground application. Error %d", (int)returnCode);
278         /* Disable the system-wide tab feature as we only have one window. */
279         if ([ NSWindow respondsToSelector:@selector(setAllowsAutomaticWindowTabbing:) ]) {
280                 /* We use nil instead of NO as withObject requires an id. */
281                 [ NSWindow performSelector:@selector(setAllowsAutomaticWindowTabbing:) withObject:nil];
282         }
284         /* Become the front process, important when start from the command line. */
285         [ [ NSApplication sharedApplication ] setActivationPolicy:NSApplicationActivationPolicyRegular ];
286         [ [ NSApplication sharedApplication ] activateIgnoringOtherApps:YES ];
288         /* Set up the menubar */
289         [ NSApp setMainMenu:[ [ NSMenu alloc ] init ] ];
290         setApplicationMenu();
291         setupWindowMenu();
293         /* Create OTTDMain and make it the app delegate */
294         _ottd_main = [ [ OTTDMain alloc ] init ];
295         [ NSApp setDelegate:_ottd_main ];
297         return true;
301  * Deregister app delegate.
302  */
303 void CocoaExitApplication()
305         [ _ottd_main unregisterObserver ];
306         [ _ottd_main release ];
310  * Catch asserts prior to initialization of the videodriver.
312  * @param title Window title.
313  * @param message Message text.
314  * @param buttonLabel Button text.
316  * @note This is needed since sometimes assert is called before the videodriver is initialized .
317  */
318 void CocoaDialog(const char *title, const char *message, const char *buttonLabel)
320         _cocoa_video_dialog = true;
322         bool wasstarted = _cocoa_video_started;
323         if (VideoDriver::GetInstance() == nullptr) {
324                 CocoaSetupApplication(); // Setup application before showing dialog
325         } else if (!_cocoa_video_started && VideoDriver::GetInstance()->Start({}) != nullptr) {
326                 fprintf(stderr, "%s: %s\n", title, message);
327                 return;
328         }
330         @autoreleasepool {
331                 NSAlert *alert = [ [ NSAlert alloc ] init ];
332                 [ alert setAlertStyle: NSCriticalAlertStyle ];
333                 [ alert setMessageText:[ NSString stringWithUTF8String:title ] ];
334                 [ alert setInformativeText:[ NSString stringWithUTF8String:message ] ];
335                 [ alert addButtonWithTitle: [ NSString stringWithUTF8String:buttonLabel ] ];
336                 [ alert runModal ];
337                 [ alert release ];
338         }
340         if (!wasstarted && VideoDriver::GetInstance() != nullptr) VideoDriver::GetInstance()->Stop();
342         _cocoa_video_dialog = false;
347  * Re-implement the system cursor in order to allow hiding and showing it nicely
348  */
349 @implementation NSCursor (OTTD_CocoaCursor)
350 + (NSCursor *) clearCocoaCursor
352         /* RAW 16x16 transparent GIF */
353         unsigned char clearGIFBytes[] = {
354                 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x10, 0x00, 0x10, 0x00, 0x80, 0x00,
355                 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, 0x01, 0x00,
356                 0x00, 0x01, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00,
357                 0x00, 0x02, 0x0E, 0x8C, 0x8F, 0xA9, 0xCB, 0xED, 0x0F, 0xA3, 0x9C, 0xB4,
358                 0xDA, 0x8B, 0xB3, 0x3E, 0x05, 0x00, 0x3B};
359         NSData *clearGIFData = [ NSData dataWithBytesNoCopy:&clearGIFBytes[0] length:55 freeWhenDone:NO ];
360         NSImage *clearImg = [ [ NSImage alloc ] initWithData:clearGIFData ];
361         return [ [ NSCursor alloc ] initWithImage:clearImg hotSpot:NSMakePoint(0.0,0.0) ];
363 @end
365 @implementation OTTD_CocoaWindow {
366         VideoDriver_Cocoa *driver;
370  * Initialize event system for the application rectangle
371  */
372 - (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)styleMask backing:(NSBackingStoreType)backingType defer:(BOOL)flag driver:(VideoDriver_Cocoa *)drv
374         if (self = [ super initWithContentRect:contentRect styleMask:styleMask backing:backingType defer:flag ]) {
375                 self->driver = drv;
377                 [ self setContentMinSize:NSMakeSize(64.0f, 64.0f) ];
379                 std::string caption = std::string{"OpenTTD "} + _openttd_revision;
380                 NSString *nsscaption = [ [ NSString alloc ] initWithUTF8String:caption.c_str() ];
381                 [ self setTitle:nsscaption ];
382                 [ self setMiniwindowTitle:nsscaption ];
383                 [ nsscaption release ];
384         }
386         return self;
390  * This method fires just before the window deminaturizes from the Dock.
391  * We'll save the current visible surface, let the window manager redraw any
392  * UI elements, and restore the surface. This way, no expose event
393  * is required, and the deminiaturize works perfectly.
394  */
395 - (void)display
397         /* save current visible surface */
398         [ self cacheImageInRect:[ driver->cocoaview frame ] ];
400         /* let the window manager redraw controls, border, etc */
401         [ super display ];
403         /* restore visible surface */
404         [ self restoreCachedImage ];
407  * Define the rectangle we draw our window in
408  */
409 - (void)setFrame:(NSRect)frameRect display:(BOOL)flag
411         [ super setFrame:frameRect display:flag ];
413         driver->AllocateBackingStore();
416 @end
418 @implementation OTTD_CocoaView {
419         float _current_magnification;
420         NSUInteger _current_mods;
421         bool _emulated_down;
422         bool _use_hidpi; ///< Render content in native resolution?
425 - (instancetype)initWithFrame:(NSRect)frameRect
427         if (self = [ super initWithFrame:frameRect ]) {
428                 self->_use_hidpi = _allow_hidpi_window && [ self respondsToSelector:@selector(convertRectToBacking:) ] && [ self respondsToSelector:@selector(convertRectFromBacking:) ];
429         }
430         return self;
433 - (NSRect)getRealRect:(NSRect)rect
435         return _use_hidpi ? [ self convertRectToBacking:rect ] : rect;
438 - (NSRect)getVirtualRect:(NSRect)rect
440         return _use_hidpi ? [ self convertRectFromBacking:rect ] : rect;
443 - (CGFloat)getContentsScale
445         return _use_hidpi && self.window != nil ? [ self.window backingScaleFactor ] : 1.0f;
449  * Allow to handle events
450  */
451 - (BOOL)acceptsFirstResponder
453         return YES;
456 - (void)setNeedsDisplayInRect:(NSRect)invalidRect
458         /* Drawing is handled by our sub-views. Just pass it along. */
459         for ( NSView *v in [ self subviews ]) {
460                 [ v setNeedsDisplayInRect:[ v convertRect:invalidRect fromView:self ] ];
461         }
464 /** Update mouse cursor to use for this view. */
465 - (void)cursorUpdate:(NSEvent *)event
467         [ (_game_mode == GM_BOOTSTRAP ? [ NSCursor arrowCursor ] : [ NSCursor clearCocoaCursor ]) set ];
470 - (void)viewWillMoveToWindow:(NSWindow *)win
472         for (NSTrackingArea *a in [ self trackingAreas ]) {
473                 [ self removeTrackingArea:a ];
474         }
477 - (void)viewDidMoveToWindow
479         /* Install mouse tracking area. */
480         NSTrackingAreaOptions track_opt = NSTrackingInVisibleRect | NSTrackingActiveInActiveApp | NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingCursorUpdate;
481         NSTrackingArea *track = [ [ NSTrackingArea alloc ] initWithRect:[ self bounds ] options:track_opt owner:self userInfo:nil ];
482         [ self addTrackingArea:track ];
483         [ track release ];
486  * Make OpenTTD aware that it has control over the mouse
487  */
488 - (void)mouseEntered:(NSEvent *)theEvent
490         _cursor.in_window = true;
493  * Make OpenTTD aware that it has NOT control over the mouse
494  */
495 - (void)mouseExited:(NSEvent *)theEvent
497         if ([ self window ] != nil) UndrawMouseCursor();
498         _cursor.in_window = false;
502  * Return the mouse location
503  * @param event UI event
504  * @return mouse location as NSPoint
505  */
506 - (NSPoint)mousePositionFromEvent:(NSEvent *)e
508         NSPoint pt = e.locationInWindow;
509         if ([ e window ] == nil) pt = [ self.window convertRectFromScreen:NSMakeRect(pt.x, pt.y, 0, 0) ].origin;
510         pt = [ self convertPoint:pt fromView:nil ];
512         return [ self getRealRect:NSMakeRect(pt.x, self.bounds.size.height - pt.y, 0, 0) ].origin;
515 - (void)internalMouseMoveEvent:(NSEvent *)event
517         if (_cursor.fix_at) {
518                 _cursor.UpdateCursorPositionRelative(event.deltaX * self.getContentsScale, event.deltaY * self.getContentsScale);
519         } else {
520                 NSPoint pt = [ self mousePositionFromEvent:event ];
521                 _cursor.UpdateCursorPosition(pt.x, pt.y, false);
522         }
524         HandleMouseEvents();
527 - (void)internalMouseButtonEvent
529         bool cur_fix = _cursor.fix_at;
530         HandleMouseEvents();
532         /* Cursor fix mode was changed, synchronize with OS. */
533         if (cur_fix != _cursor.fix_at) CGAssociateMouseAndMouseCursorPosition(!_cursor.fix_at);
536 - (BOOL)emulateRightButton:(NSEvent *)event
538         uint32 keymask = 0;
539         if (_settings_client.gui.right_mouse_btn_emulation == RMBE_COMMAND) keymask |= NSCommandKeyMask;
540         if (_settings_client.gui.right_mouse_btn_emulation == RMBE_CONTROL) keymask |= NSControlKeyMask;
542         return (event.modifierFlags & keymask) != 0;
545 - (void)mouseMoved:(NSEvent *)event
547         [ self internalMouseMoveEvent:event ];
550 - (void)mouseDragged:(NSEvent *)event
552         [ self internalMouseMoveEvent:event ];
554 - (void)mouseDown:(NSEvent *)event
556         if ([ self emulateRightButton:event ]) {
557                 self->_emulated_down = true;
558                 [ self rightMouseDown:event ];
559         } else {
560                 _left_button_down = true;
561                 [ self internalMouseButtonEvent ];
562         }
564 - (void)mouseUp:(NSEvent *)event
566         if (self->_emulated_down) {
567                 self->_emulated_down = false;
568                 [ self rightMouseUp:event ];
569         } else {
570                 _left_button_down = false;
571                 _left_button_clicked = false;
572                 [ self internalMouseButtonEvent ];
573         }
576 - (void)rightMouseDragged:(NSEvent *)event
578         [ self internalMouseMoveEvent:event ];
580 - (void)rightMouseDown:(NSEvent *)event
582         _right_button_down = true;
583         _right_button_clicked = true;
584         [ self internalMouseButtonEvent ];
586 - (void)rightMouseUp:(NSEvent *)event
588         _right_button_down = false;
589         [ self internalMouseButtonEvent ];
592 - (void)scrollWheel:(NSEvent *)event
594         if ([ event deltaY ] > 0.0) { /* Scroll up */
595                 _cursor.wheel--;
596         } else if ([ event deltaY ] < 0.0) { /* Scroll down */
597                 _cursor.wheel++;
598         } /* else: deltaY was 0.0 and we don't want to do anything */
600         /* Update the scroll count for 2D scrolling */
601         CGFloat deltaX;
602         CGFloat deltaY;
604         /* Use precise scrolling-specific deltas if they're supported. */
605         if ([ event respondsToSelector:@selector(hasPreciseScrollingDeltas) ]) {
606                 /* No precise deltas indicates a scroll wheel is being used, so we don't want 2D scrolling. */
607                 if (![ event hasPreciseScrollingDeltas ]) return;
609                 deltaX = [ event scrollingDeltaX ] * 0.5f;
610                 deltaY = [ event scrollingDeltaY ] * 0.5f;
611         } else {
612                 deltaX = [ event deltaX ] * 5;
613                 deltaY = [ event deltaY ] * 5;
614         }
616         _cursor.h_wheel -= (int)(deltaX * _settings_client.gui.scrollwheel_multiplier);
617         _cursor.v_wheel -= (int)(deltaY * _settings_client.gui.scrollwheel_multiplier);
620 - (void)magnifyWithEvent:(NSEvent *)event
622         /* Pinch open or close gesture. */
623         self->_current_magnification += [ event magnification ] * 5.0f;
625         while (self->_current_magnification >= 1.0f) {
626                 self->_current_magnification -= 1.0f;
627                 _cursor.wheel--;
628                 HandleMouseEvents();
629         }
630         while (self->_current_magnification <= -1.0f) {
631                 self->_current_magnification += 1.0f;
632                 _cursor.wheel++;
633                 HandleMouseEvents();
634         }
637 - (void)endGestureWithEvent:(NSEvent *)event
639         /* Gesture ended. */
640         self->_current_magnification = 0.0f;
644 - (BOOL)internalHandleKeycode:(unsigned short)keycode unicode:(WChar)unicode pressed:(BOOL)down modifiers:(NSUInteger)modifiers
646         switch (keycode) {
647                 case QZ_UP:    SB(_dirkeys, 1, 1, down); break;
648                 case QZ_DOWN:  SB(_dirkeys, 3, 1, down); break;
649                 case QZ_LEFT:  SB(_dirkeys, 0, 1, down); break;
650                 case QZ_RIGHT: SB(_dirkeys, 2, 1, down); break;
652                 case QZ_TAB: _tab_is_down = down; break;
654                 case QZ_RETURN:
655                 case QZ_f:
656                         if (down && (modifiers & NSCommandKeyMask)) {
657                                 VideoDriver::GetInstance()->ToggleFullscreen(!_fullscreen);
658                         }
659                         break;
661                 case QZ_v:
662                         if (down && EditBoxInGlobalFocus() && (modifiers & (NSCommandKeyMask | NSControlKeyMask))) {
663                                 HandleKeypress(WKC_CTRL | 'V', unicode);
664                         }
665                         break;
666                 case QZ_u:
667                         if (down && EditBoxInGlobalFocus() && (modifiers & (NSCommandKeyMask | NSControlKeyMask))) {
668                                 HandleKeypress(WKC_CTRL | 'U', unicode);
669                         }
670                         break;
671         }
673         BOOL interpret_keys = YES;
674         if (down) {
675                 /* Map keycode to OTTD code. */
676                 auto vk = std::find_if(std::begin(_vk_mapping), std::end(_vk_mapping), [=](const CocoaVkMapping &m) { return m.vk_from == keycode; });
677                 uint32 pressed_key = vk != std::end(_vk_mapping) ? vk->map_to : 0;
679                 if (modifiers & NSShiftKeyMask)     pressed_key |= WKC_SHIFT;
680                 if (modifiers & NSControlKeyMask)   pressed_key |= (_settings_client.gui.right_mouse_btn_emulation != RMBE_CONTROL ? WKC_CTRL : WKC_META);
681                 if (modifiers & NSAlternateKeyMask) pressed_key |= WKC_ALT;
682                 if (modifiers & NSCommandKeyMask)   pressed_key |= (_settings_client.gui.right_mouse_btn_emulation != RMBE_CONTROL ? WKC_META : WKC_CTRL);
684                 static bool console = false;
686                 /* The second backquote may have a character, which we don't want to interpret. */
687                 if (pressed_key == WKC_BACKQUOTE && (console || unicode == 0)) {
688                         if (!console) {
689                                 /* Backquote is a dead key, require a double press for hotkey behaviour (i.e. console). */
690                                 console = true;
691                                 return YES;
692                         } else {
693                                 /* Second backquote, don't interpret as text input. */
694                                 interpret_keys = NO;
695                         }
696                 }
697                 console = false;
699                 /* Don't handle normal characters if an edit box has the focus. */
700                 if (!EditBoxInGlobalFocus() || IsInsideMM(pressed_key & ~WKC_SPECIAL_KEYS, WKC_F1, WKC_PAUSE + 1)) {
701                         HandleKeypress(pressed_key, unicode);
702                 }
703                 DEBUG(driver, 2, "cocoa_v: QZ_KeyEvent: %x (%x), down, mapping: %x", keycode, unicode, pressed_key);
704         } else {
705                 DEBUG(driver, 2, "cocoa_v: QZ_KeyEvent: %x (%x), up", keycode, unicode);
706         }
708         return interpret_keys;
711 - (void)keyDown:(NSEvent *)event
713         /* Quit, hide and minimize */
714         switch (event.keyCode) {
715                 case QZ_q:
716                 case QZ_h:
717                 case QZ_m:
718                         if (event.modifierFlags & NSCommandKeyMask) {
719                                 [ self interpretKeyEvents:[ NSArray arrayWithObject:event ] ];
720                         }
721                         break;
722         }
724         /* Convert UTF-16 characters to UCS-4 chars. */
725         std::vector<WChar> unicode_str = NSStringToUTF32([ event characters ]);
726         if (unicode_str.empty()) unicode_str.push_back(0);
728         if (EditBoxInGlobalFocus()) {
729                 if ([ self internalHandleKeycode:event.keyCode unicode:unicode_str[0] pressed:YES modifiers:event.modifierFlags ]) {
730                         [ self interpretKeyEvents:[ NSArray arrayWithObject:event ] ];
731                 }
732         } else {
733                 [ self internalHandleKeycode:event.keyCode unicode:unicode_str[0] pressed:YES modifiers:event.modifierFlags ];
734                 for (size_t i = 1; i < unicode_str.size(); i++) {
735                         [ self internalHandleKeycode:0 unicode:unicode_str[i] pressed:YES modifiers:event.modifierFlags ];
736                 }
737         }
740 - (void)keyUp:(NSEvent *)event
742         /* Quit, hide and minimize */
743         switch (event.keyCode) {
744                 case QZ_q:
745                 case QZ_h:
746                 case QZ_m:
747                         if (event.modifierFlags & NSCommandKeyMask) {
748                                 [ self interpretKeyEvents:[ NSArray arrayWithObject:event ] ];
749                         }
750                         break;
751         }
753         /* Convert UTF-16 characters to UCS-4 chars. */
754         std::vector<WChar> unicode_str = NSStringToUTF32([ event characters ]);
755         if (unicode_str.empty()) unicode_str.push_back(0);
757         [ self internalHandleKeycode:event.keyCode unicode:unicode_str[0] pressed:NO modifiers:event.modifierFlags ];
760 - (void)flagsChanged:(NSEvent *)event
762         const int mapping[] = { QZ_CAPSLOCK, QZ_LSHIFT, QZ_LCTRL, QZ_LALT, QZ_LMETA };
764         NSUInteger newMods = event.modifierFlags;
765         if (self->_current_mods == newMods) return;
767         /* Iterate through the bits, testing each against the current modifiers */
768         for (unsigned int i = 0, bit = NSAlphaShiftKeyMask; bit <= NSCommandKeyMask; bit <<= 1, ++i) {
769                 unsigned int currentMask, newMask;
771                 currentMask = self->_current_mods & bit;
772                 newMask     = newMods & bit;
774                 if (currentMask && currentMask != newMask) { // modifier up event
775                         [ self internalHandleKeycode:mapping[i] unicode:0 pressed:NO modifiers:newMods ];
776                 } else if (newMask && currentMask != newMask) { // modifier down event
777                         [ self internalHandleKeycode:mapping[i] unicode:0 pressed:YES modifiers:newMods ];
778                 }
779         }
781         _current_mods = newMods;
785 /** Insert the given text at the given range. */
786 - (void)insertText:(id)aString replacementRange:(NSRange)replacementRange
788         if (!EditBoxInGlobalFocus()) return;
790         NSString *s = [ aString isKindOfClass:[ NSAttributedString class ] ] ? [ aString string ] : (NSString *)aString;
792         const char *insert_point = NULL;
793         const char *replace_range = NULL;
794         if (replacementRange.location != NSNotFound) {
795                 /* Calculate the part to be replaced. */
796                 insert_point = Utf8AdvanceByUtf16Units(_focused_window->GetFocusedText(), replacementRange.location);
797                 replace_range = Utf8AdvanceByUtf16Units(insert_point, replacementRange.length);
798         }
800         HandleTextInput(NULL, true);
801         HandleTextInput([ s UTF8String ], false, NULL, insert_point, replace_range);
804 /** Insert the given text at the caret. */
805 - (void)insertText:(id)aString
807         [ self insertText:aString replacementRange:NSMakeRange(NSNotFound, 0) ];
810 /** Set a new marked text and reposition the caret. */
811 - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange replacementRange:(NSRange)replacementRange
813         if (!EditBoxInGlobalFocus()) return;
815         NSString *s = [ aString isKindOfClass:[ NSAttributedString class ] ] ? [ aString string ] : (NSString *)aString;
817         const char *utf8 = [ s UTF8String ];
818         if (utf8 != NULL) {
819                 const char *insert_point = NULL;
820                 const char *replace_range = NULL;
821                 if (replacementRange.location != NSNotFound) {
822                         /* Calculate the part to be replaced. */
823                         NSRange marked = [ self markedRange ];
824                         insert_point = Utf8AdvanceByUtf16Units(_focused_window->GetFocusedText(), replacementRange.location + (marked.location != NSNotFound ? marked.location : 0u));
825                         replace_range = Utf8AdvanceByUtf16Units(insert_point, replacementRange.length);
826                 }
828                 /* Convert caret index into a pointer in the UTF-8 string. */
829                 const char *selection = Utf8AdvanceByUtf16Units(utf8, selRange.location);
831                 HandleTextInput(utf8, true, selection, insert_point, replace_range);
832         }
835 /** Set a new marked text and reposition the caret. */
836 - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange
838         [ self setMarkedText:aString selectedRange:selRange replacementRange:NSMakeRange(NSNotFound, 0) ];
841 /** Unmark the current marked text. */
842 - (void)unmarkText
844         HandleTextInput(nullptr, true);
847 /** Get the caret position. */
848 - (NSRange)selectedRange
850         if (!EditBoxInGlobalFocus()) return NSMakeRange(NSNotFound, 0);
852         NSUInteger start = CountUtf16Units(_focused_window->GetFocusedText(), _focused_window->GetCaret());
853         return NSMakeRange(start, 0);
856 /** Get the currently marked range. */
857 - (NSRange)markedRange
859         if (!EditBoxInGlobalFocus()) return NSMakeRange(NSNotFound, 0);
861         size_t mark_len;
862         const char *mark = _focused_window->GetMarkedText(&mark_len);
863         if (mark != nullptr) {
864                 NSUInteger start = CountUtf16Units(_focused_window->GetFocusedText(), mark);
865                 NSUInteger len = CountUtf16Units(mark, mark + mark_len);
867                 return NSMakeRange(start, len);
868         }
870         return NSMakeRange(NSNotFound, 0);
873 /** Is any text marked? */
874 - (BOOL)hasMarkedText
876         if (!EditBoxInGlobalFocus()) return NO;
878         size_t len;
879         return _focused_window->GetMarkedText(&len) != nullptr;
882 /** Get a string corresponding to the given range. */
883 - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)theRange actualRange:(NSRangePointer)actualRange
885         if (!EditBoxInGlobalFocus()) return nil;
887         NSString *s = [ NSString stringWithUTF8String:_focused_window->GetFocusedText() ];
888         NSRange valid_range = NSIntersectionRange(NSMakeRange(0, [ s length ]), theRange);
890         if (actualRange != nullptr) *actualRange = valid_range;
891         if (valid_range.length == 0) return nil;
893         return [ [ [ NSAttributedString alloc ] initWithString:[ s substringWithRange:valid_range ] ] autorelease ];
896 /** Get a string corresponding to the given range. */
897 - (NSAttributedString *)attributedSubstringFromRange:(NSRange)theRange
899         return [ self attributedSubstringForProposedRange:theRange actualRange:nil ];
902 /** Get the current edit box string. */
903 - (NSAttributedString *)attributedString
905         if (!EditBoxInGlobalFocus()) return [ [ [ NSAttributedString alloc ] initWithString:@"" ] autorelease ];
907         return [ [ [ NSAttributedString alloc ] initWithString:[ NSString stringWithUTF8String:_focused_window->GetFocusedText() ] ] autorelease ];
910 /** Get the character that is rendered at the given point. */
911 - (NSUInteger)characterIndexForPoint:(NSPoint)thePoint
913         if (!EditBoxInGlobalFocus()) return NSNotFound;
915         NSPoint view_pt = [ self convertRect:[ [ self window ] convertRectFromScreen:NSMakeRect(thePoint.x, thePoint.y, 0, 0) ] fromView:nil ].origin;
917         Point pt = { (int)view_pt.x, (int)[ self frame ].size.height - (int)view_pt.y };
919         const char *ch = _focused_window->GetTextCharacterAtPosition(pt);
920         if (ch == nullptr) return NSNotFound;
922         return CountUtf16Units(_focused_window->GetFocusedText(), ch);
925 /** Get the bounding rect for the given range. */
926 - (NSRect)firstRectForCharacterRange:(NSRange)aRange
928         if (!EditBoxInGlobalFocus()) return NSMakeRect(0, 0, 0, 0);
930         /* Convert range to UTF-8 string pointers. */
931         const char *start = Utf8AdvanceByUtf16Units(_focused_window->GetFocusedText(), aRange.location);
932         const char *end = aRange.length != 0 ? Utf8AdvanceByUtf16Units(_focused_window->GetFocusedText(), aRange.location + aRange.length) : start;
934         /* Get the bounding rect for the text range.*/
935         Rect r = _focused_window->GetTextBoundingRect(start, end);
936         NSRect view_rect = NSMakeRect(_focused_window->left + r.left, [ self frame ].size.height - _focused_window->top - r.bottom, r.right - r.left, r.bottom - r.top);
938         return [ [ self window ] convertRectToScreen:[ self convertRect:view_rect toView:nil ] ];
941 /** Get the bounding rect for the given range. */
942 - (NSRect)firstRectForCharacterRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange
944         return [ self firstRectForCharacterRange:aRange ];
947 /** Get all string attributes that we can process for marked text. */
948 - (NSArray*)validAttributesForMarkedText
950         return [ NSArray array ];
953 /** Delete single character left of the cursor. */
954 - (void)deleteBackward:(id)sender
956         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_BACKSPACE, 0);
959 /** Delete word left of the cursor. */
960 - (void)deleteWordBackward:(id)sender
962         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_BACKSPACE | WKC_CTRL, 0);
965 /** Delete single character right of the cursor. */
966 - (void)deleteForward:(id)sender
968         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_DELETE, 0);
971 /** Delete word right of the cursor. */
972 - (void)deleteWordForward:(id)sender
974         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_DELETE | WKC_CTRL, 0);
977 /** Move cursor one character left. */
978 - (void)moveLeft:(id)sender
980         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_LEFT, 0);
983 /** Move cursor one word left. */
984 - (void)moveWordLeft:(id)sender
986         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_LEFT | WKC_CTRL, 0);
989 /** Move cursor one character right. */
990 - (void)moveRight:(id)sender
992         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_RIGHT, 0);
995 /** Move cursor one word right. */
996 - (void)moveWordRight:(id)sender
998         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_RIGHT | WKC_CTRL, 0);
1001 /** Move cursor one line up. */
1002 - (void)moveUp:(id)sender
1004         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_UP, 0);
1007 /** Move cursor one line down. */
1008 - (void)moveDown:(id)sender
1010         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_DOWN, 0);
1013 /** MScroll one line up. */
1014 - (void)moveUpAndModifySelection:(id)sender
1016         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_UP | WKC_SHIFT, 0);
1019 /** Scroll one line down. */
1020 - (void)moveDownAndModifySelection:(id)sender
1022         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_DOWN | WKC_SHIFT, 0);
1025 /** Move cursor to the start of the line. */
1026 - (void)moveToBeginningOfLine:(id)sender
1028         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_HOME, 0);
1031 /** Move cursor to the end of the line. */
1032 - (void)moveToEndOfLine:(id)sender
1034         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_END, 0);
1037 /** Scroll one page up. */
1038 - (void)scrollPageUp:(id)sender
1040         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_PAGEUP, 0);
1043 /** Scroll one page down. */
1044 - (void)scrollPageDown:(id)sender
1046         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_PAGEDOWN, 0);
1049 /** Move cursor (and selection) one page up. */
1050 - (void)pageUpAndModifySelection:(id)sender
1052         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_PAGEUP | WKC_SHIFT, 0);
1055 /** Move cursor (and selection) one page down. */
1056 - (void)pageDownAndModifySelection:(id)sender
1058         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_PAGEDOWN | WKC_SHIFT, 0);
1061 /** Scroll to the beginning of the document. */
1062 - (void)scrollToBeginningOfDocument:(id)sender
1064         /* For compatibility with OTTD on Win/Linux. */
1065         [ self moveToBeginningOfLine:sender ];
1068 /** Scroll to the end of the document. */
1069 - (void)scrollToEndOfDocument:(id)sender
1071         /* For compatibility with OTTD on Win/Linux. */
1072         [ self moveToEndOfLine:sender ];
1075 /** Return was pressed. */
1076 - (void)insertNewline:(id)sender
1078         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_RETURN, '\r');
1081 /** Escape was pressed. */
1082 - (void)cancelOperation:(id)sender
1084         if (EditBoxInGlobalFocus()) HandleKeypress(WKC_ESC, 0);
1087 /** Invoke the selector if we implement it. */
1088 - (void)doCommandBySelector:(SEL)aSelector
1090         if ([ self respondsToSelector:aSelector ]) [ self performSelector:aSelector ];
1093 @end
1096 @implementation OTTD_CocoaWindowDelegate {
1097         VideoDriver_Cocoa *driver;
1100 /** Initialize the video driver */
1101 - (instancetype)initWithDriver:(VideoDriver_Cocoa *)drv
1103         if (self = [ super init ]) {
1104                 self->driver = drv;
1105         }
1106         return self;
1108 /** Handle closure requests */
1109 - (BOOL)windowShouldClose:(id)sender
1111         HandleExitGameRequest();
1113         return NO;
1115 /** Window entered fullscreen mode (10.7). */
1116 - (void)windowDidEnterFullScreen:(NSNotification *)aNotification
1118         NSPoint loc = [ driver->cocoaview convertPoint:[ [ aNotification object ] mouseLocationOutsideOfEventStream ] fromView:nil ];
1119         BOOL inside = ([ driver->cocoaview hitTest:loc ] == driver->cocoaview);
1121         if (inside) {
1122                 /* We don't care about the event, but the compiler does. */
1123                 NSEvent *e = [ [ NSEvent alloc ] init ];
1124                 [ driver->cocoaview mouseEntered:e ];
1125                 [ e release ];
1126         }
1128 /** Screen the window is on changed. */
1129 - (void)windowDidChangeBackingProperties:(NSNotification *)notification
1131         /* Reallocate screen buffer if necessary. */
1132         driver->AllocateBackingStore();
1135 /** Presentation options to use for fullsreen mode. */
1136 - (NSApplicationPresentationOptions)window:(NSWindow *)window willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions
1138         return NSApplicationPresentationFullScreen | NSApplicationPresentationHideMenuBar | NSApplicationPresentationHideDock;
1141 @end
1143 #endif /* WITH_COCOA */