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