xref: /haiku/src/apps/terminal/TermViewStates.cpp (revision 02354704729d38c3b078c696adc1bbbd33cbcf72)
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) {
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);
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::WindowActivated(bool active)
457 {
458 	if (active)
459 		_CheckEnterHyperLinkState(fView->fModifiers);
460 }
461 
462 
463 bool
464 TermView::DefaultState::_CheckEnterHyperLinkState(int32 modifiers)
465 {
466 	if ((modifiers & B_COMMAND_KEY) != 0 && fView->Window()->IsActive()) {
467 		fView->_NextState(fView->fHyperLinkState);
468 		return true;
469 	}
470 
471 	return false;
472 }
473 
474 
475 // #pragma mark - SelectState
476 
477 
478 TermView::SelectState::SelectState(TermView* view)
479 	:
480 	StandardBaseState(view),
481 	fSelectGranularity(SELECT_CHARS),
482 	fCheckMouseTracking(false),
483 	fMouseTracking(false)
484 {
485 }
486 
487 
488 void
489 TermView::SelectState::Prepare(BPoint where, int32 modifiers)
490 {
491 	int32 clicks;
492 	fView->Window()->CurrentMessage()->FindInt32("clicks", &clicks);
493 
494 	if (fView->_HasSelection()) {
495 		TermPos inPos = fView->_ConvertToTerminal(where);
496 		if (fView->fSelection.RangeContains(inPos)) {
497 			if (modifiers & B_CONTROL_KEY) {
498 				BPoint p;
499 				uint32 bt;
500 				do {
501 					fView->GetMouse(&p, &bt);
502 
503 					if (bt == 0) {
504 						fView->_Deselect();
505 						return;
506 					}
507 
508 					snooze(40000);
509 
510 				} while (abs((int)(where.x - p.x)) < 4
511 					&& abs((int)(where.y - p.y)) < 4);
512 
513 				fView->InitiateDrag();
514 				return;
515 			}
516 		}
517 	}
518 
519 	// If mouse has moved too much, disable double/triple click.
520 	if (fView->_MouseDistanceSinceLastClick(where) > 8)
521 		clicks = 1;
522 
523 	fView->SetMouseEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS,
524 		B_NO_POINTER_HISTORY | B_LOCK_WINDOW_FOCUS);
525 
526 	TermPos clickPos = fView->_ConvertToTerminal(where);
527 
528 	if (modifiers & B_SHIFT_KEY) {
529 		fView->fInitialSelectionStart = clickPos;
530 		fView->fInitialSelectionEnd = clickPos;
531 		fView->_ExtendSelection(fView->fInitialSelectionStart, true, false);
532 	} else {
533 		fView->_Deselect();
534 		fView->fInitialSelectionStart = clickPos;
535 		fView->fInitialSelectionEnd = clickPos;
536 	}
537 
538 	// If clicks larger than 3, reset mouse click counter.
539 	clicks = (clicks - 1) % 3 + 1;
540 
541 	switch (clicks) {
542 		case 1:
543 			fCheckMouseTracking = true;
544 			fSelectGranularity = SELECT_CHARS;
545 			break;
546 
547 		case 2:
548 			fView->_SelectWord(where, (modifiers & B_SHIFT_KEY) != 0, false);
549 			fMouseTracking = true;
550 			fSelectGranularity = SELECT_WORDS;
551 			break;
552 
553 		case 3:
554 			fView->_SelectLine(where, (modifiers & B_SHIFT_KEY) != 0, false);
555 			fMouseTracking = true;
556 			fSelectGranularity = SELECT_LINES;
557 			break;
558 	}
559 }
560 
561 
562 bool
563 TermView::SelectState::MessageReceived(BMessage* message)
564 {
565 	if (message->what == kAutoScroll) {
566 		_AutoScrollUpdate();
567 		return true;
568 	}
569 
570 	return false;
571 }
572 
573 
574 void
575 TermView::SelectState::MouseMoved(BPoint where, uint32 transit,
576 	const BMessage* message, int32 modifiers)
577 {
578 	if (_StandardMouseMoved(where, modifiers))
579 		return;
580 
581 	if (fCheckMouseTracking) {
582 		if (fView->_MouseDistanceSinceLastClick(where) > 9)
583 			fMouseTracking = true;
584 	}
585 	if (!fMouseTracking)
586 		return;
587 
588 	bool doAutoScroll = false;
589 
590 	if (where.y < 0) {
591 		doAutoScroll = true;
592 		fView->fAutoScrollSpeed = where.y;
593 		where.x = 0;
594 		where.y = 0;
595 	}
596 
597 	BRect bounds(fView->Bounds());
598 	if (where.y > bounds.bottom) {
599 		doAutoScroll = true;
600 		fView->fAutoScrollSpeed = where.y - bounds.bottom;
601 		where.x = bounds.right;
602 		where.y = bounds.bottom;
603 	}
604 
605 	if (doAutoScroll) {
606 		if (fView->fAutoScrollRunner == NULL) {
607 			BMessage message(kAutoScroll);
608 			fView->fAutoScrollRunner = new (std::nothrow) BMessageRunner(
609 				BMessenger(fView), &message, 10000);
610 		}
611 	} else {
612 		delete fView->fAutoScrollRunner;
613 		fView->fAutoScrollRunner = NULL;
614 	}
615 
616 	switch (fSelectGranularity) {
617 		case SELECT_CHARS:
618 		{
619 			// If we just start selecting, we first select the initially
620 			// hit char, so that we get a proper initial selection -- the char
621 			// in question, which will thus always be selected, regardless of
622 			// whether selecting forward or backward.
623 			if (fView->fInitialSelectionStart == fView->fInitialSelectionEnd) {
624 				fView->_Select(fView->fInitialSelectionStart,
625 					fView->fInitialSelectionEnd, true, true);
626 			}
627 
628 			fView->_ExtendSelection(fView->_ConvertToTerminal(where), true,
629 				true);
630 			break;
631 		}
632 		case SELECT_WORDS:
633 			fView->_SelectWord(where, true, true);
634 			break;
635 		case SELECT_LINES:
636 			fView->_SelectLine(where, true, true);
637 			break;
638 	}
639 }
640 
641 
642 void
643 TermView::SelectState::MouseUp(BPoint where, int32 buttons)
644 {
645 	fCheckMouseTracking = false;
646 	fMouseTracking = false;
647 
648 	if (fView->fAutoScrollRunner != NULL) {
649 		delete fView->fAutoScrollRunner;
650 		fView->fAutoScrollRunner = NULL;
651 	}
652 
653 	// When releasing the first mouse button, we copy the selected text to the
654 	// clipboard.
655 
656 	if (fView->fReportAnyMouseEvent || fView->fReportButtonMouseEvent
657 		|| fView->fReportNormalMouseEvent) {
658 		TermPos clickPos = fView->_ConvertToTerminal(where);
659 		fView->_SendMouseEvent(0, 0, clickPos.x, clickPos.y, false);
660 	} else if ((buttons & B_PRIMARY_MOUSE_BUTTON) == 0
661 		&& (fView->fMouseButtons & B_PRIMARY_MOUSE_BUTTON) != 0) {
662 		fView->Copy(fView->fMouseClipboard);
663 	}
664 
665 	fView->_NextState(fView->fDefaultState);
666 }
667 
668 
669 void
670 TermView::SelectState::_AutoScrollUpdate()
671 {
672 	if (fMouseTracking && fView->fAutoScrollRunner != NULL
673 		&& fView->fScrollBar != NULL) {
674 		float value = fView->fScrollBar->Value();
675 		fView->_ScrollTo(value + fView->fAutoScrollSpeed, true);
676 		if (fView->fAutoScrollSpeed < 0) {
677 			fView->_ExtendSelection(
678 				fView->_ConvertToTerminal(BPoint(0, 0)), true, true);
679 		} else {
680 			fView->_ExtendSelection(
681 				fView->_ConvertToTerminal(fView->Bounds().RightBottom()), true,
682 				true);
683 		}
684 	}
685 }
686 
687 
688 // #pragma mark - HyperLinkState
689 
690 
691 TermView::HyperLinkState::HyperLinkState(TermView* view)
692 	:
693 	State(view),
694 	fURLCharClassifier(kURLAdditionalWordCharacters),
695 	fPathComponentCharClassifier(
696 		BString(kDefaultAdditionalWordCharacters).RemoveFirst("/")),
697 	fCurrentDirectory(),
698 	fHighlight(),
699 	fHighlightActive(false)
700 {
701 	fHighlight.SetHighlighter(this);
702 }
703 
704 
705 void
706 TermView::HyperLinkState::Entered()
707 {
708 	ActiveProcessInfo activeProcessInfo;
709 	if (fView->GetActiveProcessInfo(activeProcessInfo))
710 		fCurrentDirectory = activeProcessInfo.CurrentDirectory();
711 	else
712 		fCurrentDirectory.Truncate(0);
713 
714 	_UpdateHighlight();
715 }
716 
717 
718 void
719 TermView::HyperLinkState::Exited()
720 {
721 	_DeactivateHighlight();
722 }
723 
724 
725 void
726 TermView::HyperLinkState::ModifiersChanged(int32 oldModifiers, int32 modifiers)
727 {
728 	if ((modifiers & B_COMMAND_KEY) == 0)
729 		fView->_NextState(fView->fDefaultState);
730 	else
731 		_UpdateHighlight();
732 }
733 
734 
735 void
736 TermView::HyperLinkState::MouseDown(BPoint where, int32 buttons,
737 	int32 modifiers)
738 {
739 	TermPos start;
740 	TermPos end;
741 	HyperLink link;
742 
743 	bool pathPrefixOnly = (modifiers & B_SHIFT_KEY) != 0;
744 	if (!_GetHyperLinkAt(where, pathPrefixOnly, link, start, end))
745 		return;
746 
747 	if ((buttons & B_PRIMARY_MOUSE_BUTTON) != 0) {
748 		link.Open();
749 	} else if ((buttons & B_SECONDARY_MOUSE_BUTTON) != 0) {
750 		fView->fHyperLinkMenuState->Prepare(where, link);
751 		fView->_NextState(fView->fHyperLinkMenuState);
752 	}
753 }
754 
755 
756 void
757 TermView::HyperLinkState::MouseMoved(BPoint where, uint32 transit,
758 	const BMessage* message, int32 modifiers)
759 {
760 	_UpdateHighlight(where, modifiers);
761 }
762 
763 
764 void
765 TermView::HyperLinkState::WindowActivated(bool active)
766 {
767 	if (!active)
768 		fView->_NextState(fView->fDefaultState);
769 }
770 
771 
772 void
773 TermView::HyperLinkState::VisibleTextBufferChanged()
774 {
775 	_UpdateHighlight();
776 }
777 
778 
779 rgb_color
780 TermView::HyperLinkState::ForegroundColor()
781 {
782 	return make_color(0, 0, 255);
783 }
784 
785 
786 rgb_color
787 TermView::HyperLinkState::BackgroundColor()
788 {
789 	return fView->fTextBackColor;
790 }
791 
792 
793 uint32
794 TermView::HyperLinkState::AdjustTextAttributes(uint32 attributes)
795 {
796 	return attributes | UNDERLINE;
797 }
798 
799 
800 bool
801 TermView::HyperLinkState::_GetHyperLinkAt(BPoint where, bool pathPrefixOnly,
802 	HyperLink& _link, TermPos& _start, TermPos& _end)
803 {
804 	TerminalBuffer* textBuffer = fView->fTextBuffer;
805 	BAutolock textBufferLocker(textBuffer);
806 
807 	TermPos pos = fView->_ConvertToTerminal(where);
808 
809 	// try to get a URL first
810 	BString text;
811 	if (!textBuffer->FindWord(pos, &fURLCharClassifier, false, _start, _end))
812 		return false;
813 
814 	text.Truncate(0);
815 	textBuffer->GetStringFromRegion(text, _start, _end);
816 	text.Trim();
817 
818 	// We're only happy, if it has a protocol part which we know.
819 	int32 colonIndex = text.FindFirst(':');
820 	if (colonIndex >= 0) {
821 		BString protocol(text, colonIndex);
822 		if (strstr(kKnownURLProtocols, protocol) != NULL) {
823 			_link = HyperLink(text, HyperLink::TYPE_URL);
824 			return true;
825 		}
826 	}
827 
828 	// no obvious URL -- try file name
829 	if (!textBuffer->FindWord(pos, fView->fCharClassifier, false, _start, _end))
830 		return false;
831 
832 	// In path-prefix-only mode we determine the end position anew by omitting
833 	// the '/' in the allowed word chars.
834 	if (pathPrefixOnly) {
835 		TermPos componentStart;
836 		TermPos componentEnd;
837 		if (textBuffer->FindWord(pos, &fPathComponentCharClassifier, false,
838 				componentStart, componentEnd)) {
839 			_end = componentEnd;
840 		} else {
841 			// That means pos points to a '/'. We simply use the previous
842 			// position.
843 			_end = pos;
844 			if (_start == _end) {
845 				// Well, must be just "/". Advance to the next position.
846 				if (!textBuffer->NextLinePos(_end, false))
847 					return false;
848 			}
849 		}
850 	}
851 
852 	text.Truncate(0);
853 	textBuffer->GetStringFromRegion(text, _start, _end);
854 	text.Trim();
855 	if (text.IsEmpty())
856 		return false;
857 
858 	// Collect a list of colons in the string and their respective positions in
859 	// the text buffer. We do this up-front so we can unlock the text buffer
860 	// while we're doing all the entry existence tests.
861 	typedef Array<CharPosition> ColonList;
862 	ColonList colonPositions;
863 	TermPos searchPos = _start;
864 	for (int32 index = 0; (index = text.FindFirst(':', index)) >= 0;) {
865 		TermPos foundStart;
866 		TermPos foundEnd;
867 		if (!textBuffer->Find(":", searchPos, true, true, false, foundStart,
868 				foundEnd)) {
869 			return false;
870 		}
871 
872 		CharPosition colonPosition;
873 		colonPosition.index = index;
874 		colonPosition.position = foundStart;
875 		if (!colonPositions.Add(colonPosition))
876 			return false;
877 
878 		index++;
879 		searchPos = foundEnd;
880 	}
881 
882 	textBufferLocker.Unlock();
883 
884 	// Since we also want to consider ':' a potential path delimiter, in two
885 	// nested loops we chop off components from the beginning respective the
886 	// end.
887 	BString originalText = text;
888 	TermPos originalStart = _start;
889 	TermPos originalEnd = _end;
890 
891 	int32 colonCount = colonPositions.Count();
892 	for (int32 startColonIndex = -1; startColonIndex < colonCount;
893 			startColonIndex++) {
894 		int32 startIndex;
895 		if (startColonIndex < 0) {
896 			startIndex = 0;
897 			_start = originalStart;
898 		} else {
899 			startIndex = colonPositions[startColonIndex].index + 1;
900 			_start = colonPositions[startColonIndex].position;
901 			if (_start >= pos)
902 				break;
903 			_start.x++;
904 				// Note: This is potentially a non-normalized position (i.e.
905 				// the end of a soft-wrapped line). While not that nice, it
906 				// works anyway.
907 		}
908 
909 		for (int32 endColonIndex = colonCount; endColonIndex > startColonIndex;
910 				endColonIndex--) {
911 			int32 endIndex;
912 			if (endColonIndex == colonCount) {
913 				endIndex = originalText.Length();
914 				_end = originalEnd;
915 			} else {
916 				endIndex = colonPositions[endColonIndex].index;
917 				_end = colonPositions[endColonIndex].position;
918 				if (_end <= pos)
919 					break;
920 			}
921 
922 			originalText.CopyInto(text, startIndex, endIndex - startIndex);
923 			if (text.IsEmpty())
924 				continue;
925 
926 			// check, whether the file exists
927 			BString actualPath;
928 			if (_EntryExists(text, actualPath)) {
929 				_link = HyperLink(text, actualPath, HyperLink::TYPE_PATH);
930 				return true;
931 			}
932 
933 			// As such this isn't an existing path. We also want to recognize:
934 			// * "<path>:<line>"
935 			// * "<path>:<line>:<column>"
936 
937 			BString path = text;
938 
939 			for (int32 i = 0; i < 2; i++) {
940 				int32 colonIndex = path.FindLast(':');
941 				if (colonIndex <= 0 || colonIndex == path.Length() - 1)
942 					break;
943 
944 				char* numberEnd;
945 				strtol(path.String() + colonIndex + 1, &numberEnd, 0);
946 				if (*numberEnd != '\0')
947 					break;
948 
949 				path.Truncate(colonIndex);
950 				if (_EntryExists(path, actualPath)) {
951 					BString address = path == actualPath
952 						? text
953 						: BString(actualPath) << (text.String() + colonIndex);
954 					_link = HyperLink(text, address,
955 						i == 0
956 							? HyperLink::TYPE_PATH_WITH_LINE
957 							: HyperLink::TYPE_PATH_WITH_LINE_AND_COLUMN);
958 					return true;
959 				}
960 			}
961 		}
962 	}
963 
964 	return false;
965 }
966 
967 
968 bool
969 TermView::HyperLinkState::_EntryExists(const BString& path,
970 	BString& _actualPath) const
971 {
972 	if (path.IsEmpty())
973 		return false;
974 
975 	if (path[0] == '/' || fCurrentDirectory.IsEmpty()) {
976 		_actualPath = path;
977 	} else if (path == "~" || path.StartsWith("~/")) {
978 		// Replace '~' with the user's home directory. We don't handle "~user"
979 		// here yet.
980 		BPath homeDirectory;
981 		if (find_directory(B_USER_DIRECTORY, &homeDirectory) != B_OK)
982 			return false;
983 		_actualPath = homeDirectory.Path();
984 		_actualPath << path.String() + 1;
985 	} else {
986 		_actualPath.Truncate(0);
987 		_actualPath << fCurrentDirectory << '/' << path;
988 	}
989 
990 	struct stat st;
991 	return lstat(_actualPath, &st) == 0;
992 }
993 
994 
995 void
996 TermView::HyperLinkState::_UpdateHighlight()
997 {
998 	BPoint where;
999 	uint32 buttons;
1000 	fView->GetMouse(&where, &buttons, false);
1001 	_UpdateHighlight(where, fView->fModifiers);
1002 }
1003 
1004 
1005 void
1006 TermView::HyperLinkState::_UpdateHighlight(BPoint where, int32 modifiers)
1007 {
1008 	TermPos start;
1009 	TermPos end;
1010 	HyperLink link;
1011 
1012 	bool pathPrefixOnly = (modifiers & B_SHIFT_KEY) != 0;
1013 	if (_GetHyperLinkAt(where, pathPrefixOnly, link, start, end))
1014 		_ActivateHighlight(start, end);
1015 	else
1016 		_DeactivateHighlight();
1017 }
1018 
1019 
1020 void
1021 TermView::HyperLinkState::_ActivateHighlight(const TermPos& start,
1022 	const TermPos& end)
1023 {
1024 	if (fHighlightActive) {
1025 		if (fHighlight.Start() == start && fHighlight.End() == end)
1026 			return;
1027 
1028 		_DeactivateHighlight();
1029 	}
1030 
1031 	fHighlight.SetRange(start, end);
1032 	fView->_AddHighlight(&fHighlight);
1033 	BCursor cursor(B_CURSOR_ID_FOLLOW_LINK);
1034 	fView->SetViewCursor(&cursor);
1035 	fHighlightActive = true;
1036 }
1037 
1038 
1039 void
1040 TermView::HyperLinkState::_DeactivateHighlight()
1041 {
1042 	if (fHighlightActive) {
1043 		fView->_RemoveHighlight(&fHighlight);
1044 		BCursor cursor(B_CURSOR_ID_SYSTEM_DEFAULT);
1045 		fView->SetViewCursor(&cursor);
1046 		fHighlightActive = false;
1047 	}
1048 }
1049 
1050 
1051 // #pragma mark - HyperLinkMenuState
1052 
1053 
1054 class TermView::HyperLinkMenuState::PopUpMenu : public BPopUpMenu {
1055 public:
1056 	PopUpMenu(const BMessenger& messageTarget)
1057 		:
1058 		BPopUpMenu("open hyperlink"),
1059 		fMessageTarget(messageTarget)
1060 	{
1061 		SetAsyncAutoDestruct(true);
1062 	}
1063 
1064 	~PopUpMenu()
1065 	{
1066 		fMessageTarget.SendMessage(kMessageMenuClosed);
1067 	}
1068 
1069 private:
1070 	BMessenger	fMessageTarget;
1071 };
1072 
1073 
1074 TermView::HyperLinkMenuState::HyperLinkMenuState(TermView* view)
1075 	:
1076 	State(view),
1077 	fLink()
1078 {
1079 }
1080 
1081 
1082 void
1083 TermView::HyperLinkMenuState::Prepare(BPoint point, const HyperLink& link)
1084 {
1085 	fLink = link;
1086 
1087 	// open context menu
1088 	PopUpMenu* menu = new PopUpMenu(fView);
1089 	BLayoutBuilder::Menu<> menuBuilder(menu);
1090 	switch (link.GetType()) {
1091 		case HyperLink::TYPE_URL:
1092 			menuBuilder
1093 				.AddItem(B_TRANSLATE("Open link"), kMessageOpenLink)
1094 				.AddItem(B_TRANSLATE("Copy link location"), kMessageCopyLink);
1095 			break;
1096 
1097 		case HyperLink::TYPE_PATH:
1098 		case HyperLink::TYPE_PATH_WITH_LINE:
1099 		case HyperLink::TYPE_PATH_WITH_LINE_AND_COLUMN:
1100 			menuBuilder.AddItem(B_TRANSLATE("Open path"), kMessageOpenLink);
1101 			menuBuilder.AddItem(B_TRANSLATE("Copy path"), kMessageCopyLink);
1102 			if (fLink.Text() != fLink.Address()) {
1103 				menuBuilder.AddItem(B_TRANSLATE("Copy absolute path"),
1104 					kMessageCopyAbsolutePath);
1105 			}
1106 			break;
1107 	}
1108 	menu->SetTargetForItems(fView);
1109 	menu->Go(fView->ConvertToScreen(point), true, true, true);
1110 }
1111 
1112 
1113 void
1114 TermView::HyperLinkMenuState::Exited()
1115 {
1116 	fLink = HyperLink();
1117 }
1118 
1119 
1120 bool
1121 TermView::HyperLinkMenuState::MessageReceived(BMessage* message)
1122 {
1123 	switch (message->what) {
1124 		case kMessageOpenLink:
1125 			if (fLink.IsValid())
1126 				fLink.Open();
1127 			return true;
1128 
1129 		case kMessageCopyLink:
1130 		case kMessageCopyAbsolutePath:
1131 		{
1132 			if (fLink.IsValid()) {
1133 				BString toCopy = message->what == kMessageCopyLink
1134 					? fLink.Text() : fLink.Address();
1135 
1136 				if (!be_clipboard->Lock())
1137 					return true;
1138 
1139 				be_clipboard->Clear();
1140 
1141 				if (BMessage *data = be_clipboard->Data()) {
1142 					data->AddData("text/plain", B_MIME_TYPE, toCopy.String(),
1143 						toCopy.Length());
1144 					be_clipboard->Commit();
1145 				}
1146 
1147 				be_clipboard->Unlock();
1148 			}
1149 			return true;
1150 		}
1151 
1152 		case kMessageMenuClosed:
1153 			fView->_NextState(fView->fDefaultState);
1154 			return true;
1155 	}
1156 
1157 	return false;
1158 }
1159