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
State(TermView * view)71 TermView::State::State(TermView* view)
72 :
73 fView(view)
74 {
75 }
76
77
~State()78 TermView::State::~State()
79 {
80 }
81
82
83 void
Entered()84 TermView::State::Entered()
85 {
86 }
87
88
89 void
Exited()90 TermView::State::Exited()
91 {
92 }
93
94
95 bool
MessageReceived(BMessage * message)96 TermView::State::MessageReceived(BMessage* message)
97 {
98 return false;
99 }
100
101
102 void
ModifiersChanged(int32 oldModifiers,int32 modifiers)103 TermView::State::ModifiersChanged(int32 oldModifiers, int32 modifiers)
104 {
105 }
106
107
108 void
KeyDown(const char * bytes,int32 numBytes)109 TermView::State::KeyDown(const char* bytes, int32 numBytes)
110 {
111 }
112
113
114 void
MouseDown(BPoint where,int32 buttons,int32 modifiers)115 TermView::State::MouseDown(BPoint where, int32 buttons, int32 modifiers)
116 {
117 }
118
119
120 void
MouseMoved(BPoint where,uint32 transit,const BMessage * message,int32 modifiers)121 TermView::State::MouseMoved(BPoint where, uint32 transit,
122 const BMessage* message, int32 modifiers)
123 {
124 }
125
126
127 void
MouseUp(BPoint where,int32 buttons)128 TermView::State::MouseUp(BPoint where, int32 buttons)
129 {
130 }
131
132
133 void
WindowActivated(bool active)134 TermView::State::WindowActivated(bool active)
135 {
136 }
137
138
139 void
VisibleTextBufferChanged()140 TermView::State::VisibleTextBufferChanged()
141 {
142 }
143
144
145 // #pragma mark - StandardBaseState
146
147
StandardBaseState(TermView * view)148 TermView::StandardBaseState::StandardBaseState(TermView* view)
149 :
150 State(view)
151 {
152 }
153
154
155 bool
_StandardMouseMoved(BPoint where,int32 modifiers)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
DefaultState(TermView * view)182 TermView::DefaultState::DefaultState(TermView* view)
183 :
184 StandardBaseState(view)
185 {
186 }
187
188
189 void
ModifiersChanged(int32 oldModifiers,int32 modifiers)190 TermView::DefaultState::ModifiersChanged(int32 oldModifiers, int32 modifiers)
191 {
192 _CheckEnterHyperLinkState(modifiers);
193 }
194
195
196 void
KeyDown(const char * bytes,int32 numBytes)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
MouseDown(BPoint where,int32 buttons,int32 modifiers)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
MouseMoved(BPoint where,uint32 transit,const BMessage * dragMessage,int32 modifiers)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
MouseUp(BPoint where,int32 buttons)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
WindowActivated(bool active)469 TermView::DefaultState::WindowActivated(bool active)
470 {
471 if (active)
472 _CheckEnterHyperLinkState(fView->fModifiers);
473 }
474
475
476 bool
_CheckEnterHyperLinkState(int32 modifiers)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
SelectState(TermView * view)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
Prepare(BPoint where,int32 modifiers)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
MessageReceived(BMessage * message)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
MouseMoved(BPoint where,uint32 transit,const BMessage * message,int32 modifiers)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
MouseUp(BPoint where,int32 buttons)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
_AutoScrollUpdate()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
HyperLinkState(TermView * view)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
Entered()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
Exited()732 TermView::HyperLinkState::Exited()
733 {
734 _DeactivateHighlight();
735 }
736
737
738 void
ModifiersChanged(int32 oldModifiers,int32 modifiers)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
MouseDown(BPoint where,int32 buttons,int32 modifiers)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
MouseMoved(BPoint where,uint32 transit,const BMessage * message,int32 modifiers)770 TermView::HyperLinkState::MouseMoved(BPoint where, uint32 transit,
771 const BMessage* message, int32 modifiers)
772 {
773 _UpdateHighlight(where, modifiers);
774 }
775
776
777 void
WindowActivated(bool active)778 TermView::HyperLinkState::WindowActivated(bool active)
779 {
780 if (!active)
781 fView->_NextState(fView->fDefaultState);
782 }
783
784
785 void
VisibleTextBufferChanged()786 TermView::HyperLinkState::VisibleTextBufferChanged()
787 {
788 _UpdateHighlight();
789 }
790
791
792 rgb_color
ForegroundColor()793 TermView::HyperLinkState::ForegroundColor()
794 {
795 return make_color(0, 0, 255);
796 }
797
798
799 rgb_color
BackgroundColor()800 TermView::HyperLinkState::BackgroundColor()
801 {
802 return fView->fTextBackColor;
803 }
804
805
806 uint32
AdjustTextAttributes(uint32 attributes)807 TermView::HyperLinkState::AdjustTextAttributes(uint32 attributes)
808 {
809 return attributes | UNDERLINE;
810 }
811
812
813 bool
_GetHyperLinkAt(BPoint where,bool pathPrefixOnly,HyperLink & _link,TermPos & _start,TermPos & _end)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
_EntryExists(const BString & path,BString & _actualPath) const982 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
_UpdateHighlight()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
_UpdateHighlight(BPoint where,int32 modifiers)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
_ActivateHighlight(const TermPos & start,const TermPos & end)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
_DeactivateHighlight()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:
PopUpMenu(const BMessenger & messageTarget)1069 PopUpMenu(const BMessenger& messageTarget)
1070 :
1071 BPopUpMenu("open hyperlink"),
1072 fMessageTarget(messageTarget)
1073 {
1074 SetAsyncAutoDestruct(true);
1075 }
1076
~PopUpMenu()1077 ~PopUpMenu()
1078 {
1079 fMessageTarget.SendMessage(kMessageMenuClosed);
1080 }
1081
1082 private:
1083 BMessenger fMessageTarget;
1084 };
1085
1086
HyperLinkMenuState(TermView * view)1087 TermView::HyperLinkMenuState::HyperLinkMenuState(TermView* view)
1088 :
1089 State(view),
1090 fLink()
1091 {
1092 }
1093
1094
1095 void
Prepare(BPoint point,const HyperLink & link)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
Exited()1127 TermView::HyperLinkMenuState::Exited()
1128 {
1129 fLink = HyperLink();
1130 }
1131
1132
1133 bool
MessageReceived(BMessage * message)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