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