1/* 2 File: MBCBoardViewMouse.mm 3 Contains: Handle mouse coordinate transformations 4 Copyright: © 2002-2012 by Apple Inc., all rights reserved. 5 6 IMPORTANT: This Apple software is supplied to you by Apple Computer, 7 Inc. ("Apple") in consideration of your agreement to the following 8 terms, and your use, installation, modification or redistribution of 9 this Apple software constitutes acceptance of these terms. If you do 10 not agree with these terms, please do not use, install, modify or 11 redistribute this Apple software. 12 13 In consideration of your agreement to abide by the following terms, 14 and subject to these terms, Apple grants you a personal, non-exclusive 15 license, under Apple's copyrights in this original Apple software (the 16 "Apple Software"), to use, reproduce, modify and redistribute the 17 Apple Software, with or without modifications, in source and/or binary 18 forms; provided that if you redistribute the Apple Software in its 19 entirety and without modifications, you must retain this notice and 20 the following text and disclaimers in all such redistributions of the 21 Apple Software. Neither the name, trademarks, service marks or logos 22 of Apple Inc. may be used to endorse or promote products 23 derived from the Apple Software without specific prior written 24 permission from Apple. Except as expressly stated in this notice, no 25 other rights or licenses, express or implied, are granted by Apple 26 herein, including but not limited to any patent rights that may be 27 infringed by your derivative works or by other works in which the 28 Apple Software may be incorporated. 29 30 The Apple Software is provided by Apple on an "AS IS" basis. APPLE 31 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 32 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND 33 FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS 34 USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 35 36 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, 37 INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 38 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 39 PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, 40 REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, 41 HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING 42 NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN 43 ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 44*/ 45 46#import "MBCBoardViewMouse.h" 47#import "MBCBoardViewDraw.h" // For drawBoardPlane 48#import "MBCInteractivePlayer.h" 49#import "MBCController.h" 50#import "MBCEngine.h" 51#import "MBCBoardWin.h" 52#import "MBCDebug.h" 53 54#import <OpenGL/glu.h> 55 56#import <algorithm> 57 58using std::min; 59using std::max; 60 61// 62// We're doing a lot of Projects and UnProjects. 63// These classes encapsulate them. 64// 65class MBCProjector { 66public: 67 MBCProjector(); 68 69 NSPoint Project(MBCPosition pos); 70protected: 71 GLint fViewport[4]; 72 GLdouble fMVMatrix[16]; 73 GLdouble fProjMatrix[16]; 74}; 75 76class MBCUnProjector : private MBCProjector { 77public: 78 MBCUnProjector(GLdouble winX, GLdouble winY); 79 80 MBCPosition UnProject(MBCBoardView * view); 81 MBCPosition UnProject(GLfloat knownY); 82private: 83 GLdouble fWinX; 84 GLdouble fWinY; 85}; 86 87MBCProjector::MBCProjector() 88{ 89 glGetIntegerv(GL_VIEWPORT, fViewport); 90 glGetDoublev(GL_MODELVIEW_MATRIX, fMVMatrix); 91 glGetDoublev(GL_PROJECTION_MATRIX, fProjMatrix); 92} 93 94NSPoint MBCProjector::Project(MBCPosition pos) 95{ 96 GLdouble w[3]; 97 98 gluProject(pos[0], pos[1], pos[2], fMVMatrix, fProjMatrix, fViewport, 99 w+0, w+1, w+2); 100 101 NSPoint pt = {w[0], w[1]}; 102 103 return pt; 104} 105 106MBCUnProjector::MBCUnProjector(GLdouble winX, GLdouble winY) 107 : MBCProjector(), fWinX(winX), fWinY(winY) 108{ 109} 110 111MBCPosition MBCUnProjector::UnProject(MBCBoardView * view) 112{ 113 MBCPosition pos; 114 GLfloat z; 115 GLdouble wv[3]; 116 117 glReadPixels((GLint)fWinX, (GLint)fWinY, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &z); 118 if (z < 0.0001) { 119 if (MBCDebug::LogMouse()) 120 fprintf(stderr, "Z buffer corruption, redrawing scene\n"); 121 [view drawNow]; 122 glReadPixels((GLint)fWinX, (GLint)fWinY, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &z); 123 } 124 gluUnProject(fWinX, fWinY, z, fMVMatrix, fProjMatrix, fViewport, 125 wv+0, wv+1, wv+2); 126 127 pos[0] = wv[0]; 128 pos[1] = wv[1]; 129 pos[2] = wv[2]; 130 131 if (MBCDebug::LogMouse()) 132 fprintf(stderr, "Mouse (%.0f,%.0f) @ %5.3f -> (%4.1f,%4.1f,%4.1f)\n", 133 fWinX, fWinY, z, pos[0], pos[1], pos[2]); 134 135 return pos; 136} 137 138MBCPosition MBCUnProjector::UnProject(GLfloat knownY) 139{ 140 MBCPosition pos; 141 GLdouble p1[3]; 142 GLdouble p0[3]; 143 144 gluUnProject(fWinX, fWinY, 1.0f, fMVMatrix, fProjMatrix, fViewport, 145 p1+0, p1+1, p1+2); 146 gluUnProject(fWinX, fWinY, 0.0f, fMVMatrix, fProjMatrix, fViewport, 147 p0+0, p0+1, p0+2); 148 GLdouble yint = (knownY-p1[1])/(p0[1]-p1[1]); 149 pos[0] = p1[0]+(p0[0]-p1[0])*yint; 150 pos[1] = knownY; 151 pos[2] = p1[2]+(p0[2]-p1[2])*yint; 152 if (MBCDebug::LogMouse()) 153 fprintf(stderr, "Mouse (%.0f,%.0f) [%5.3f] -> (%4.1f,%4.1f,%4.1f)\n", 154 fWinX, fWinY, knownY, pos[0], pos[1], pos[2]); 155 156 return pos; 157} 158 159MBCPosition operator-(const MBCPosition & a, const MBCPosition & b) 160{ 161 MBCPosition res; 162 163 res[0] = a[0]-b[0]; 164 res[1] = a[1]-b[1]; 165 res[2] = a[2]-b[2]; 166 167 return res; 168} 169 170@implementation MBCBoardView ( Mouse ) 171 172- (NSRect) approximateBoundsOfSquare:(MBCSquare)square 173{ 174 const float kSquare = 4.5f; 175 176 MBCProjector proj; 177 MBCPosition pos = [self squareToPosition:square]; 178 179 pos[0] -= kSquare; 180 pos[2] -= kSquare; 181 NSPoint p0 = proj.Project(pos); 182 183 pos[0] += 2.0f*kSquare; 184 NSPoint p1 = proj.Project(pos); 185 186 pos[2] += 2.0f*kSquare; 187 NSPoint p2 = proj.Project(pos); 188 189 pos[0] -= 2.0f*kSquare; 190 NSPoint p3 = proj.Project(pos); 191 192 NSRect r; 193 if (p1.x > p0.x) { 194 r.origin.x = max(p0.x, p3.x); 195 r.size.width = min(p1.x, p2.x)-r.origin.x; 196 } else { 197 r.origin.x = max(p1.x, p2.x); 198 r.size.width = min(p0.x, p3.x)-r.origin.x; 199 } 200 if (p2.y > p1.y) { 201 r.origin.y = max(p0.y, p1.y); 202 r.size.height = min(p2.y, p3.y)-r.origin.y; 203 } else { 204 r.origin.y = max(p2.y, p3.y); 205 r.size.height = min(p0.y, p1.y)-r.origin.y; 206 } 207 208 return [self convertRectFromBacking:r]; 209} 210 211- (MBCPosition) mouseToPosition:(NSPoint)mouse 212{ 213 if (MBCDebug::LogMouse()) 214 fprintf(stderr, "[%.0f,%.0f] ", mouse.x, mouse.y); 215 mouse = [self convertPointToBacking:mouse]; 216 MBCUnProjector unproj(mouse.x, mouse.y); 217 218 return unproj.UnProject(self); 219} 220 221- (MBCPosition) mouseToPositionIgnoringY:(NSPoint)mouse 222{ 223 mouse = [self convertPointToBacking:mouse]; 224 MBCUnProjector unproj(mouse.x, mouse.y); 225 226 return unproj.UnProject(0.0f); 227} 228 229- (MBCPosition) eventToPosition:(NSEvent *)event 230{ 231 [[self openGLContext] makeCurrentContext]; 232 233 NSPoint p = [event locationInWindow]; 234 NSPoint l = [self convertPoint:p fromView:nil]; 235 236 return [self mouseToPosition:l]; 237} 238 239- (void) mouseEntered:(NSEvent *)theEvent 240{ 241 if (MBCDebug::LogMouse()) 242 fprintf(stderr, "mouseEntered\n"); 243 244 [[self window] setAcceptsMouseMovedEvents:YES]; 245 [[self window] makeFirstResponder:self]; 246} 247 248- (void)mouseExited:(NSEvent *)theEvent 249{ 250 if (MBCDebug::LogMouse()) 251 fprintf(stderr, "mouseExited\n"); 252 253 [[self window] setAcceptsMouseMovedEvents:NO]; 254} 255 256- (void) mouseMoved:(NSEvent *)event 257{ 258 if (MBCDebug::LogMouse()) 259 fprintf(stderr, "mouseMoved\n"); 260 261 MBCPosition pos = [self eventToPosition:event]; 262 float pxa = fabs(pos[0]); 263 float pza = fabs(pos[2]); 264 NSCursor * cursor = fArrowCursor; 265 266 if (pxa > kBoardRadius || pza > kBoardRadius) 267 if (pxa < kBoardRadius+kBorderWidth+.1f 268 && pza < kBoardRadius+kBorderWidth+.1f) 269 cursor = fHandCursor; 270 [cursor set]; 271} 272 273- (void) mouseDown:(NSEvent *)event 274{ 275 if (MBCDebug::LogMouse()) 276 fprintf(stderr, "mouseDown\n"); 277 278 MBCSquare previouslyPicked = fPickedSquare; 279 280 NSPoint p = [event locationInWindow]; 281 NSPoint l = [self convertPoint:p fromView:nil]; 282 283 // 284 // On mousedown, we determine the point on the board surface that 285 // corresponds to the mouse location by the frontmost Z value, but 286 // then pretend that the click happened at board surface level. Weirdly 287 // enough, this seems to give the most natural feeling mouse behavior. 288 // 289 [[self openGLContext] makeCurrentContext]; 290 MBCPosition pos = [self mouseToPosition:l]; 291 fSelectedDest = [self positionToSquareOrRegion:&pos]; 292 switch (fSelectedDest) { 293 case kInvalidSquare: 294 return; 295 case kWhitePromoSquare: 296 case kBlackPromoSquare: 297 return; 298 case kBorderRegion: 299 fInBoardManipulation= true; 300 fOrigMouse = l; 301 fCurMouse = l; 302 fRawAzimuth = fAzimuth; 303 [NSCursor hide]; 304 [NSEvent startPeriodicEventsAfterDelay: 0.008f withPeriod: 0.008f]; 305 break; 306 default: 307 if (!fWantMouse || fInAnimation || pos[1] < 0.1) 308 return; 309 if (fSelectedDest == fPickedSquare) { 310 // 311 // When trying to move a large piece by clicking the destination, the piece 312 // sometimes can hide the destination. We try again by ignoring y. 313 // 314 MBCPosition altPos = [self mouseToPositionIgnoringY:l]; 315 MBCSquare altDest = [self positionToSquareOrRegion:&altPos]; 316 if (altDest < kSyntheticSquare) { 317 pos = altPos; 318 fSelectedDest = altDest; 319 } 320 } 321 // 322 // Let interactive player decide whether we hit one of their pieces 323 // 324 [fInteractive startSelection:fSelectedDest]; 325 if (!fSelectedPiece) // Apparently not... 326 return; 327 break; 328 } 329 pos[1] = 0.0f; 330 gettimeofday(&fLastRedraw, NULL); 331 fLastSelectedPos = pos; 332 [self drawNow]; 333 334 NSDate * whenever = [NSDate distantFuture]; 335 for (bool goOn = true; goOn; ) { 336 event = 337 [NSApp nextEventMatchingMask: 338 NSPeriodicMask|NSLeftMouseUpMask|NSLeftMouseDraggedMask 339 untilDate:whenever inMode:NSEventTrackingRunLoopMode 340 dequeue:YES]; 341 switch ([event type]) { 342 case NSPeriodic: 343 case NSLeftMouseDragged: 344 [self dragAndRedraw:event forceRedraw:NO]; 345 break; 346 case NSLeftMouseUp: { 347 [self dragAndRedraw:event forceRedraw:YES]; 348 [fController setAngle:fElevation spin:fAzimuth]; 349 [fInteractive endSelection:fSelectedDest animate:NO]; 350 if (fPickedSquare == previouslyPicked) 351 fPickedSquare = kInvalidSquare; // Toggle pick 352 goOn = false; 353 if (fInBoardManipulation) { 354 fInBoardManipulation = false; 355 [NSCursor unhide]; 356 [NSEvent stopPeriodicEvents]; 357 } 358 break; } 359 default: 360 /* Ignore any other kind of event. */ 361 break; 362 } 363 } 364 fSelectedDest = kInvalidSquare; 365} 366 367- (void) mouseUp:(NSEvent *)event 368{ 369 if (MBCDebug::LogMouse()) 370 fprintf(stderr, "mouseUp\n"); 371 372 if (!fWantMouse || fInAnimation) 373 return; 374 375 MBCPiece promo; 376 if (fSelectedDest == kWhitePromoSquare) { 377 promo = [fBoard defaultPromotion:YES]; 378 } else if (fSelectedDest == kBlackPromoSquare) { 379 promo = [fBoard defaultPromotion:NO]; 380 } else if (fPickedSquare != kInvalidSquare) { 381 [fInteractive startSelection:fPickedSquare]; 382 [fInteractive endSelection:fSelectedDest animate:YES]; 383 384 return; 385 } else 386 return; 387 388 switch (promo) { 389 case QUEEN: 390 if (fVariant == kVarSuicide) 391 promo = KING; // King promotion is very popular in suicide 392 else 393 promo = KNIGHT; // Second most useful 394 break; 395 case KING: // Suicide only 396 promo = KNIGHT; 397 break; 398 case KNIGHT: 399 promo = ROOK; 400 break; 401 case ROOK: 402 promo = BISHOP; 403 break; 404 case BISHOP: 405 promo = QUEEN; 406 break; 407 } 408 [fBoard setDefaultPromotion:promo 409 for:fSelectedDest == kWhitePromoSquare]; 410 411 [self setNeedsDisplay:YES]; 412} 413 414- (void) dragAndRedraw:(NSEvent *)event forceRedraw:(BOOL)force 415{ 416 if ([event type] != NSPeriodic) { 417 NSPoint p = [event locationInWindow]; 418 NSPoint l = [self convertPoint:p fromView:nil]; 419 fCurMouse = l; 420 421 if (!fInAnimation) { 422 // 423 // On drag, we can use a fairly fast interpolation to determine 424 // the 3D coordinate using the y where we touched the piece 425 // 426 [[self openGLContext] makeCurrentContext]; 427 l = [self convertPointToBacking:l]; 428 MBCUnProjector unproj(l.x, l.y); 429 430 fSelectedPos = unproj.UnProject(0.0f); 431 [self snapToSquare:&fSelectedPos]; 432 } 433 } 434 struct timeval now; 435 gettimeofday(&now, NULL); 436 NSTimeInterval dt = 437 now.tv_sec - fLastRedraw.tv_sec 438 + 0.000001 * (now.tv_usec - fLastRedraw.tv_usec); 439 440 const float kTiltSpeed = 0.50f; 441 const float kSpinSpeed = 0.50f; 442 const float kThreshold = 10.0f; 443 const float kAzimuthRound = 5.0f; 444 445 if (force) { 446 [self setNeedsDisplay:YES]; 447 } else if (fSelectedDest == kBorderRegion) { 448 float dx = fCurMouse.x-fOrigMouse.x; 449 float dy = fCurMouse.y-fOrigMouse.y; 450#if FULL_DIAGONAL_MOVES 451 bool mustDraw = false; 452 if (fabs(dx) > kThreshold) { 453 fRawAzimuth += dx*dt*kSpinSpeed; 454 fRawAzimuth = fmod(fRawAzimuth+360.0f, 360.0f); 455 float angle = fmod((fAzimuth = fRawAzimuth), 90.0f); 456 if (angle < kAzimuthRound) 457 fAzimuth -= angle; 458 else if (angle > 90.0f-kAzimuthRound) 459 fAzimuth += 90.0f-angle; 460 mustDraw = true; 461 } 462 if (fabs(dy) > kThreshold) { 463 fElevation -= dy*dt*kTiltSpeed; 464 fElevation = max(kMinElevation, min(kMaxElevation, fElevation)); 465 mustDraw = true; 466 } 467 if (mustDraw) { 468 fNeedPerspective= true; 469 fLastRedraw = now; 470 [self drawNow]; 471 } 472#else 473 if (fabs(dx) > fabs(dy) && fabs(dx) > kThreshold) { 474 fRawAzimuth += dx*dt*kSpinSpeed; 475 fRawAzimuth = fmod(fRawAzimuth+360.0f, 360.0f); 476 float angle = fmod((fAzimuth = fRawAzimuth), 90.0f); 477 if (angle < kAzimuthRound) 478 fAzimuth -= angle; 479 else if (angle > 90.0f-kAzimuthRound) 480 fAzimuth += 90.0f-angle; 481 fNeedPerspective= true; 482 fLastRedraw = now; 483 [self drawNow]; 484 } else if (fabs(dy) > kThreshold) { 485 fElevation -= dy*dt*kTiltSpeed; 486 fElevation = 487 max(kMinElevation, min(kMaxElevation, fElevation)); 488 fNeedPerspective= true; 489 fLastRedraw = now; 490 [self drawNow]; 491 } 492#endif 493 } else { 494 MBCPosition delta = fSelectedPos-fLastSelectedPos; 495 GLfloat d2 = delta[0]*delta[0]+delta[2]*delta[2]; 496 497 if (d2 > 25.0f || (d2 > 1.0f && dt > 0.02)) { 498 fSelectedDest = [self positionToSquare:&fSelectedPos]; 499 fLastRedraw = now; 500 [self drawNow]; 501 } 502 } 503} 504 505- (BOOL)acceptsFirstResponder 506{ 507 return YES; 508} 509 510- (void)keyDown:(NSEvent *)event 511{ 512 NSString * chr = [event characters]; 513 if ([chr length] != 1) 514 return; // Ignore 515 switch (char ch = [chr characterAtIndex:0]) { 516 case 'A': 517 case 'B': 518 case 'C': 519 case 'D': 520 case 'E': 521 case 'F': 522 case 'G': 523 case 'H': 524 ch = tolower(ch); 525 // Fall through 526 case 'b': 527 case 'a': 528 case 'c': 529 case 'd': 530 case 'e': 531 case 'f': 532 case 'g': 533 case 'h': 534 case '=': 535 if (ch == 'b' && fKeyBuffer == '=') 536 goto promotion_piece; 537 if (fWantMouse) 538 fKeyBuffer = ch; 539 else 540 NSBeep(); 541 break; 542 case '1': 543 case '2': 544 case '3': 545 case '4': 546 case '5': 547 case '6': 548 case '7': 549 case '8': 550 if (fWantMouse && isalpha(fKeyBuffer)) { 551 MBCSquare sq = Square(fKeyBuffer, ch-'0'); 552 if (fPickedSquare != kInvalidSquare) { 553 [fInteractive startSelection:fPickedSquare]; 554 [fInteractive endSelection:sq animate:YES]; 555 } else { 556 [fInteractive startSelection:sq]; 557 [self clickPiece]; 558 } 559 } else 560 NSBeep(); 561 fKeyBuffer = 0; 562 break; 563 case '\177': // Delete 564 case '\r': 565 if (fKeyBuffer) { 566 fKeyBuffer = 0; 567 } else if (fPickedSquare != kInvalidSquare) { 568 [fInteractive endSelection:fPickedSquare animate:NO]; 569 fPickedSquare = kInvalidSquare; 570 [self setNeedsDisplay:YES]; 571 } 572 break; 573 case 'K': 574 if (fVariant != kVarSuicide) { 575 NSBeep(); 576 break; 577 } 578 // Fall through 579 case 'Q': 580 case 'N': 581 case 'R': 582 ch = tolower(ch); 583 // Fall through 584 case 'k': 585 if (fVariant != kVarSuicide) { 586 NSBeep(); 587 break; 588 } 589 // Fall through 590 case 'q': 591 case 'n': 592 case 'r': 593 promotion_piece: 594 if (fKeyBuffer == '=') { 595 const char * kPiece = " kqbnr"; 596 [fBoard setDefaultPromotion:strchr(kPiece, ch)-kPiece for:YES]; 597 [fBoard setDefaultPromotion:strchr(kPiece, ch)-kPiece for:NO]; 598 [self setNeedsDisplay:YES]; 599 } else { 600 NSBeep(); 601 } 602 fKeyBuffer = 0; 603 break; 604 default: 605 // 606 // Propagate ESC etc. 607 // 608 [super keyDown:event]; 609 break; 610 } 611} 612 613@end 614 615// Local Variables: 616// mode:ObjC 617// End: 618