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/>.
8 /******************************************************************************
10 * Known things left to do: *
11 * List available resolutions. *
12 ******************************************************************************/
16 #include "../../stdafx.h"
17 #include "../../os/macosx/macos.h"
20 #define Point OTTDPoint
21 #import <Cocoa/Cocoa.h>
25 #include "../../openttd.h"
26 #include "../../debug.h"
27 #include "../../rev.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"
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.
49 bool _allow_hidpi_window = true; // Referenced from table/misc_settings.ini
51 @interface OTTDMain : NSObject <NSApplicationDelegate>
54 NSString *OTTDMainLaunchGameEngine = @"ottdmain_launch_game_engine";
58 static bool _cocoa_video_dialog = false;
59 static OTTDMain *_ottd_main;
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.
68 static NSUInteger CountUtf16Units(const char *from, const char *to)
74 size_t len = Utf8Decode(&c, from);
75 i += len < 4 ? 1 : 2; // Watch for surrogate pairs.
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.
88 static const char *Utf8AdvanceByUtf16Units(const char *str, NSUInteger count)
90 for (NSUInteger i = 0; i < count && *str != '\0'; ) {
92 size_t len = Utf8Decode(&c, str);
93 i += len < 4 ? 1 : 2; // Watch for surrogates.
101 * Convert a NSString to an UTF-32 encoded string.
102 * @param s String to convert.
103 * @return Vector of UTF-32 characters.
105 static std::vector<WChar> NSStringToUTF32(NSString *s)
107 std::vector<WChar> unicode_str;
110 for (NSUInteger i = 0; i < s.length; i++) {
111 unichar c = [ s characterAtIndex:i ];
112 if (Utf16IsLeadSurrogate(c)) {
115 } else if (Utf16IsTrailSurrogate(c)) {
116 if (lead != 0) unicode_str.push_back(Utf16DecodeSurrogate(lead, c));
118 unicode_str.push_back(c);
127 * The main class of the application, the application's delegate.
129 @implementation OTTDMain
131 * Stop the game engine. Must be called on main thread.
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.
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 ];
154 /* Hand off to main application code. */
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.
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.
176 - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*) sender
178 HandleExitGameRequest();
180 return NSTerminateCancel;
184 * Remove ourself as a notification observer.
186 - (void)unregisterObserver
188 [ [ NSNotificationCenter defaultCenter ] removeObserver:self ];
193 * Initialize the application menu shown in top bar.
195 static void setApplicationMenu()
197 NSString *appName = @"OpenTTD";
198 NSMenu *appleMenu = [ [ NSMenu alloc ] initWithTitle:appName ];
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 ];
231 /* Finally give up our references to the objects */
232 [ appleMenu release ];
233 [ menuItem release ];
237 * Create a window menu.
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" ];
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.
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];
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();
293 /* Create OTTDMain and make it the app delegate */
294 _ottd_main = [ [ OTTDMain alloc ] init ];
295 [ NSApp setDelegate:_ottd_main ];
301 * Deregister app delegate.
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 .
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);
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 ] ];
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
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) ];
365 @implementation OTTD_CocoaWindow {
366 VideoDriver_Cocoa *driver;
370 * Initialize event system for the application rectangle
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 ]) {
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 ];
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.
397 /* save current visible surface */
398 [ self cacheImageInRect:[ driver->cocoaview frame ] ];
400 /* let the window manager redraw controls, border, etc */
403 /* restore visible surface */
404 [ self restoreCachedImage ];
407 * Define the rectangle we draw our window in
409 - (void)setFrame:(NSRect)frameRect display:(BOOL)flag
411 [ super setFrame:frameRect display:flag ];
413 driver->AllocateBackingStore();
418 @implementation OTTD_CocoaView {
419 float _current_magnification;
420 NSUInteger _current_mods;
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:) ];
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
451 - (BOOL)acceptsFirstResponder
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 ] ];
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 ];
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 ];
486 * Make OpenTTD aware that it has control over the mouse
488 - (void)mouseEntered:(NSEvent *)theEvent
490 _cursor.in_window = true;
493 * Make OpenTTD aware that it has NOT control over the mouse
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
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);
520 NSPoint pt = [ self mousePositionFromEvent:event ];
521 _cursor.UpdateCursorPosition(pt.x, pt.y, false);
527 - (void)internalMouseButtonEvent
529 bool cur_fix = _cursor.fix_at;
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
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 ];
560 _left_button_down = true;
561 [ self internalMouseButtonEvent ];
564 - (void)mouseUp:(NSEvent *)event
566 if (self->_emulated_down) {
567 self->_emulated_down = false;
568 [ self rightMouseUp:event ];
570 _left_button_down = false;
571 _left_button_clicked = false;
572 [ self internalMouseButtonEvent ];
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 */
596 } else if ([ event deltaY ] < 0.0) { /* Scroll down */
598 } /* else: deltaY was 0.0 and we don't want to do anything */
600 /* Update the scroll count for 2D scrolling */
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;
612 deltaX = [ event deltaX ] * 5;
613 deltaY = [ event deltaY ] * 5;
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;
630 while (self->_current_magnification <= -1.0f) {
631 self->_current_magnification += 1.0f;
637 - (void)endGestureWithEvent:(NSEvent *)event
640 self->_current_magnification = 0.0f;
644 - (BOOL)internalHandleKeycode:(unsigned short)keycode unicode:(WChar)unicode pressed:(BOOL)down modifiers:(NSUInteger)modifiers
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;
656 if (down && (modifiers & NSCommandKeyMask)) {
657 VideoDriver::GetInstance()->ToggleFullscreen(!_fullscreen);
662 if (down && EditBoxInGlobalFocus() && (modifiers & (NSCommandKeyMask | NSControlKeyMask))) {
663 HandleKeypress(WKC_CTRL | 'V', unicode);
667 if (down && EditBoxInGlobalFocus() && (modifiers & (NSCommandKeyMask | NSControlKeyMask))) {
668 HandleKeypress(WKC_CTRL | 'U', unicode);
673 BOOL interpret_keys = YES;
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)) {
689 /* Backquote is a dead key, require a double press for hotkey behaviour (i.e. console). */
693 /* Second backquote, don't interpret as text input. */
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);
703 DEBUG(driver, 2, "cocoa_v: QZ_KeyEvent: %x (%x), down, mapping: %x", keycode, unicode, pressed_key);
705 DEBUG(driver, 2, "cocoa_v: QZ_KeyEvent: %x (%x), up", keycode, unicode);
708 return interpret_keys;
711 - (void)keyDown:(NSEvent *)event
713 /* Quit, hide and minimize */
714 switch (event.keyCode) {
718 if (event.modifierFlags & NSCommandKeyMask) {
719 [ self interpretKeyEvents:[ NSArray arrayWithObject:event ] ];
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 ] ];
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 ];
740 - (void)keyUp:(NSEvent *)event
742 /* Quit, hide and minimize */
743 switch (event.keyCode) {
747 if (event.modifierFlags & NSCommandKeyMask) {
748 [ self interpretKeyEvents:[ NSArray arrayWithObject:event ] ];
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 ];
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);
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 ];
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);
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);
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. */
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);
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);
870 return NSMakeRange(NSNotFound, 0);
873 /** Is any text marked? */
874 - (BOOL)hasMarkedText
876 if (!EditBoxInGlobalFocus()) return NO;
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 ];
1096 @implementation OTTD_CocoaWindowDelegate {
1097 VideoDriver_Cocoa *driver;
1100 /** Initialize the video driver */
1101 - (instancetype)initWithDriver:(VideoDriver_Cocoa *)drv
1103 if (self = [ super init ]) {
1108 /** Handle closure requests */
1109 - (BOOL)windowShouldClose:(id)sender
1111 HandleExitGameRequest();
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);
1122 /* We don't care about the event, but the compiler does. */
1123 NSEvent *e = [ [ NSEvent alloc ] init ];
1124 [ driver->cocoaview mouseEntered:e ];
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;
1143 #endif /* WITH_COCOA */