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