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