4 * This file is part of OpenTTD.
5 * 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.
6 * 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.
7 * 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/>.
10 /******************************************************************************
11 * Cocoa video driver *
12 * Known things left to do: *
13 * Nothing at the moment. *
14 ******************************************************************************/
18 #include "../../stdafx.h"
21 #define Point OTTDPoint
22 #import <Cocoa/Cocoa.h>
26 #include "../../openttd.h"
27 #include "../../debug.h"
28 #include "../../os/macosx/splash.h"
29 #include "../../settings_type.h"
30 #include "../../core/geometry_type.hpp"
32 #include "cocoa_keys.h"
33 #include "../../blitter/factory.hpp"
34 #include "../../gfx_func.h"
35 #include "../../network/network.h"
36 #include "../../core/random_func.hpp"
37 #include "../../core/math_func.hpp"
38 #include "../../texteff.hpp"
39 #include "../../window_func.h"
41 #import <sys/time.h> /* gettimeofday */
44 * Important notice regarding all modifications!!!!!!!
45 * There are certain limitations because the file is objective C++.
46 * gdb has limitations.
47 * C++ and objective C code can't be joined in all cases (classes stuff).
48 * Read http://developer.apple.com/releasenotes/Cocoa/Objective-C++.html for more information.
52 /* Right Mouse Button Emulation enum */
53 enum RightMouseButtonEmulationState {
60 static unsigned int _current_mods;
61 static bool _tab_is_down;
62 static bool _emulating_right_button;
63 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
64 static float _current_magnification;
67 static uint32 _tEvent;
71 /* Support for touch gestures is only available starting with the
72 * 10.6 SDK, even if it says that support starts in fact with 10.5.2.
73 * Replicate the needed stuff for older SDKs. */
74 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 && MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_6)
75 static const NSUInteger NSEventTypeMagnify = 30;
76 static const NSUInteger NSEventTypeEndGesture = 20;
79 /* This message is valid for events of type NSEventTypeMagnify, on 10.5.2 or later */
80 - (CGFloat)magnification WEAK_IMPORT_ATTRIBUTE;
85 static uint32 GetTick()
89 gettimeofday(&tim, NULL);
90 return tim.tv_usec / 1000 + tim.tv_sec * 1000;
93 static void QZ_WarpCursor(int x, int y)
95 assert(_cocoa_subdriver != NULL);
97 /* Only allow warping when in foreground */
98 if (![ NSApp isActive ]) return;
100 NSPoint p = NSMakePoint(x, y);
101 CGPoint cgp = _cocoa_subdriver->PrivateLocalToCG(&p);
103 /* this is the magic call that fixes cursor "freezing" after warp */
104 CGSetLocalEventsSuppressionInterval(0.0);
105 /* Do the actual warp */
106 CGWarpMouseCursorPosition(cgp);
110 static void QZ_CheckPaletteAnim()
112 if (_cur_palette.count_dirty != 0) {
113 Blitter *blitter = BlitterFactory::GetCurrentBlitter();
115 switch (blitter->UsePaletteAnimation()) {
116 case Blitter::PALETTE_ANIMATION_VIDEO_BACKEND:
117 _cocoa_subdriver->UpdatePalette(_cur_palette.first_dirty, _cur_palette.count_dirty);
120 case Blitter::PALETTE_ANIMATION_BLITTER:
121 blitter->PaletteAnimate(_cur_palette);
124 case Blitter::PALETTE_ANIMATION_NONE:
130 _cur_palette.count_dirty = 0;
137 unsigned short vk_from;
141 #define AS(x, z) {x, z}
143 static const VkMapping _vk_mapping[] = {
144 AS(QZ_BACKQUOTE, WKC_BACKQUOTE), // key left of '1'
145 AS(QZ_BACKQUOTE2, WKC_BACKQUOTE), // some keyboards have it on another scancode
147 /* Pageup stuff + up/down */
148 AS(QZ_PAGEUP, WKC_PAGEUP),
149 AS(QZ_PAGEDOWN, WKC_PAGEDOWN),
152 AS(QZ_DOWN, WKC_DOWN),
153 AS(QZ_LEFT, WKC_LEFT),
154 AS(QZ_RIGHT, WKC_RIGHT),
156 AS(QZ_HOME, WKC_HOME),
159 AS(QZ_INSERT, WKC_INSERT),
160 AS(QZ_DELETE, WKC_DELETE),
162 /* Letters. QZ_[a-z] is not in numerical order so we can't use AM(...) */
189 /* Same thing for digits */
201 AS(QZ_ESCAPE, WKC_ESC),
202 AS(QZ_PAUSE, WKC_PAUSE),
203 AS(QZ_BACKSPACE, WKC_BACKSPACE),
205 AS(QZ_SPACE, WKC_SPACE),
206 AS(QZ_RETURN, WKC_RETURN),
234 AS(QZ_KP_DIVIDE, WKC_NUM_DIV),
235 AS(QZ_KP_MULTIPLY, WKC_NUM_MUL),
236 AS(QZ_KP_MINUS, WKC_NUM_MINUS),
237 AS(QZ_KP_PLUS, WKC_NUM_PLUS),
238 AS(QZ_KP_ENTER, WKC_NUM_ENTER),
239 AS(QZ_KP_PERIOD, WKC_NUM_DECIMAL),
241 /* Other non-letter keys */
242 AS(QZ_SLASH, WKC_SLASH),
243 AS(QZ_SEMICOLON, WKC_SEMICOLON),
244 AS(QZ_EQUALS, WKC_EQUALS),
245 AS(QZ_LEFTBRACKET, WKC_L_BRACKET),
246 AS(QZ_BACKSLASH, WKC_BACKSLASH),
247 AS(QZ_RIGHTBRACKET, WKC_R_BRACKET),
249 AS(QZ_QUOTE, WKC_SINGLEQUOTE),
250 AS(QZ_COMMA, WKC_COMMA),
251 AS(QZ_MINUS, WKC_MINUS),
252 AS(QZ_PERIOD, WKC_PERIOD)
256 static uint32 QZ_MapKey(unsigned short sym)
260 for (const VkMapping *map = _vk_mapping; map != endof(_vk_mapping); ++map) {
261 if (sym == map->vk_from) {
267 if (_current_mods & NSShiftKeyMask) key |= WKC_SHIFT;
268 if (_current_mods & NSControlKeyMask) key |= (_settings_client.gui.right_mouse_btn_emulation != RMBE_CONTROL ? WKC_CTRL : WKC_META);
269 if (_current_mods & NSAlternateKeyMask) key |= WKC_ALT;
270 if (_current_mods & NSCommandKeyMask) key |= (_settings_client.gui.right_mouse_btn_emulation != RMBE_CONTROL ? WKC_META : WKC_CTRL);
275 static bool QZ_KeyEvent(unsigned short keycode, unsigned short unicode, BOOL down)
277 bool interpret_keys = true;
280 case QZ_UP: SB(_dirkeys, 1, 1, down); break;
281 case QZ_DOWN: SB(_dirkeys, 3, 1, down); break;
282 case QZ_LEFT: SB(_dirkeys, 0, 1, down); break;
283 case QZ_RIGHT: SB(_dirkeys, 2, 1, down); break;
285 case QZ_TAB: _tab_is_down = down; break;
289 if (down && (_current_mods & NSCommandKeyMask)) {
290 VideoDriver::GetInstance()->ToggleFullscreen(!_fullscreen);
295 if (down && EditBoxInGlobalFocus() && (_current_mods & (NSCommandKeyMask | NSControlKeyMask))) {
296 HandleKeypress(WKC_CTRL | 'V', unicode);
300 if (down && EditBoxInGlobalFocus() && (_current_mods & (NSCommandKeyMask | NSControlKeyMask))) {
301 HandleKeypress(WKC_CTRL | 'U', unicode);
307 uint32 pressed_key = QZ_MapKey(keycode);
309 static bool console = false;
311 /* The second backquote may have a character, which we don't want to interpret. */
312 if (pressed_key == WKC_BACKQUOTE && (console || unicode == 0)) {
314 /* Backquote is a dead key, require a double press for hotkey behaviour (i.e. console). */
318 /* Second backquote, don't interpret as text input. */
319 interpret_keys = false;
324 /* Don't handle normal characters if an edit box has the focus. */
325 if (!EditBoxInGlobalFocus() || IsInsideMM(pressed_key & ~WKC_SPECIAL_KEYS, WKC_F1, WKC_PAUSE + 1)) {
326 HandleKeypress(pressed_key, unicode);
328 DEBUG(driver, 2, "cocoa_v: QZ_KeyEvent: %x (%x), down, mapping: %x", keycode, unicode, pressed_key);
330 DEBUG(driver, 2, "cocoa_v: QZ_KeyEvent: %x (%x), up", keycode, unicode);
333 return interpret_keys;
336 static void QZ_DoUnsidedModifiers(unsigned int newMods)
338 const int mapping[] = { QZ_CAPSLOCK, QZ_LSHIFT, QZ_LCTRL, QZ_LALT, QZ_LMETA };
340 if (_current_mods == newMods) return;
342 /* Iterate through the bits, testing each against the current modifiers */
343 for (unsigned int i = 0, bit = NSAlphaShiftKeyMask; bit <= NSCommandKeyMask; bit <<= 1, ++i) {
344 unsigned int currentMask, newMask;
346 currentMask = _current_mods & bit;
347 newMask = newMods & bit;
349 if (currentMask && currentMask != newMask) { // modifier up event
350 /* If this was Caps Lock, we need some additional voodoo to make SDL happy (is this needed in ottd?) */
351 if (bit == NSAlphaShiftKeyMask) QZ_KeyEvent(mapping[i], 0, YES);
352 QZ_KeyEvent(mapping[i], 0, NO);
353 } else if (newMask && currentMask != newMask) { // modifier down event
354 QZ_KeyEvent(mapping[i], 0, YES);
355 /* If this was Caps Lock, we need some additional voodoo to make SDL happy (is this needed in ottd?) */
356 if (bit == NSAlphaShiftKeyMask) QZ_KeyEvent(mapping[i], 0, NO);
360 _current_mods = newMods;
363 static void QZ_MouseMovedEvent(int x, int y)
365 if (_cursor.UpdateCursorPosition(x, y, false)) {
366 QZ_WarpCursor(_cursor.pos.x, _cursor.pos.y);
372 static void QZ_MouseButtonEvent(int button, BOOL down)
377 _left_button_down = true;
379 _left_button_down = false;
380 _left_button_clicked = false;
387 _right_button_down = true;
388 _right_button_clicked = true;
390 _right_button_down = false;
400 static bool QZ_PollEvent()
402 assert(_cocoa_subdriver != NULL);
405 uint32 et0 = GetTick();
407 NSEvent *event = [ NSApp nextEventMatchingMask:NSAnyEventMask
408 untilDate:[ NSDate distantPast ]
409 inMode:NSDefaultRunLoopMode dequeue:YES ];
411 _tEvent += GetTick() - et0;
414 if (event == nil) return false;
415 if (!_cocoa_subdriver->IsActive()) {
416 [ NSApp sendEvent:event ];
420 QZ_DoUnsidedModifiers( [ event modifierFlags ] );
424 switch ([ event type ]) {
426 case NSOtherMouseDragged:
427 case NSLeftMouseDragged:
428 pt = _cocoa_subdriver->GetMouseLocation(event);
429 if (!_cocoa_subdriver->MouseIsInsideView(&pt) && !_emulating_right_button) {
430 [ NSApp sendEvent:event ];
434 QZ_MouseMovedEvent((int)pt.x, (int)pt.y);
437 case NSRightMouseDragged:
438 pt = _cocoa_subdriver->GetMouseLocation(event);
439 QZ_MouseMovedEvent((int)pt.x, (int)pt.y);
442 case NSLeftMouseDown:
445 if (_settings_client.gui.right_mouse_btn_emulation == RMBE_COMMAND) keymask |= NSCommandKeyMask;
446 if (_settings_client.gui.right_mouse_btn_emulation == RMBE_CONTROL) keymask |= NSControlKeyMask;
448 pt = _cocoa_subdriver->GetMouseLocation(event);
450 if (!([ event modifierFlags ] & keymask) || !_cocoa_subdriver->MouseIsInsideView(&pt)) {
451 [ NSApp sendEvent:event ];
454 QZ_MouseMovedEvent((int)pt.x, (int)pt.y);
456 /* Right mouse button emulation */
457 if ([ event modifierFlags ] & keymask) {
458 _emulating_right_button = true;
459 QZ_MouseButtonEvent(1, YES);
461 QZ_MouseButtonEvent(0, YES);
466 [ NSApp sendEvent:event ];
468 pt = _cocoa_subdriver->GetMouseLocation(event);
470 QZ_MouseMovedEvent((int)pt.x, (int)pt.y);
472 /* Right mouse button emulation */
473 if (_emulating_right_button) {
474 _emulating_right_button = false;
475 QZ_MouseButtonEvent(1, NO);
477 QZ_MouseButtonEvent(0, NO);
481 case NSRightMouseDown:
482 pt = _cocoa_subdriver->GetMouseLocation(event);
483 if (!_cocoa_subdriver->MouseIsInsideView(&pt)) {
484 [ NSApp sendEvent:event ];
488 QZ_MouseMovedEvent((int)pt.x, (int)pt.y);
489 QZ_MouseButtonEvent(1, YES);
493 pt = _cocoa_subdriver->GetMouseLocation(event);
494 if (!_cocoa_subdriver->MouseIsInsideView(&pt)) {
495 [ NSApp sendEvent:event ];
499 QZ_MouseMovedEvent((int)pt.x, (int)pt.y);
500 QZ_MouseButtonEvent(1, NO);
504 /* This is not needed since openttd currently only use two buttons */
505 case NSOtherMouseDown:
506 pt = QZ_GetMouseLocation(event);
507 if (!QZ_MouseIsInsideView(&pt)) {
508 [ NSApp sendEvent:event ];
512 QZ_MouseMovedEvent((int)pt.x, (int)pt.y);
513 QZ_MouseButtonEvent([ event buttonNumber ], YES);
517 pt = QZ_GetMouseLocation(event);
518 if (!QZ_MouseIsInsideView(&pt)) {
519 [ NSApp sendEvent:event ];
523 QZ_MouseMovedEvent((int)pt.x, (int)pt.y);
524 QZ_MouseButtonEvent([ event buttonNumber ], NO);
529 /* Quit, hide and minimize */
530 switch ([ event keyCode ]) {
534 if ([ event modifierFlags ] & NSCommandKeyMask) {
535 [ NSApp sendEvent:event ];
540 chars = [ event characters ];
541 unsigned short unicode = [ chars length ] > 0 ? [ chars characterAtIndex:0 ] : 0;
542 if (EditBoxInGlobalFocus()) {
543 if (QZ_KeyEvent([ event keyCode ], unicode, YES)) {
544 [ _cocoa_subdriver->cocoaview interpretKeyEvents:[ NSArray arrayWithObject:event ] ];
547 QZ_KeyEvent([ event keyCode ], unicode, YES);
548 for (uint i = 1; i < [ chars length ]; i++) {
549 QZ_KeyEvent(0, [ chars characterAtIndex:i ], YES);
556 /* Quit, hide and minimize */
557 switch ([ event keyCode ]) {
561 if ([ event modifierFlags ] & NSCommandKeyMask) {
562 [ NSApp sendEvent:event ];
567 chars = [ event characters ];
568 QZ_KeyEvent([ event keyCode ], [ chars length ] ? [ chars characterAtIndex:0 ] : 0, NO);
572 if ([ event deltaY ] > 0.0) { /* Scroll up */
574 } else if ([ event deltaY ] < 0.0) { /* Scroll down */
576 } /* else: deltaY was 0.0 and we don't want to do anything */
578 /* Set the scroll count for scrollwheel scrolling */
579 _cursor.h_wheel -= (int)([ event deltaX ] * 5 * _settings_client.gui.scrollwheel_multiplier);
580 _cursor.v_wheel -= (int)([ event deltaY ] * 5 * _settings_client.gui.scrollwheel_multiplier);
583 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
584 case NSEventTypeMagnify:
585 /* Pinch open or close gesture. */
586 _current_magnification += [ event magnification ] * 5.0f;
588 while (_current_magnification >= 1.0f) {
589 _current_magnification -= 1.0f;
593 while (_current_magnification <= -1.0f) {
594 _current_magnification += 1.0f;
600 case NSEventTypeEndGesture:
602 _current_magnification = 0.0f;
609 /* Catch these events if the cursor is dragging. During dragging, we reset
610 * the mouse position programmatically, which would trigger OS X to show
611 * the default arrow cursor if the events are propagated. */
612 if (_cursor.fix_at) break;
616 [ NSApp sendEvent:event ];
625 uint32 cur_ticks = GetTick();
626 uint32 last_cur_ticks = cur_ticks;
627 uint32 next_tick = cur_ticks + MILLISECONDS_PER_TICK;
630 uint32 et0 = GetTick();
634 DisplaySplashImage();
635 QZ_CheckPaletteAnim();
636 _cocoa_subdriver->Draw(true);
639 for (int i = 0; i < 2; i++) GameLoop();
642 QZ_CheckPaletteAnim();
643 _cocoa_subdriver->Draw();
646 /* Set the proper OpenTTD palette which got spoilt by the splash
647 * image when using 8bpp blitter */
649 QZ_CheckPaletteAnim();
650 _cocoa_subdriver->Draw(true);
653 uint32 prev_cur_ticks = cur_ticks; // to check for wrapping
654 InteractiveRandom(); // randomness
656 while (QZ_PollEvent()) {}
658 if (_exit_game) break;
661 if (_current_mods & NSShiftKeyMask)
666 if (!_networking && _game_mode != GM_MENU) _fast_forward |= 2;
667 } else if (_fast_forward & 2) {
671 cur_ticks = GetTick();
672 if (cur_ticks >= next_tick || (_fast_forward && !_pause_mode) || cur_ticks < prev_cur_ticks) {
673 _realtime_tick += cur_ticks - last_cur_ticks;
674 last_cur_ticks = cur_ticks;
675 next_tick = cur_ticks + MILLISECONDS_PER_TICK;
677 bool old_ctrl_pressed = _ctrl_pressed;
679 _ctrl_pressed = !!(_current_mods & ( _settings_client.gui.right_mouse_btn_emulation != RMBE_CONTROL ? NSControlKeyMask : NSCommandKeyMask));
680 _shift_pressed = !!(_current_mods & NSShiftKeyMask);
682 if (old_ctrl_pressed != _ctrl_pressed) HandleCtrlChanged();
687 QZ_CheckPaletteAnim();
688 _cocoa_subdriver->Draw();
691 uint32 st0 = GetTick();
695 st += GetTick() - st0;
697 NetworkDrawChatMessage();
699 _cocoa_subdriver->Draw();
704 uint32 et = GetTick();
706 DEBUG(driver, 1, "cocoa_v: nextEventMatchingMask took %i ms total", _tEvent);
707 DEBUG(driver, 1, "cocoa_v: game loop took %i ms total (%i ms without sleep)", et - et0, et - et0 - st);
708 DEBUG(driver, 1, "cocoa_v: (nextEventMatchingMask total)/(game loop total) is %f%%", (double)_tEvent / (double)(et - et0) * 100);
709 DEBUG(driver, 1, "cocoa_v: (nextEventMatchingMask total)/(game loop without sleep total) is %f%%", (double)_tEvent / (double)(et - et0 - st) * 100);
713 #endif /* WITH_COCOA */