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