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, mod, rawChar; 197 BMessage *currentMessage = fView->Looper()->CurrentMessage(); 198 if (currentMessage == NULL) 199 return; 200 201 currentMessage->FindInt32("modifiers", &mod); 202 currentMessage->FindInt32("key", &key); 203 currentMessage->FindInt32("raw_char", &rawChar); 204 205 fView->_ActivateCursor(true); 206 207 // handle multi-byte chars 208 if (numBytes > 1) { 209 if (fView->fEncoding != M_UTF8) { 210 char destBuffer[16]; 211 int32 destLen = sizeof(destBuffer); 212 int32 state = 0; 213 convert_from_utf8(fView->fEncoding, bytes, &numBytes, destBuffer, 214 &destLen, &state, '?'); 215 fView->_ScrollTo(0, true); 216 fView->fShell->Write(destBuffer, destLen); 217 return; 218 } 219 220 fView->_ScrollTo(0, true); 221 fView->fShell->Write(bytes, numBytes); 222 return; 223 } 224 225 // Terminal filters RET, ENTER, F1...F12, and ARROW key code. 226 const char *toWrite = NULL; 227 228 switch (*bytes) { 229 case B_RETURN: 230 if (rawChar == B_RETURN) 231 toWrite = "\r"; 232 break; 233 234 case B_DELETE: 235 toWrite = DELETE_KEY_CODE; 236 break; 237 238 case B_BACKSPACE: 239 // Translate only the actual backspace key to the backspace 240 // code. CTRL-H shall just be echoed. 241 if (!((mod & B_CONTROL_KEY) && rawChar == 'h')) 242 toWrite = BACKSPACE_KEY_CODE; 243 break; 244 245 case B_LEFT_ARROW: 246 if (rawChar == B_LEFT_ARROW) { 247 if ((mod & B_SHIFT_KEY) != 0) { 248 if (fView->fListener != NULL) 249 fView->fListener->PreviousTermView(fView); 250 return; 251 } 252 if ((mod & B_CONTROL_KEY) || (mod & B_COMMAND_KEY)) 253 toWrite = CTRL_LEFT_ARROW_KEY_CODE; 254 else 255 toWrite = LEFT_ARROW_KEY_CODE; 256 } 257 break; 258 259 case B_RIGHT_ARROW: 260 if (rawChar == B_RIGHT_ARROW) { 261 if ((mod & B_SHIFT_KEY) != 0) { 262 if (fView->fListener != NULL) 263 fView->fListener->NextTermView(fView); 264 return; 265 } 266 if ((mod & B_CONTROL_KEY) || (mod & B_COMMAND_KEY)) 267 toWrite = CTRL_RIGHT_ARROW_KEY_CODE; 268 else 269 toWrite = RIGHT_ARROW_KEY_CODE; 270 } 271 break; 272 273 case B_UP_ARROW: 274 if (mod & B_SHIFT_KEY) { 275 fView->_ScrollTo(fView->fScrollOffset - fView->fFontHeight, 276 true); 277 return; 278 } 279 if (rawChar == B_UP_ARROW) { 280 if (mod & B_CONTROL_KEY) 281 toWrite = CTRL_UP_ARROW_KEY_CODE; 282 else 283 toWrite = UP_ARROW_KEY_CODE; 284 } 285 break; 286 287 case B_DOWN_ARROW: 288 if (mod & B_SHIFT_KEY) { 289 fView->_ScrollTo(fView->fScrollOffset + fView->fFontHeight, 290 true); 291 return; 292 } 293 294 if (rawChar == B_DOWN_ARROW) { 295 if (mod & B_CONTROL_KEY) 296 toWrite = CTRL_DOWN_ARROW_KEY_CODE; 297 else 298 toWrite = DOWN_ARROW_KEY_CODE; 299 } 300 break; 301 302 case B_INSERT: 303 if (rawChar == B_INSERT) 304 toWrite = INSERT_KEY_CODE; 305 break; 306 307 case B_HOME: 308 if (rawChar == B_HOME) 309 toWrite = HOME_KEY_CODE; 310 break; 311 312 case B_END: 313 if (rawChar == B_END) 314 toWrite = END_KEY_CODE; 315 break; 316 317 case B_PAGE_UP: 318 if (mod & B_SHIFT_KEY) { 319 fView->_ScrollTo( 320 fView->fScrollOffset - fView->fFontHeight * fView->fRows, 321 true); 322 return; 323 } 324 if (rawChar == B_PAGE_UP) 325 toWrite = PAGE_UP_KEY_CODE; 326 break; 327 328 case B_PAGE_DOWN: 329 if (mod & B_SHIFT_KEY) { 330 fView->_ScrollTo( 331 fView->fScrollOffset + fView->fFontHeight * fView->fRows, 332 true); 333 return; 334 } 335 if (rawChar == B_PAGE_DOWN) 336 toWrite = PAGE_DOWN_KEY_CODE; 337 break; 338 339 case B_FUNCTION_KEY: 340 for (int32 i = 0; i < 12; i++) { 341 if (key == function_keycode_table[i]) { 342 toWrite = function_key_char_table[i]; 343 break; 344 } 345 } 346 break; 347 } 348 349 // If the above code proposed an alternative string to write, we get it's 350 // length. Otherwise we write exactly the bytes passed to this method. 351 size_t toWriteLen; 352 if (toWrite != NULL) { 353 toWriteLen = strlen(toWrite); 354 } else { 355 toWrite = bytes; 356 toWriteLen = numBytes; 357 } 358 359 fView->_ScrollTo(0, true); 360 fView->fShell->Write(toWrite, toWriteLen); 361 } 362 363 364 void 365 TermView::DefaultState::MouseDown(BPoint where, int32 buttons, int32 modifiers) 366 { 367 if (fView->fReportAnyMouseEvent || fView->fReportButtonMouseEvent 368 || fView->fReportNormalMouseEvent || fView->fReportX10MouseEvent) { 369 TermPos clickPos = fView->_ConvertToTerminal(where); 370 fView->_SendMouseEvent(buttons, modifiers, clickPos.x, clickPos.y, 371 false); 372 return; 373 } 374 375 // paste button 376 if ((buttons & (B_SECONDARY_MOUSE_BUTTON | B_TERTIARY_MOUSE_BUTTON)) != 0) { 377 fView->Paste(fView->fMouseClipboard); 378 return; 379 } 380 381 // Select Region 382 if (buttons == B_PRIMARY_MOUSE_BUTTON) { 383 fView->fSelectState->Prepare(where, modifiers); 384 fView->_NextState(fView->fSelectState); 385 } 386 } 387 388 389 void 390 TermView::DefaultState::MouseMoved(BPoint where, uint32 transit, 391 const BMessage* message, int32 modifiers) 392 { 393 if (_CheckEnterHyperLinkState(modifiers)) 394 return; 395 396 _StandardMouseMoved(where, modifiers); 397 } 398 399 400 void 401 TermView::DefaultState::WindowActivated(bool active) 402 { 403 if (active) 404 _CheckEnterHyperLinkState(fView->fModifiers); 405 } 406 407 408 bool 409 TermView::DefaultState::_CheckEnterHyperLinkState(int32 modifiers) 410 { 411 if ((modifiers & B_COMMAND_KEY) != 0 && fView->Window()->IsActive()) { 412 fView->_NextState(fView->fHyperLinkState); 413 return true; 414 } 415 416 return false; 417 } 418 419 420 // #pragma mark - SelectState 421 422 423 TermView::SelectState::SelectState(TermView* view) 424 : 425 StandardBaseState(view), 426 fSelectGranularity(SELECT_CHARS), 427 fCheckMouseTracking(false), 428 fMouseTracking(false) 429 { 430 } 431 432 433 void 434 TermView::SelectState::Prepare(BPoint where, int32 modifiers) 435 { 436 int32 clicks; 437 fView->Window()->CurrentMessage()->FindInt32("clicks", &clicks); 438 439 if (fView->_HasSelection()) { 440 TermPos inPos = fView->_ConvertToTerminal(where); 441 if (fView->fSelection.RangeContains(inPos)) { 442 if (modifiers & B_CONTROL_KEY) { 443 BPoint p; 444 uint32 bt; 445 do { 446 fView->GetMouse(&p, &bt); 447 448 if (bt == 0) { 449 fView->_Deselect(); 450 return; 451 } 452 453 snooze(40000); 454 455 } while (abs((int)(where.x - p.x)) < 4 456 && abs((int)(where.y - p.y)) < 4); 457 458 fView->InitiateDrag(); 459 return; 460 } 461 } 462 } 463 464 // If mouse has moved too much, disable double/triple click. 465 if (fView->_MouseDistanceSinceLastClick(where) > 8) 466 clicks = 1; 467 468 fView->SetMouseEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS, 469 B_NO_POINTER_HISTORY | B_LOCK_WINDOW_FOCUS); 470 471 TermPos clickPos = fView->_ConvertToTerminal(where); 472 473 if (modifiers & B_SHIFT_KEY) { 474 fView->fInitialSelectionStart = clickPos; 475 fView->fInitialSelectionEnd = clickPos; 476 fView->_ExtendSelection(fView->fInitialSelectionStart, true, false); 477 } else { 478 fView->_Deselect(); 479 fView->fInitialSelectionStart = clickPos; 480 fView->fInitialSelectionEnd = clickPos; 481 } 482 483 // If clicks larger than 3, reset mouse click counter. 484 clicks = (clicks - 1) % 3 + 1; 485 486 switch (clicks) { 487 case 1: 488 fCheckMouseTracking = true; 489 fSelectGranularity = SELECT_CHARS; 490 break; 491 492 case 2: 493 fView->_SelectWord(where, (modifiers & B_SHIFT_KEY) != 0, false); 494 fMouseTracking = true; 495 fSelectGranularity = SELECT_WORDS; 496 break; 497 498 case 3: 499 fView->_SelectLine(where, (modifiers & B_SHIFT_KEY) != 0, false); 500 fMouseTracking = true; 501 fSelectGranularity = SELECT_LINES; 502 break; 503 } 504 } 505 506 507 bool 508 TermView::SelectState::MessageReceived(BMessage* message) 509 { 510 if (message->what == kAutoScroll) { 511 _AutoScrollUpdate(); 512 return true; 513 } 514 515 return false; 516 } 517 518 519 void 520 TermView::SelectState::MouseMoved(BPoint where, uint32 transit, 521 const BMessage* message, int32 modifiers) 522 { 523 if (_StandardMouseMoved(where, modifiers)) 524 return; 525 526 if (fCheckMouseTracking) { 527 if (fView->_MouseDistanceSinceLastClick(where) > 9) 528 fMouseTracking = true; 529 } 530 if (!fMouseTracking) 531 return; 532 533 bool doAutoScroll = false; 534 535 if (where.y < 0) { 536 doAutoScroll = true; 537 fView->fAutoScrollSpeed = where.y; 538 where.x = 0; 539 where.y = 0; 540 } 541 542 BRect bounds(fView->Bounds()); 543 if (where.y > bounds.bottom) { 544 doAutoScroll = true; 545 fView->fAutoScrollSpeed = where.y - bounds.bottom; 546 where.x = bounds.right; 547 where.y = bounds.bottom; 548 } 549 550 if (doAutoScroll) { 551 if (fView->fAutoScrollRunner == NULL) { 552 BMessage message(kAutoScroll); 553 fView->fAutoScrollRunner = new (std::nothrow) BMessageRunner( 554 BMessenger(fView), &message, 10000); 555 } 556 } else { 557 delete fView->fAutoScrollRunner; 558 fView->fAutoScrollRunner = NULL; 559 } 560 561 switch (fSelectGranularity) { 562 case SELECT_CHARS: 563 { 564 // If we just start selecting, we first select the initially 565 // hit char, so that we get a proper initial selection -- the char 566 // in question, which will thus always be selected, regardless of 567 // whether selecting forward or backward. 568 if (fView->fInitialSelectionStart == fView->fInitialSelectionEnd) { 569 fView->_Select(fView->fInitialSelectionStart, 570 fView->fInitialSelectionEnd, true, true); 571 } 572 573 fView->_ExtendSelection(fView->_ConvertToTerminal(where), true, 574 true); 575 break; 576 } 577 case SELECT_WORDS: 578 fView->_SelectWord(where, true, true); 579 break; 580 case SELECT_LINES: 581 fView->_SelectLine(where, true, true); 582 break; 583 } 584 } 585 586 587 void 588 TermView::SelectState::MouseUp(BPoint where, int32 buttons) 589 { 590 fCheckMouseTracking = false; 591 fMouseTracking = false; 592 593 if (fView->fAutoScrollRunner != NULL) { 594 delete fView->fAutoScrollRunner; 595 fView->fAutoScrollRunner = NULL; 596 } 597 598 // When releasing the first mouse button, we copy the selected text to the 599 // clipboard. 600 601 if (fView->fReportAnyMouseEvent || fView->fReportButtonMouseEvent 602 || fView->fReportNormalMouseEvent) { 603 TermPos clickPos = fView->_ConvertToTerminal(where); 604 fView->_SendMouseEvent(0, 0, clickPos.x, clickPos.y, false); 605 } else { 606 if ((buttons & B_PRIMARY_MOUSE_BUTTON) == 0 607 && (fView->fMouseButtons & B_PRIMARY_MOUSE_BUTTON) != 0) { 608 fView->Copy(fView->fMouseClipboard); 609 } 610 611 } 612 613 fView->_NextState(fView->fDefaultState); 614 } 615 616 617 void 618 TermView::SelectState::_AutoScrollUpdate() 619 { 620 if (fMouseTracking && fView->fAutoScrollRunner != NULL 621 && fView->fScrollBar != NULL) { 622 float value = fView->fScrollBar->Value(); 623 fView->_ScrollTo(value + fView->fAutoScrollSpeed, true); 624 if (fView->fAutoScrollSpeed < 0) { 625 fView->_ExtendSelection( 626 fView->_ConvertToTerminal(BPoint(0, 0)), true, true); 627 } else { 628 fView->_ExtendSelection( 629 fView->_ConvertToTerminal(fView->Bounds().RightBottom()), true, 630 true); 631 } 632 } 633 } 634 635 636 // #pragma mark - HyperLinkState 637 638 639 TermView::HyperLinkState::HyperLinkState(TermView* view) 640 : 641 State(view), 642 fURLCharClassifier(kURLAdditionalWordCharacters), 643 fPathComponentCharClassifier( 644 BString(kDefaultAdditionalWordCharacters).RemoveFirst("/")), 645 fCurrentDirectory(), 646 fHighlight(), 647 fHighlightActive(false) 648 { 649 fHighlight.SetHighlighter(this); 650 } 651 652 653 void 654 TermView::HyperLinkState::Entered() 655 { 656 ActiveProcessInfo activeProcessInfo; 657 if (fView->GetActiveProcessInfo(activeProcessInfo)) 658 fCurrentDirectory = activeProcessInfo.CurrentDirectory(); 659 else 660 fCurrentDirectory.Truncate(0); 661 662 _UpdateHighlight(); 663 } 664 665 666 void 667 TermView::HyperLinkState::Exited() 668 { 669 _DeactivateHighlight(); 670 } 671 672 673 void 674 TermView::HyperLinkState::ModifiersChanged(int32 oldModifiers, int32 modifiers) 675 { 676 if ((modifiers & B_COMMAND_KEY) == 0) 677 fView->_NextState(fView->fDefaultState); 678 else 679 _UpdateHighlight(); 680 } 681 682 683 void 684 TermView::HyperLinkState::MouseDown(BPoint where, int32 buttons, 685 int32 modifiers) 686 { 687 TermPos start; 688 TermPos end; 689 HyperLink link; 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 void 943 TermView::HyperLinkState::_UpdateHighlight(BPoint where, int32 modifiers) 944 { 945 TermPos start; 946 TermPos end; 947 HyperLink link; 948 bool pathPrefixOnly = (modifiers & B_SHIFT_KEY) != 0; 949 if (_GetHyperLinkAt(where, pathPrefixOnly, link, start, end)) 950 _ActivateHighlight(start, end); 951 else 952 _DeactivateHighlight(); 953 } 954 955 956 void 957 TermView::HyperLinkState::_ActivateHighlight(const TermPos& start, 958 const TermPos& end) 959 { 960 if (fHighlightActive) { 961 if (fHighlight.Start() == start && fHighlight.End() == end) 962 return; 963 _DeactivateHighlight(); 964 } 965 966 fHighlight.SetRange(start, end); 967 fView->_AddHighlight(&fHighlight); 968 BCursor cursor(B_CURSOR_ID_FOLLOW_LINK); 969 fView->SetViewCursor(&cursor); 970 fHighlightActive = true; 971 } 972 973 974 void 975 TermView::HyperLinkState::_DeactivateHighlight() 976 { 977 if (fHighlightActive) { 978 fView->_RemoveHighlight(&fHighlight); 979 BCursor cursor(B_CURSOR_ID_SYSTEM_DEFAULT); 980 fView->SetViewCursor(&cursor); 981 fHighlightActive = false; 982 } 983 } 984 985 986 // #pragma mark - HyperLinkMenuState 987 988 989 class TermView::HyperLinkMenuState::PopUpMenu : public BPopUpMenu { 990 public: 991 PopUpMenu(const BMessenger& messageTarget) 992 : 993 BPopUpMenu("open hyperlink"), 994 fMessageTarget(messageTarget) 995 { 996 SetAsyncAutoDestruct(true); 997 } 998 999 ~PopUpMenu() 1000 { 1001 fMessageTarget.SendMessage(kMessageMenuClosed); 1002 } 1003 1004 private: 1005 BMessenger fMessageTarget; 1006 }; 1007 1008 1009 TermView::HyperLinkMenuState::HyperLinkMenuState(TermView* view) 1010 : 1011 State(view), 1012 fLink() 1013 { 1014 } 1015 1016 1017 void 1018 TermView::HyperLinkMenuState::Prepare(BPoint point, const HyperLink& link) 1019 { 1020 fLink = link; 1021 1022 // open context menu 1023 PopUpMenu* menu = new PopUpMenu(fView); 1024 BLayoutBuilder::Menu<> menuBuilder(menu); 1025 switch (link.GetType()) { 1026 case HyperLink::TYPE_URL: 1027 menuBuilder 1028 .AddItem(B_TRANSLATE("Open link"), kMessageOpenLink) 1029 .AddItem(B_TRANSLATE("Copy link location"), kMessageCopyLink); 1030 break; 1031 1032 case HyperLink::TYPE_PATH: 1033 case HyperLink::TYPE_PATH_WITH_LINE: 1034 case HyperLink::TYPE_PATH_WITH_LINE_AND_COLUMN: 1035 menuBuilder.AddItem(B_TRANSLATE("Open path"), kMessageOpenLink); 1036 menuBuilder.AddItem(B_TRANSLATE("Copy path"), kMessageCopyLink); 1037 if (fLink.Text() != fLink.Address()) { 1038 menuBuilder.AddItem(B_TRANSLATE("Copy absolute path"), 1039 kMessageCopyAbsolutePath); 1040 } 1041 break; 1042 } 1043 menu->SetTargetForItems(fView); 1044 menu->Go(fView->ConvertToScreen(point), true, true, true); 1045 } 1046 1047 1048 void 1049 TermView::HyperLinkMenuState::Exited() 1050 { 1051 fLink = HyperLink(); 1052 } 1053 1054 1055 bool 1056 TermView::HyperLinkMenuState::MessageReceived(BMessage* message) 1057 { 1058 switch (message->what) { 1059 case kMessageOpenLink: 1060 if (fLink.IsValid()) 1061 fLink.Open(); 1062 return true; 1063 1064 case kMessageCopyLink: 1065 case kMessageCopyAbsolutePath: 1066 { 1067 if (fLink.IsValid()) { 1068 BString toCopy = message->what == kMessageCopyLink 1069 ? fLink.Text() : fLink.Address(); 1070 1071 if (!be_clipboard->Lock()) 1072 return true; 1073 1074 be_clipboard->Clear(); 1075 1076 if (BMessage *data = be_clipboard->Data()) { 1077 data->AddData("text/plain", B_MIME_TYPE, toCopy.String(), 1078 toCopy.Length()); 1079 be_clipboard->Commit(); 1080 } 1081 1082 be_clipboard->Unlock(); 1083 } 1084 return true; 1085 } 1086 1087 case kMessageMenuClosed: 1088 fView->_NextState(fView->fDefaultState); 1089 return true; 1090 } 1091 1092 return false; 1093 } 1094 1095