1 /* 2 * Copyright 2001-2013, Haiku, Inc. 3 * Copyright 2003-2004 Kian Duffy, myob@users.sourceforge.net 4 * Parts Copyright 1998-1999 Kazuho Okui and Takashi Murai. 5 * All rights reserved. Distributed under the terms of the MIT license. 6 * 7 * Authors: 8 * Stefano Ceccherini, stefano.ceccherini@gmail.com 9 * Kian Duffy, myob@users.sourceforge.net 10 * Y.Hayakawa, hida@sawada.riec.tohoku.ac.jp 11 * Ingo Weinhold, ingo_weinhold@gmx.de 12 * Clemens Zeidler, haiku@Clemens-Zeidler.de 13 * Siarzhuk Zharski, zharik@gmx.li 14 */ 15 16 17 #include "TermViewStates.h" 18 19 #include <stdio.h> 20 #include <stdlib.h> 21 #include <sys/stat.h> 22 23 #include <Catalog.h> 24 #include <Clipboard.h> 25 #include <Cursor.h> 26 #include <LayoutBuilder.h> 27 #include <MessageRunner.h> 28 #include <PopUpMenu.h> 29 #include <ScrollBar.h> 30 #include <UTF8.h> 31 #include <Window.h> 32 33 #include <Array.h> 34 35 #include "ActiveProcessInfo.h" 36 #include "Shell.h" 37 #include "TermConst.h" 38 #include "TerminalBuffer.h" 39 #include "VTkeymap.h" 40 #include "VTKeyTbl.h" 41 42 43 #undef B_TRANSLATION_CONTEXT 44 #define B_TRANSLATION_CONTEXT "Terminal TermView" 45 46 47 // selection granularity 48 enum { 49 SELECT_CHARS, 50 SELECT_WORDS, 51 SELECT_LINES 52 }; 53 54 static const uint32 kAutoScroll = 'AScr'; 55 56 static const uint32 kMessageOpenLink = 'OLnk'; 57 static const uint32 kMessageCopyLink = 'CLnk'; 58 static const uint32 kMessageCopyAbsolutePath = 'CAbs'; 59 static const uint32 kMessageMenuClosed = 'MClo'; 60 61 62 static const char* const kKnownURLProtocols = "http:https:ftp:mailto"; 63 64 65 // #pragma mark - State 66 67 68 TermView::State::State(TermView* view) 69 : 70 fView(view) 71 { 72 } 73 74 75 TermView::State::~State() 76 { 77 } 78 79 80 void 81 TermView::State::Entered() 82 { 83 } 84 85 86 void 87 TermView::State::Exited() 88 { 89 } 90 91 92 bool 93 TermView::State::MessageReceived(BMessage* message) 94 { 95 return false; 96 } 97 98 99 void 100 TermView::State::ModifiersChanged(int32 oldModifiers, int32 modifiers) 101 { 102 } 103 104 105 void 106 TermView::State::KeyDown(const char* bytes, int32 numBytes) 107 { 108 } 109 110 111 void 112 TermView::State::MouseDown(BPoint where, int32 buttons, int32 modifiers) 113 { 114 } 115 116 117 void 118 TermView::State::MouseMoved(BPoint where, uint32 transit, 119 const BMessage* message, int32 modifiers) 120 { 121 } 122 123 124 void 125 TermView::State::MouseUp(BPoint where, int32 buttons) 126 { 127 } 128 129 130 void 131 TermView::State::WindowActivated(bool active) 132 { 133 } 134 135 136 void 137 TermView::State::VisibleTextBufferChanged() 138 { 139 } 140 141 142 // #pragma mark - StandardBaseState 143 144 145 TermView::StandardBaseState::StandardBaseState(TermView* view) 146 : 147 State(view) 148 { 149 } 150 151 152 bool 153 TermView::StandardBaseState::_StandardMouseMoved(BPoint where, int32 modifiers) 154 { 155 if (!fView->fReportAnyMouseEvent && !fView->fReportButtonMouseEvent) 156 return false; 157 158 TermPos clickPos = fView->_ConvertToTerminal(where); 159 160 if (fView->fReportButtonMouseEvent) { 161 if (fView->fPrevPos.x != clickPos.x 162 || fView->fPrevPos.y != clickPos.y) { 163 fView->_SendMouseEvent(fView->fMouseButtons, modifiers, 164 clickPos.x, clickPos.y, true); 165 } 166 fView->fPrevPos = clickPos; 167 } else { 168 fView->_SendMouseEvent(fView->fMouseButtons, modifiers, clickPos.x, 169 clickPos.y, true); 170 } 171 172 return true; 173 } 174 175 176 // #pragma mark - DefaultState 177 178 179 TermView::DefaultState::DefaultState(TermView* view) 180 : 181 StandardBaseState(view) 182 { 183 } 184 185 186 void 187 TermView::DefaultState::ModifiersChanged(int32 oldModifiers, int32 modifiers) 188 { 189 _CheckEnterHyperLinkState(modifiers); 190 } 191 192 193 void 194 TermView::DefaultState::KeyDown(const char* bytes, int32 numBytes) 195 { 196 int32 key; 197 int32 mod; 198 int32 rawChar; 199 BMessage* currentMessage = fView->Looper()->CurrentMessage(); 200 if (currentMessage == NULL) 201 return; 202 203 currentMessage->FindInt32("modifiers", &mod); 204 currentMessage->FindInt32("key", &key); 205 currentMessage->FindInt32("raw_char", &rawChar); 206 207 fView->_ActivateCursor(true); 208 209 // handle multi-byte chars 210 if (numBytes > 1) { 211 if (fView->fEncoding != M_UTF8) { 212 char destBuffer[16]; 213 int32 destLen = sizeof(destBuffer); 214 int32 state = 0; 215 convert_from_utf8(fView->fEncoding, bytes, &numBytes, destBuffer, 216 &destLen, &state, '?'); 217 fView->_ScrollTo(0, true); 218 fView->fShell->Write(destBuffer, destLen); 219 return; 220 } 221 222 fView->_ScrollTo(0, true); 223 fView->fShell->Write(bytes, numBytes); 224 return; 225 } 226 227 // Terminal filters RET, ENTER, F1...F12, and ARROW key code. 228 const char *toWrite = NULL; 229 230 switch (*bytes) { 231 case B_RETURN: 232 if (rawChar == B_RETURN) 233 toWrite = "\r"; 234 break; 235 236 case B_DELETE: 237 toWrite = DELETE_KEY_CODE; 238 break; 239 240 case B_BACKSPACE: 241 // Translate only the actual backspace key to the backspace 242 // code. CTRL-H shall just be echoed. 243 if (!((mod & B_CONTROL_KEY) && rawChar == 'h')) 244 toWrite = BACKSPACE_KEY_CODE; 245 break; 246 247 case B_LEFT_ARROW: 248 if (rawChar == B_LEFT_ARROW) { 249 if ((mod & B_SHIFT_KEY) != 0) { 250 if (fView->fListener != NULL) 251 fView->fListener->PreviousTermView(fView); 252 return; 253 } 254 if ((mod & B_CONTROL_KEY) || (mod & B_COMMAND_KEY)) 255 toWrite = CTRL_LEFT_ARROW_KEY_CODE; 256 else 257 toWrite = LEFT_ARROW_KEY_CODE; 258 } 259 break; 260 261 case B_RIGHT_ARROW: 262 if (rawChar == B_RIGHT_ARROW) { 263 if ((mod & B_SHIFT_KEY) != 0) { 264 if (fView->fListener != NULL) 265 fView->fListener->NextTermView(fView); 266 return; 267 } 268 if ((mod & B_CONTROL_KEY) || (mod & B_COMMAND_KEY)) 269 toWrite = CTRL_RIGHT_ARROW_KEY_CODE; 270 else 271 toWrite = RIGHT_ARROW_KEY_CODE; 272 } 273 break; 274 275 case B_UP_ARROW: 276 if (mod & B_SHIFT_KEY) { 277 fView->_ScrollTo(fView->fScrollOffset - fView->fFontHeight, 278 true); 279 return; 280 } 281 if (rawChar == B_UP_ARROW) { 282 if (mod & B_CONTROL_KEY) 283 toWrite = CTRL_UP_ARROW_KEY_CODE; 284 else 285 toWrite = UP_ARROW_KEY_CODE; 286 } 287 break; 288 289 case B_DOWN_ARROW: 290 if (mod & B_SHIFT_KEY) { 291 fView->_ScrollTo(fView->fScrollOffset + fView->fFontHeight, 292 true); 293 return; 294 } 295 296 if (rawChar == B_DOWN_ARROW) { 297 if (mod & B_CONTROL_KEY) 298 toWrite = CTRL_DOWN_ARROW_KEY_CODE; 299 else 300 toWrite = DOWN_ARROW_KEY_CODE; 301 } 302 break; 303 304 case B_INSERT: 305 if (rawChar == B_INSERT) 306 toWrite = INSERT_KEY_CODE; 307 break; 308 309 case B_HOME: 310 if (rawChar == B_HOME) 311 toWrite = HOME_KEY_CODE; 312 break; 313 314 case B_END: 315 if (rawChar == B_END) 316 toWrite = END_KEY_CODE; 317 break; 318 319 case B_PAGE_UP: 320 if (mod & B_SHIFT_KEY) { 321 fView->_ScrollTo( 322 fView->fScrollOffset - fView->fFontHeight * fView->fRows, 323 true); 324 return; 325 } 326 if (rawChar == B_PAGE_UP) 327 toWrite = PAGE_UP_KEY_CODE; 328 break; 329 330 case B_PAGE_DOWN: 331 if (mod & B_SHIFT_KEY) { 332 fView->_ScrollTo( 333 fView->fScrollOffset + fView->fFontHeight * fView->fRows, 334 true); 335 return; 336 } 337 if (rawChar == B_PAGE_DOWN) 338 toWrite = PAGE_DOWN_KEY_CODE; 339 break; 340 341 case B_FUNCTION_KEY: 342 for (int32 i = 0; i < 12; i++) { 343 if (key == function_keycode_table[i]) { 344 toWrite = function_key_char_table[i]; 345 break; 346 } 347 } 348 break; 349 } 350 351 // If the above code proposed an alternative string to write, we get it's 352 // length. Otherwise we write exactly the bytes passed to this method. 353 size_t toWriteLen; 354 if (toWrite != NULL) { 355 toWriteLen = strlen(toWrite); 356 } else { 357 toWrite = bytes; 358 toWriteLen = numBytes; 359 } 360 361 fView->_ScrollTo(0, true); 362 fView->fShell->Write(toWrite, toWriteLen); 363 } 364 365 366 void 367 TermView::DefaultState::MouseDown(BPoint where, int32 buttons, int32 modifiers) 368 { 369 if (fView->fReportAnyMouseEvent || fView->fReportButtonMouseEvent 370 || fView->fReportNormalMouseEvent || fView->fReportX10MouseEvent) { 371 TermPos clickPos = fView->_ConvertToTerminal(where); 372 fView->_SendMouseEvent(buttons, modifiers, clickPos.x, clickPos.y, 373 false); 374 return; 375 } 376 377 // paste button 378 if ((buttons & (B_SECONDARY_MOUSE_BUTTON | B_TERTIARY_MOUSE_BUTTON)) != 0) { 379 fView->Paste(fView->fMouseClipboard); 380 return; 381 } 382 383 // select region 384 if (buttons == B_PRIMARY_MOUSE_BUTTON) { 385 fView->fSelectState->Prepare(where, modifiers); 386 fView->_NextState(fView->fSelectState); 387 } 388 } 389 390 391 void 392 TermView::DefaultState::MouseMoved(BPoint where, uint32 transit, 393 const BMessage* dragMessage, int32 modifiers) 394 { 395 if (_CheckEnterHyperLinkState(modifiers)) 396 return; 397 398 _StandardMouseMoved(where, modifiers); 399 } 400 401 402 void 403 TermView::DefaultState::WindowActivated(bool active) 404 { 405 if (active) 406 _CheckEnterHyperLinkState(fView->fModifiers); 407 } 408 409 410 bool 411 TermView::DefaultState::_CheckEnterHyperLinkState(int32 modifiers) 412 { 413 if ((modifiers & B_COMMAND_KEY) != 0 && fView->Window()->IsActive()) { 414 fView->_NextState(fView->fHyperLinkState); 415 return true; 416 } 417 418 return false; 419 } 420 421 422 // #pragma mark - SelectState 423 424 425 TermView::SelectState::SelectState(TermView* view) 426 : 427 StandardBaseState(view), 428 fSelectGranularity(SELECT_CHARS), 429 fCheckMouseTracking(false), 430 fMouseTracking(false) 431 { 432 } 433 434 435 void 436 TermView::SelectState::Prepare(BPoint where, int32 modifiers) 437 { 438 int32 clicks; 439 fView->Window()->CurrentMessage()->FindInt32("clicks", &clicks); 440 441 if (fView->_HasSelection()) { 442 TermPos inPos = fView->_ConvertToTerminal(where); 443 if (fView->fSelection.RangeContains(inPos)) { 444 if (modifiers & B_CONTROL_KEY) { 445 BPoint p; 446 uint32 bt; 447 do { 448 fView->GetMouse(&p, &bt); 449 450 if (bt == 0) { 451 fView->_Deselect(); 452 return; 453 } 454 455 snooze(40000); 456 457 } while (abs((int)(where.x - p.x)) < 4 458 && abs((int)(where.y - p.y)) < 4); 459 460 fView->InitiateDrag(); 461 return; 462 } 463 } 464 } 465 466 // If mouse has moved too much, disable double/triple click. 467 if (fView->_MouseDistanceSinceLastClick(where) > 8) 468 clicks = 1; 469 470 fView->SetMouseEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS, 471 B_NO_POINTER_HISTORY | B_LOCK_WINDOW_FOCUS); 472 473 TermPos clickPos = fView->_ConvertToTerminal(where); 474 475 if (modifiers & B_SHIFT_KEY) { 476 fView->fInitialSelectionStart = clickPos; 477 fView->fInitialSelectionEnd = clickPos; 478 fView->_ExtendSelection(fView->fInitialSelectionStart, true, false); 479 } else { 480 fView->_Deselect(); 481 fView->fInitialSelectionStart = clickPos; 482 fView->fInitialSelectionEnd = clickPos; 483 } 484 485 // If clicks larger than 3, reset mouse click counter. 486 clicks = (clicks - 1) % 3 + 1; 487 488 switch (clicks) { 489 case 1: 490 fCheckMouseTracking = true; 491 fSelectGranularity = SELECT_CHARS; 492 break; 493 494 case 2: 495 fView->_SelectWord(where, (modifiers & B_SHIFT_KEY) != 0, false); 496 fMouseTracking = true; 497 fSelectGranularity = SELECT_WORDS; 498 break; 499 500 case 3: 501 fView->_SelectLine(where, (modifiers & B_SHIFT_KEY) != 0, false); 502 fMouseTracking = true; 503 fSelectGranularity = SELECT_LINES; 504 break; 505 } 506 } 507 508 509 bool 510 TermView::SelectState::MessageReceived(BMessage* message) 511 { 512 if (message->what == kAutoScroll) { 513 _AutoScrollUpdate(); 514 return true; 515 } 516 517 return false; 518 } 519 520 521 void 522 TermView::SelectState::MouseMoved(BPoint where, uint32 transit, 523 const BMessage* message, int32 modifiers) 524 { 525 if (_StandardMouseMoved(where, modifiers)) 526 return; 527 528 if (fCheckMouseTracking) { 529 if (fView->_MouseDistanceSinceLastClick(where) > 9) 530 fMouseTracking = true; 531 } 532 if (!fMouseTracking) 533 return; 534 535 bool doAutoScroll = false; 536 537 if (where.y < 0) { 538 doAutoScroll = true; 539 fView->fAutoScrollSpeed = where.y; 540 where.x = 0; 541 where.y = 0; 542 } 543 544 BRect bounds(fView->Bounds()); 545 if (where.y > bounds.bottom) { 546 doAutoScroll = true; 547 fView->fAutoScrollSpeed = where.y - bounds.bottom; 548 where.x = bounds.right; 549 where.y = bounds.bottom; 550 } 551 552 if (doAutoScroll) { 553 if (fView->fAutoScrollRunner == NULL) { 554 BMessage message(kAutoScroll); 555 fView->fAutoScrollRunner = new (std::nothrow) BMessageRunner( 556 BMessenger(fView), &message, 10000); 557 } 558 } else { 559 delete fView->fAutoScrollRunner; 560 fView->fAutoScrollRunner = NULL; 561 } 562 563 switch (fSelectGranularity) { 564 case SELECT_CHARS: 565 { 566 // If we just start selecting, we first select the initially 567 // hit char, so that we get a proper initial selection -- the char 568 // in question, which will thus always be selected, regardless of 569 // whether selecting forward or backward. 570 if (fView->fInitialSelectionStart == fView->fInitialSelectionEnd) { 571 fView->_Select(fView->fInitialSelectionStart, 572 fView->fInitialSelectionEnd, true, true); 573 } 574 575 fView->_ExtendSelection(fView->_ConvertToTerminal(where), true, 576 true); 577 break; 578 } 579 case SELECT_WORDS: 580 fView->_SelectWord(where, true, true); 581 break; 582 case SELECT_LINES: 583 fView->_SelectLine(where, true, true); 584 break; 585 } 586 } 587 588 589 void 590 TermView::SelectState::MouseUp(BPoint where, int32 buttons) 591 { 592 fCheckMouseTracking = false; 593 fMouseTracking = false; 594 595 if (fView->fAutoScrollRunner != NULL) { 596 delete fView->fAutoScrollRunner; 597 fView->fAutoScrollRunner = NULL; 598 } 599 600 // When releasing the first mouse button, we copy the selected text to the 601 // clipboard. 602 603 if (fView->fReportAnyMouseEvent || fView->fReportButtonMouseEvent 604 || fView->fReportNormalMouseEvent) { 605 TermPos clickPos = fView->_ConvertToTerminal(where); 606 fView->_SendMouseEvent(0, 0, clickPos.x, clickPos.y, false); 607 } else if ((buttons & B_PRIMARY_MOUSE_BUTTON) == 0 608 && (fView->fMouseButtons & B_PRIMARY_MOUSE_BUTTON) != 0) { 609 fView->Copy(fView->fMouseClipboard); 610 } 611 612 fView->_NextState(fView->fDefaultState); 613 } 614 615 616 void 617 TermView::SelectState::_AutoScrollUpdate() 618 { 619 if (fMouseTracking && fView->fAutoScrollRunner != NULL 620 && fView->fScrollBar != NULL) { 621 float value = fView->fScrollBar->Value(); 622 fView->_ScrollTo(value + fView->fAutoScrollSpeed, true); 623 if (fView->fAutoScrollSpeed < 0) { 624 fView->_ExtendSelection( 625 fView->_ConvertToTerminal(BPoint(0, 0)), true, true); 626 } else { 627 fView->_ExtendSelection( 628 fView->_ConvertToTerminal(fView->Bounds().RightBottom()), true, 629 true); 630 } 631 } 632 } 633 634 635 // #pragma mark - HyperLinkState 636 637 638 TermView::HyperLinkState::HyperLinkState(TermView* view) 639 : 640 State(view), 641 fURLCharClassifier(kURLAdditionalWordCharacters), 642 fPathComponentCharClassifier( 643 BString(kDefaultAdditionalWordCharacters).RemoveFirst("/")), 644 fCurrentDirectory(), 645 fHighlight(), 646 fHighlightActive(false) 647 { 648 fHighlight.SetHighlighter(this); 649 } 650 651 652 void 653 TermView::HyperLinkState::Entered() 654 { 655 ActiveProcessInfo activeProcessInfo; 656 if (fView->GetActiveProcessInfo(activeProcessInfo)) 657 fCurrentDirectory = activeProcessInfo.CurrentDirectory(); 658 else 659 fCurrentDirectory.Truncate(0); 660 661 _UpdateHighlight(); 662 } 663 664 665 void 666 TermView::HyperLinkState::Exited() 667 { 668 _DeactivateHighlight(); 669 } 670 671 672 void 673 TermView::HyperLinkState::ModifiersChanged(int32 oldModifiers, int32 modifiers) 674 { 675 if ((modifiers & B_COMMAND_KEY) == 0) 676 fView->_NextState(fView->fDefaultState); 677 else 678 _UpdateHighlight(); 679 } 680 681 682 void 683 TermView::HyperLinkState::MouseDown(BPoint where, int32 buttons, 684 int32 modifiers) 685 { 686 TermPos start; 687 TermPos end; 688 HyperLink link; 689 690 bool pathPrefixOnly = (modifiers & B_SHIFT_KEY) != 0; 691 if (!_GetHyperLinkAt(where, pathPrefixOnly, link, start, end)) 692 return; 693 694 if ((buttons & B_PRIMARY_MOUSE_BUTTON) != 0) { 695 link.Open(); 696 } else if ((buttons & B_SECONDARY_MOUSE_BUTTON) != 0) { 697 fView->fHyperLinkMenuState->Prepare(where, link); 698 fView->_NextState(fView->fHyperLinkMenuState); 699 } 700 } 701 702 703 void 704 TermView::HyperLinkState::MouseMoved(BPoint where, uint32 transit, 705 const BMessage* message, int32 modifiers) 706 { 707 _UpdateHighlight(where, modifiers); 708 } 709 710 711 void 712 TermView::HyperLinkState::WindowActivated(bool active) 713 { 714 if (!active) 715 fView->_NextState(fView->fDefaultState); 716 } 717 718 719 void 720 TermView::HyperLinkState::VisibleTextBufferChanged() 721 { 722 _UpdateHighlight(); 723 } 724 725 726 rgb_color 727 TermView::HyperLinkState::ForegroundColor() 728 { 729 return make_color(0, 0, 255); 730 } 731 732 733 rgb_color 734 TermView::HyperLinkState::BackgroundColor() 735 { 736 return fView->fTextBackColor; 737 } 738 739 740 uint32 741 TermView::HyperLinkState::AdjustTextAttributes(uint32 attributes) 742 { 743 return attributes | UNDERLINE; 744 } 745 746 747 bool 748 TermView::HyperLinkState::_GetHyperLinkAt(BPoint where, bool pathPrefixOnly, 749 HyperLink& _link, TermPos& _start, TermPos& _end) 750 { 751 TerminalBuffer* textBuffer = fView->fTextBuffer; 752 BAutolock textBufferLocker(textBuffer); 753 754 TermPos pos = fView->_ConvertToTerminal(where); 755 756 // try to get a URL first 757 BString text; 758 if (!textBuffer->FindWord(pos, &fURLCharClassifier, false, _start, _end)) 759 return false; 760 761 text.Truncate(0); 762 textBuffer->GetStringFromRegion(text, _start, _end); 763 text.Trim(); 764 765 // We're only happy, if it has a protocol part which we know. 766 int32 colonIndex = text.FindFirst(':'); 767 if (colonIndex >= 0) { 768 BString protocol(text, colonIndex); 769 if (strstr(kKnownURLProtocols, protocol) != NULL) { 770 _link = HyperLink(text, HyperLink::TYPE_URL); 771 return true; 772 } 773 } 774 775 // no obvious URL -- try file name 776 if (!textBuffer->FindWord(pos, fView->fCharClassifier, false, _start, _end)) 777 return false; 778 779 // In path-prefix-only mode we determine the end position anew by omitting 780 // the '/' in the allowed word chars. 781 if (pathPrefixOnly) { 782 TermPos componentStart; 783 TermPos componentEnd; 784 if (textBuffer->FindWord(pos, &fPathComponentCharClassifier, false, 785 componentStart, componentEnd)) { 786 _end = componentEnd; 787 } else { 788 // That means pos points to a '/'. We simply use the previous 789 // position. 790 _end = pos; 791 if (_start == _end) { 792 // Well, must be just "/". Advance to the next position. 793 if (!textBuffer->NextLinePos(_end, false)) 794 return false; 795 } 796 } 797 } 798 799 text.Truncate(0); 800 textBuffer->GetStringFromRegion(text, _start, _end); 801 text.Trim(); 802 if (text.IsEmpty()) 803 return false; 804 805 // Collect a list of colons in the string and their respective positions in 806 // the text buffer. We do this up-front so we can unlock the text buffer 807 // while we're doing all the entry existence tests. 808 typedef Array<CharPosition> ColonList; 809 ColonList colonPositions; 810 TermPos searchPos = _start; 811 for (int32 index = 0; (index = text.FindFirst(':', index)) >= 0;) { 812 TermPos foundStart; 813 TermPos foundEnd; 814 if (!textBuffer->Find(":", searchPos, true, true, false, foundStart, 815 foundEnd)) { 816 return false; 817 } 818 819 CharPosition colonPosition; 820 colonPosition.index = index; 821 colonPosition.position = foundStart; 822 if (!colonPositions.Add(colonPosition)) 823 return false; 824 825 index++; 826 searchPos = foundEnd; 827 } 828 829 textBufferLocker.Unlock(); 830 831 // Since we also want to consider ':' a potential path delimiter, in two 832 // nested loops we chop off components from the beginning respective the 833 // end. 834 BString originalText = text; 835 TermPos originalStart = _start; 836 TermPos originalEnd = _end; 837 838 int32 colonCount = colonPositions.Count(); 839 for (int32 startColonIndex = -1; startColonIndex < colonCount; 840 startColonIndex++) { 841 int32 startIndex; 842 if (startColonIndex < 0) { 843 startIndex = 0; 844 _start = originalStart; 845 } else { 846 startIndex = colonPositions[startColonIndex].index + 1; 847 _start = colonPositions[startColonIndex].position; 848 if (_start >= pos) 849 break; 850 _start.x++; 851 // Note: This is potentially a non-normalized position (i.e. 852 // the end of a soft-wrapped line). While not that nice, it 853 // works anyway. 854 } 855 856 for (int32 endColonIndex = colonCount; endColonIndex > startColonIndex; 857 endColonIndex--) { 858 int32 endIndex; 859 if (endColonIndex == colonCount) { 860 endIndex = originalText.Length(); 861 _end = originalEnd; 862 } else { 863 endIndex = colonPositions[endColonIndex].index; 864 _end = colonPositions[endColonIndex].position; 865 if (_end <= pos) 866 break; 867 } 868 869 originalText.CopyInto(text, startIndex, endIndex - startIndex); 870 if (text.IsEmpty()) 871 continue; 872 873 // check, whether the file exists 874 BString actualPath; 875 if (_EntryExists(text, actualPath)) { 876 _link = HyperLink(text, actualPath, HyperLink::TYPE_PATH); 877 return true; 878 } 879 880 // As such this isn't an existing path. We also want to recognize: 881 // * "<path>:<line>" 882 // * "<path>:<line>:<column>" 883 884 BString path = text; 885 886 for (int32 i = 0; i < 2; i++) { 887 int32 colonIndex = path.FindLast(':'); 888 if (colonIndex <= 0 || colonIndex == path.Length() - 1) 889 break; 890 891 char* numberEnd; 892 strtol(path.String() + colonIndex + 1, &numberEnd, 0); 893 if (*numberEnd != '\0') 894 break; 895 896 path.Truncate(colonIndex); 897 if (_EntryExists(path, actualPath)) { 898 BString address = path == actualPath 899 ? text : BString(fCurrentDirectory) << '/' << text; 900 _link = HyperLink(text, address, 901 i == 0 902 ? HyperLink::TYPE_PATH_WITH_LINE 903 : HyperLink::TYPE_PATH_WITH_LINE_AND_COLUMN); 904 return true; 905 } 906 } 907 } 908 } 909 910 return false; 911 } 912 913 914 bool 915 TermView::HyperLinkState::_EntryExists(const BString& path, 916 BString& _actualPath) const 917 { 918 if (path.IsEmpty()) 919 return false; 920 921 if (path[0] == '/' || fCurrentDirectory.IsEmpty()) { 922 _actualPath = path; 923 } else { 924 _actualPath.Truncate(0); 925 _actualPath << fCurrentDirectory << '/' << path; 926 } 927 928 struct stat st; 929 return lstat(_actualPath, &st) == 0; 930 } 931 932 933 void 934 TermView::HyperLinkState::_UpdateHighlight() 935 { 936 BPoint where; 937 uint32 buttons; 938 fView->GetMouse(&where, &buttons, false); 939 _UpdateHighlight(where, fView->fModifiers); 940 } 941 942 943 void 944 TermView::HyperLinkState::_UpdateHighlight(BPoint where, int32 modifiers) 945 { 946 TermPos start; 947 TermPos end; 948 HyperLink link; 949 950 bool pathPrefixOnly = (modifiers & B_SHIFT_KEY) != 0; 951 if (_GetHyperLinkAt(where, pathPrefixOnly, link, start, end)) 952 _ActivateHighlight(start, end); 953 else 954 _DeactivateHighlight(); 955 } 956 957 958 void 959 TermView::HyperLinkState::_ActivateHighlight(const TermPos& start, 960 const TermPos& end) 961 { 962 if (fHighlightActive) { 963 if (fHighlight.Start() == start && fHighlight.End() == end) 964 return; 965 966 _DeactivateHighlight(); 967 } 968 969 fHighlight.SetRange(start, end); 970 fView->_AddHighlight(&fHighlight); 971 BCursor cursor(B_CURSOR_ID_FOLLOW_LINK); 972 fView->SetViewCursor(&cursor); 973 fHighlightActive = true; 974 } 975 976 977 void 978 TermView::HyperLinkState::_DeactivateHighlight() 979 { 980 if (fHighlightActive) { 981 fView->_RemoveHighlight(&fHighlight); 982 BCursor cursor(B_CURSOR_ID_SYSTEM_DEFAULT); 983 fView->SetViewCursor(&cursor); 984 fHighlightActive = false; 985 } 986 } 987 988 989 // #pragma mark - HyperLinkMenuState 990 991 992 class TermView::HyperLinkMenuState::PopUpMenu : public BPopUpMenu { 993 public: 994 PopUpMenu(const BMessenger& messageTarget) 995 : 996 BPopUpMenu("open hyperlink"), 997 fMessageTarget(messageTarget) 998 { 999 SetAsyncAutoDestruct(true); 1000 } 1001 1002 ~PopUpMenu() 1003 { 1004 fMessageTarget.SendMessage(kMessageMenuClosed); 1005 } 1006 1007 private: 1008 BMessenger fMessageTarget; 1009 }; 1010 1011 1012 TermView::HyperLinkMenuState::HyperLinkMenuState(TermView* view) 1013 : 1014 State(view), 1015 fLink() 1016 { 1017 } 1018 1019 1020 void 1021 TermView::HyperLinkMenuState::Prepare(BPoint point, const HyperLink& link) 1022 { 1023 fLink = link; 1024 1025 // open context menu 1026 PopUpMenu* menu = new PopUpMenu(fView); 1027 BLayoutBuilder::Menu<> menuBuilder(menu); 1028 switch (link.GetType()) { 1029 case HyperLink::TYPE_URL: 1030 menuBuilder 1031 .AddItem(B_TRANSLATE("Open link"), kMessageOpenLink) 1032 .AddItem(B_TRANSLATE("Copy link location"), kMessageCopyLink); 1033 break; 1034 1035 case HyperLink::TYPE_PATH: 1036 case HyperLink::TYPE_PATH_WITH_LINE: 1037 case HyperLink::TYPE_PATH_WITH_LINE_AND_COLUMN: 1038 menuBuilder.AddItem(B_TRANSLATE("Open path"), kMessageOpenLink); 1039 menuBuilder.AddItem(B_TRANSLATE("Copy path"), kMessageCopyLink); 1040 if (fLink.Text() != fLink.Address()) { 1041 menuBuilder.AddItem(B_TRANSLATE("Copy absolute path"), 1042 kMessageCopyAbsolutePath); 1043 } 1044 break; 1045 } 1046 menu->SetTargetForItems(fView); 1047 menu->Go(fView->ConvertToScreen(point), true, true, true); 1048 } 1049 1050 1051 void 1052 TermView::HyperLinkMenuState::Exited() 1053 { 1054 fLink = HyperLink(); 1055 } 1056 1057 1058 bool 1059 TermView::HyperLinkMenuState::MessageReceived(BMessage* message) 1060 { 1061 switch (message->what) { 1062 case kMessageOpenLink: 1063 if (fLink.IsValid()) 1064 fLink.Open(); 1065 return true; 1066 1067 case kMessageCopyLink: 1068 case kMessageCopyAbsolutePath: 1069 { 1070 if (fLink.IsValid()) { 1071 BString toCopy = message->what == kMessageCopyLink 1072 ? fLink.Text() : fLink.Address(); 1073 1074 if (!be_clipboard->Lock()) 1075 return true; 1076 1077 be_clipboard->Clear(); 1078 1079 if (BMessage *data = be_clipboard->Data()) { 1080 data->AddData("text/plain", B_MIME_TYPE, toCopy.String(), 1081 toCopy.Length()); 1082 be_clipboard->Commit(); 1083 } 1084 1085 be_clipboard->Unlock(); 1086 } 1087 return true; 1088 } 1089 1090 case kMessageMenuClosed: 1091 fView->_NextState(fView->fDefaultState); 1092 return true; 1093 } 1094 1095 return false; 1096 } 1097