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