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