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