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