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