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