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 || fView->fEnableExtendedMouseCoordinates) { 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, 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::MouseUp(BPoint where, int32 buttons) 457 { 458 if (fView->fReportAnyMouseEvent || fView->fReportButtonMouseEvent 459 || fView->fReportNormalMouseEvent || fView->fReportX10MouseEvent) { 460 TermPos clickPos = fView->_ConvertToTerminal(where); 461 fView->_SendMouseEvent(buttons, 0, clickPos.x, clickPos.y, 462 false, true); 463 } 464 } 465 466 467 void 468 TermView::DefaultState::WindowActivated(bool active) 469 { 470 if (active) 471 _CheckEnterHyperLinkState(fView->fModifiers); 472 } 473 474 475 bool 476 TermView::DefaultState::_CheckEnterHyperLinkState(int32 modifiers) 477 { 478 if ((modifiers & B_COMMAND_KEY) != 0 && fView->Window()->IsActive()) { 479 fView->_NextState(fView->fHyperLinkState); 480 return true; 481 } 482 483 return false; 484 } 485 486 487 // #pragma mark - SelectState 488 489 490 TermView::SelectState::SelectState(TermView* view) 491 : 492 StandardBaseState(view), 493 fSelectGranularity(SELECT_CHARS), 494 fCheckMouseTracking(false), 495 fMouseTracking(false) 496 { 497 } 498 499 500 void 501 TermView::SelectState::Prepare(BPoint where, int32 modifiers) 502 { 503 int32 clicks; 504 fView->Window()->CurrentMessage()->FindInt32("clicks", &clicks); 505 506 if (fView->_HasSelection()) { 507 TermPos inPos = fView->_ConvertToTerminal(where); 508 if (fView->fSelection.RangeContains(inPos)) { 509 if (modifiers & B_CONTROL_KEY) { 510 BPoint p; 511 uint32 bt; 512 do { 513 fView->GetMouse(&p, &bt); 514 515 if (bt == 0) { 516 fView->_Deselect(); 517 return; 518 } 519 520 snooze(40000); 521 522 } while (abs((int)(where.x - p.x)) < 4 523 && abs((int)(where.y - p.y)) < 4); 524 525 fView->InitiateDrag(); 526 return; 527 } 528 } 529 } 530 531 // If mouse has moved too much, disable double/triple click. 532 if (fView->_MouseDistanceSinceLastClick(where) > 8) 533 clicks = 1; 534 535 fView->SetMouseEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS, 536 B_NO_POINTER_HISTORY | B_LOCK_WINDOW_FOCUS); 537 538 TermPos clickPos = fView->_ConvertToTerminal(where); 539 540 if (modifiers & B_SHIFT_KEY) { 541 fView->fInitialSelectionStart = clickPos; 542 fView->fInitialSelectionEnd = clickPos; 543 fView->_ExtendSelection(fView->fInitialSelectionStart, true, false); 544 } else { 545 fView->_Deselect(); 546 fView->fInitialSelectionStart = clickPos; 547 fView->fInitialSelectionEnd = clickPos; 548 } 549 550 // If clicks larger than 3, reset mouse click counter. 551 clicks = (clicks - 1) % 3 + 1; 552 553 switch (clicks) { 554 case 1: 555 fCheckMouseTracking = true; 556 fSelectGranularity = SELECT_CHARS; 557 break; 558 559 case 2: 560 fView->_SelectWord(where, (modifiers & B_SHIFT_KEY) != 0, false); 561 fMouseTracking = true; 562 fSelectGranularity = SELECT_WORDS; 563 break; 564 565 case 3: 566 fView->_SelectLine(where, (modifiers & B_SHIFT_KEY) != 0, false); 567 fMouseTracking = true; 568 fSelectGranularity = SELECT_LINES; 569 break; 570 } 571 } 572 573 574 bool 575 TermView::SelectState::MessageReceived(BMessage* message) 576 { 577 if (message->what == kAutoScroll) { 578 _AutoScrollUpdate(); 579 return true; 580 } 581 582 return false; 583 } 584 585 586 void 587 TermView::SelectState::MouseMoved(BPoint where, uint32 transit, 588 const BMessage* message, int32 modifiers) 589 { 590 if (_StandardMouseMoved(where, modifiers)) 591 return; 592 593 if (fCheckMouseTracking) { 594 if (fView->_MouseDistanceSinceLastClick(where) > 9) 595 fMouseTracking = true; 596 } 597 if (!fMouseTracking) 598 return; 599 600 bool doAutoScroll = false; 601 602 if (where.y < 0) { 603 doAutoScroll = true; 604 fView->fAutoScrollSpeed = where.y; 605 where.x = 0; 606 where.y = 0; 607 } 608 609 BRect bounds(fView->Bounds()); 610 if (where.y > bounds.bottom) { 611 doAutoScroll = true; 612 fView->fAutoScrollSpeed = where.y - bounds.bottom; 613 where.x = bounds.right; 614 where.y = bounds.bottom; 615 } 616 617 if (doAutoScroll) { 618 if (fView->fAutoScrollRunner == NULL) { 619 BMessage message(kAutoScroll); 620 fView->fAutoScrollRunner = new (std::nothrow) BMessageRunner( 621 BMessenger(fView), &message, 10000); 622 } 623 } else { 624 delete fView->fAutoScrollRunner; 625 fView->fAutoScrollRunner = NULL; 626 } 627 628 switch (fSelectGranularity) { 629 case SELECT_CHARS: 630 { 631 // If we just start selecting, we first select the initially 632 // hit char, so that we get a proper initial selection -- the char 633 // in question, which will thus always be selected, regardless of 634 // whether selecting forward or backward. 635 if (fView->fInitialSelectionStart == fView->fInitialSelectionEnd) { 636 fView->_Select(fView->fInitialSelectionStart, 637 fView->fInitialSelectionEnd, true, true); 638 } 639 640 fView->_ExtendSelection(fView->_ConvertToTerminal(where), true, 641 true); 642 break; 643 } 644 case SELECT_WORDS: 645 fView->_SelectWord(where, true, true); 646 break; 647 case SELECT_LINES: 648 fView->_SelectLine(where, true, true); 649 break; 650 } 651 } 652 653 654 void 655 TermView::SelectState::MouseUp(BPoint where, int32 buttons) 656 { 657 fCheckMouseTracking = false; 658 fMouseTracking = false; 659 660 if (fView->fAutoScrollRunner != NULL) { 661 delete fView->fAutoScrollRunner; 662 fView->fAutoScrollRunner = NULL; 663 } 664 665 // When releasing the first mouse button, we copy the selected text to the 666 // clipboard. 667 668 if (fView->fReportAnyMouseEvent || fView->fReportButtonMouseEvent 669 || fView->fReportNormalMouseEvent) { 670 TermPos clickPos = fView->_ConvertToTerminal(where); 671 fView->_SendMouseEvent(0, 0, clickPos.x, clickPos.y, false); 672 } else if ((buttons & B_PRIMARY_MOUSE_BUTTON) == 0 673 && (fView->fMouseButtons & B_PRIMARY_MOUSE_BUTTON) != 0) { 674 fView->Copy(fView->fMouseClipboard); 675 } 676 677 fView->_NextState(fView->fDefaultState); 678 } 679 680 681 void 682 TermView::SelectState::_AutoScrollUpdate() 683 { 684 if (fMouseTracking && fView->fAutoScrollRunner != NULL 685 && fView->fScrollBar != NULL) { 686 float value = fView->fScrollBar->Value(); 687 fView->_ScrollTo(value + fView->fAutoScrollSpeed, true); 688 if (fView->fAutoScrollSpeed < 0) { 689 fView->_ExtendSelection( 690 fView->_ConvertToTerminal(BPoint(0, 0)), true, true); 691 } else { 692 fView->_ExtendSelection( 693 fView->_ConvertToTerminal(fView->Bounds().RightBottom()), true, 694 true); 695 } 696 } 697 } 698 699 700 // #pragma mark - HyperLinkState 701 702 703 TermView::HyperLinkState::HyperLinkState(TermView* view) 704 : 705 State(view), 706 fURLCharClassifier(kURLAdditionalWordCharacters), 707 fPathComponentCharClassifier( 708 BString(kDefaultAdditionalWordCharacters).RemoveFirst("/")), 709 fCurrentDirectory(), 710 fHighlight(), 711 fHighlightActive(false) 712 { 713 fHighlight.SetHighlighter(this); 714 } 715 716 717 void 718 TermView::HyperLinkState::Entered() 719 { 720 ActiveProcessInfo activeProcessInfo; 721 if (fView->GetActiveProcessInfo(activeProcessInfo)) 722 fCurrentDirectory = activeProcessInfo.CurrentDirectory(); 723 else 724 fCurrentDirectory.Truncate(0); 725 726 _UpdateHighlight(); 727 } 728 729 730 void 731 TermView::HyperLinkState::Exited() 732 { 733 _DeactivateHighlight(); 734 } 735 736 737 void 738 TermView::HyperLinkState::ModifiersChanged(int32 oldModifiers, int32 modifiers) 739 { 740 if ((modifiers & B_COMMAND_KEY) == 0) 741 fView->_NextState(fView->fDefaultState); 742 else 743 _UpdateHighlight(); 744 } 745 746 747 void 748 TermView::HyperLinkState::MouseDown(BPoint where, int32 buttons, 749 int32 modifiers) 750 { 751 TermPos start; 752 TermPos end; 753 HyperLink link; 754 755 bool pathPrefixOnly = (modifiers & B_SHIFT_KEY) != 0; 756 if (!_GetHyperLinkAt(where, pathPrefixOnly, link, start, end)) 757 return; 758 759 if ((buttons & B_PRIMARY_MOUSE_BUTTON) != 0) { 760 link.Open(); 761 } else if ((buttons & B_SECONDARY_MOUSE_BUTTON) != 0) { 762 fView->fHyperLinkMenuState->Prepare(where, link); 763 fView->_NextState(fView->fHyperLinkMenuState); 764 } 765 } 766 767 768 void 769 TermView::HyperLinkState::MouseMoved(BPoint where, uint32 transit, 770 const BMessage* message, int32 modifiers) 771 { 772 _UpdateHighlight(where, modifiers); 773 } 774 775 776 void 777 TermView::HyperLinkState::WindowActivated(bool active) 778 { 779 if (!active) 780 fView->_NextState(fView->fDefaultState); 781 } 782 783 784 void 785 TermView::HyperLinkState::VisibleTextBufferChanged() 786 { 787 _UpdateHighlight(); 788 } 789 790 791 rgb_color 792 TermView::HyperLinkState::ForegroundColor() 793 { 794 return make_color(0, 0, 255); 795 } 796 797 798 rgb_color 799 TermView::HyperLinkState::BackgroundColor() 800 { 801 return fView->fTextBackColor; 802 } 803 804 805 uint32 806 TermView::HyperLinkState::AdjustTextAttributes(uint32 attributes) 807 { 808 return attributes | UNDERLINE; 809 } 810 811 812 bool 813 TermView::HyperLinkState::_GetHyperLinkAt(BPoint where, bool pathPrefixOnly, 814 HyperLink& _link, TermPos& _start, TermPos& _end) 815 { 816 TerminalBuffer* textBuffer = fView->fTextBuffer; 817 BAutolock textBufferLocker(textBuffer); 818 819 TermPos pos = fView->_ConvertToTerminal(where); 820 821 // try to get a URL first 822 BString text; 823 if (!textBuffer->FindWord(pos, &fURLCharClassifier, false, _start, _end)) 824 return false; 825 826 text.Truncate(0); 827 textBuffer->GetStringFromRegion(text, _start, _end); 828 text.Trim(); 829 830 // We're only happy, if it has a protocol part which we know. 831 int32 colonIndex = text.FindFirst(':'); 832 if (colonIndex >= 0) { 833 BString protocol(text, colonIndex); 834 if (strstr(kKnownURLProtocols, protocol) != NULL) { 835 _link = HyperLink(text, HyperLink::TYPE_URL); 836 return true; 837 } 838 } 839 840 // no obvious URL -- try file name 841 if (!textBuffer->FindWord(pos, fView->fCharClassifier, false, _start, _end)) 842 return false; 843 844 // In path-prefix-only mode we determine the end position anew by omitting 845 // the '/' in the allowed word chars. 846 if (pathPrefixOnly) { 847 TermPos componentStart; 848 TermPos componentEnd; 849 if (textBuffer->FindWord(pos, &fPathComponentCharClassifier, false, 850 componentStart, componentEnd)) { 851 _end = componentEnd; 852 } else { 853 // That means pos points to a '/'. We simply use the previous 854 // position. 855 _end = pos; 856 if (_start == _end) { 857 // Well, must be just "/". Advance to the next position. 858 if (!textBuffer->NextLinePos(_end, false)) 859 return false; 860 } 861 } 862 } 863 864 text.Truncate(0); 865 textBuffer->GetStringFromRegion(text, _start, _end); 866 text.Trim(); 867 if (text.IsEmpty()) 868 return false; 869 870 // Collect a list of colons in the string and their respective positions in 871 // the text buffer. We do this up-front so we can unlock the text buffer 872 // while we're doing all the entry existence tests. 873 typedef Array<CharPosition> ColonList; 874 ColonList colonPositions; 875 TermPos searchPos = _start; 876 for (int32 index = 0; (index = text.FindFirst(':', index)) >= 0;) { 877 TermPos foundStart; 878 TermPos foundEnd; 879 if (!textBuffer->Find(":", searchPos, true, true, false, foundStart, 880 foundEnd)) { 881 return false; 882 } 883 884 CharPosition colonPosition; 885 colonPosition.index = index; 886 colonPosition.position = foundStart; 887 if (!colonPositions.Add(colonPosition)) 888 return false; 889 890 index++; 891 searchPos = foundEnd; 892 } 893 894 textBufferLocker.Unlock(); 895 896 // Since we also want to consider ':' a potential path delimiter, in two 897 // nested loops we chop off components from the beginning respective the 898 // end. 899 BString originalText = text; 900 TermPos originalStart = _start; 901 TermPos originalEnd = _end; 902 903 int32 colonCount = colonPositions.Count(); 904 for (int32 startColonIndex = -1; startColonIndex < colonCount; 905 startColonIndex++) { 906 int32 startIndex; 907 if (startColonIndex < 0) { 908 startIndex = 0; 909 _start = originalStart; 910 } else { 911 startIndex = colonPositions[startColonIndex].index + 1; 912 _start = colonPositions[startColonIndex].position; 913 if (_start >= pos) 914 break; 915 _start.x++; 916 // Note: This is potentially a non-normalized position (i.e. 917 // the end of a soft-wrapped line). While not that nice, it 918 // works anyway. 919 } 920 921 for (int32 endColonIndex = colonCount; endColonIndex > startColonIndex; 922 endColonIndex--) { 923 int32 endIndex; 924 if (endColonIndex == colonCount) { 925 endIndex = originalText.Length(); 926 _end = originalEnd; 927 } else { 928 endIndex = colonPositions[endColonIndex].index; 929 _end = colonPositions[endColonIndex].position; 930 if (_end <= pos) 931 break; 932 } 933 934 originalText.CopyInto(text, startIndex, endIndex - startIndex); 935 if (text.IsEmpty()) 936 continue; 937 938 // check, whether the file exists 939 BString actualPath; 940 if (_EntryExists(text, actualPath)) { 941 _link = HyperLink(text, actualPath, HyperLink::TYPE_PATH); 942 return true; 943 } 944 945 // As such this isn't an existing path. We also want to recognize: 946 // * "<path>:<line>" 947 // * "<path>:<line>:<column>" 948 949 BString path = text; 950 951 for (int32 i = 0; i < 2; i++) { 952 int32 colonIndex = path.FindLast(':'); 953 if (colonIndex <= 0 || colonIndex == path.Length() - 1) 954 break; 955 956 char* numberEnd; 957 strtol(path.String() + colonIndex + 1, &numberEnd, 0); 958 if (*numberEnd != '\0') 959 break; 960 961 path.Truncate(colonIndex); 962 if (_EntryExists(path, actualPath)) { 963 BString address = path == actualPath 964 ? text 965 : BString(actualPath) << (text.String() + colonIndex); 966 _link = HyperLink(text, address, 967 i == 0 968 ? HyperLink::TYPE_PATH_WITH_LINE 969 : HyperLink::TYPE_PATH_WITH_LINE_AND_COLUMN); 970 return true; 971 } 972 } 973 } 974 } 975 976 return false; 977 } 978 979 980 bool 981 TermView::HyperLinkState::_EntryExists(const BString& path, 982 BString& _actualPath) const 983 { 984 if (path.IsEmpty()) 985 return false; 986 987 if (path[0] == '/' || fCurrentDirectory.IsEmpty()) { 988 _actualPath = path; 989 } else if (path == "~" || path.StartsWith("~/")) { 990 // Replace '~' with the user's home directory. We don't handle "~user" 991 // here yet. 992 BPath homeDirectory; 993 if (find_directory(B_USER_DIRECTORY, &homeDirectory) != B_OK) 994 return false; 995 _actualPath = homeDirectory.Path(); 996 _actualPath << path.String() + 1; 997 } else { 998 _actualPath.Truncate(0); 999 _actualPath << fCurrentDirectory << '/' << path; 1000 } 1001 1002 struct stat st; 1003 return lstat(_actualPath, &st) == 0; 1004 } 1005 1006 1007 void 1008 TermView::HyperLinkState::_UpdateHighlight() 1009 { 1010 BPoint where; 1011 uint32 buttons; 1012 fView->GetMouse(&where, &buttons, false); 1013 _UpdateHighlight(where, fView->fModifiers); 1014 } 1015 1016 1017 void 1018 TermView::HyperLinkState::_UpdateHighlight(BPoint where, int32 modifiers) 1019 { 1020 TermPos start; 1021 TermPos end; 1022 HyperLink link; 1023 1024 bool pathPrefixOnly = (modifiers & B_SHIFT_KEY) != 0; 1025 if (_GetHyperLinkAt(where, pathPrefixOnly, link, start, end)) 1026 _ActivateHighlight(start, end); 1027 else 1028 _DeactivateHighlight(); 1029 } 1030 1031 1032 void 1033 TermView::HyperLinkState::_ActivateHighlight(const TermPos& start, 1034 const TermPos& end) 1035 { 1036 if (fHighlightActive) { 1037 if (fHighlight.Start() == start && fHighlight.End() == end) 1038 return; 1039 1040 _DeactivateHighlight(); 1041 } 1042 1043 fHighlight.SetRange(start, end); 1044 fView->_AddHighlight(&fHighlight); 1045 BCursor cursor(B_CURSOR_ID_FOLLOW_LINK); 1046 fView->SetViewCursor(&cursor); 1047 fHighlightActive = true; 1048 } 1049 1050 1051 void 1052 TermView::HyperLinkState::_DeactivateHighlight() 1053 { 1054 if (fHighlightActive) { 1055 fView->_RemoveHighlight(&fHighlight); 1056 BCursor cursor(B_CURSOR_ID_SYSTEM_DEFAULT); 1057 fView->SetViewCursor(&cursor); 1058 fHighlightActive = false; 1059 } 1060 } 1061 1062 1063 // #pragma mark - HyperLinkMenuState 1064 1065 1066 class TermView::HyperLinkMenuState::PopUpMenu : public BPopUpMenu { 1067 public: 1068 PopUpMenu(const BMessenger& messageTarget) 1069 : 1070 BPopUpMenu("open hyperlink"), 1071 fMessageTarget(messageTarget) 1072 { 1073 SetAsyncAutoDestruct(true); 1074 } 1075 1076 ~PopUpMenu() 1077 { 1078 fMessageTarget.SendMessage(kMessageMenuClosed); 1079 } 1080 1081 private: 1082 BMessenger fMessageTarget; 1083 }; 1084 1085 1086 TermView::HyperLinkMenuState::HyperLinkMenuState(TermView* view) 1087 : 1088 State(view), 1089 fLink() 1090 { 1091 } 1092 1093 1094 void 1095 TermView::HyperLinkMenuState::Prepare(BPoint point, const HyperLink& link) 1096 { 1097 fLink = link; 1098 1099 // open context menu 1100 PopUpMenu* menu = new PopUpMenu(fView); 1101 BLayoutBuilder::Menu<> menuBuilder(menu); 1102 switch (link.GetType()) { 1103 case HyperLink::TYPE_URL: 1104 menuBuilder 1105 .AddItem(B_TRANSLATE("Open link"), kMessageOpenLink) 1106 .AddItem(B_TRANSLATE("Copy link location"), kMessageCopyLink); 1107 break; 1108 1109 case HyperLink::TYPE_PATH: 1110 case HyperLink::TYPE_PATH_WITH_LINE: 1111 case HyperLink::TYPE_PATH_WITH_LINE_AND_COLUMN: 1112 menuBuilder.AddItem(B_TRANSLATE("Open path"), kMessageOpenLink); 1113 menuBuilder.AddItem(B_TRANSLATE("Copy path"), kMessageCopyLink); 1114 if (fLink.Text() != fLink.Address()) { 1115 menuBuilder.AddItem(B_TRANSLATE("Copy absolute path"), 1116 kMessageCopyAbsolutePath); 1117 } 1118 break; 1119 } 1120 menu->SetTargetForItems(fView); 1121 menu->Go(fView->ConvertToScreen(point), true, true, true); 1122 } 1123 1124 1125 void 1126 TermView::HyperLinkMenuState::Exited() 1127 { 1128 fLink = HyperLink(); 1129 } 1130 1131 1132 bool 1133 TermView::HyperLinkMenuState::MessageReceived(BMessage* message) 1134 { 1135 switch (message->what) { 1136 case kMessageOpenLink: 1137 if (fLink.IsValid()) 1138 fLink.Open(); 1139 return true; 1140 1141 case kMessageCopyLink: 1142 case kMessageCopyAbsolutePath: 1143 { 1144 if (fLink.IsValid()) { 1145 BString toCopy = message->what == kMessageCopyLink 1146 ? fLink.Text() : fLink.Address(); 1147 1148 if (!be_clipboard->Lock()) 1149 return true; 1150 1151 be_clipboard->Clear(); 1152 1153 if (BMessage *data = be_clipboard->Data()) { 1154 data->AddData("text/plain", B_MIME_TYPE, toCopy.String(), 1155 toCopy.Length()); 1156 be_clipboard->Commit(); 1157 } 1158 1159 be_clipboard->Unlock(); 1160 } 1161 return true; 1162 } 1163 1164 case kMessageMenuClosed: 1165 fView->_NextState(fView->fDefaultState); 1166 return true; 1167 } 1168 1169 return false; 1170 } 1171