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