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