From e9bad28aafc6b71378bb71139cde6269bbb0afa7 Mon Sep 17 00:00:00 2001 From: Ingo Weinhold Date: Sat, 11 May 2013 01:09:17 +0200 Subject: [PATCH] Terminal: Add a hyperlink mode When holding down Command, text under the mouse is checked whether it looks like a URL or a local path. If so, it is highlighted and can be clicked, which will open the URL/file. Right-clicking opens a context menu with items for opening the link/file or copying it to the clipboard. When additionally holding down Shift, path prefixes up to the component under the mouse will be considered (no effect for URLs). Changes: * Add HyperLink class. Encapsulates a type, the address, and an optional base address. Features an Open() method to open the address. * Move/add some string constants to TermConst. * Move TermView::CharClassifier to top level and rename to DefaultCharClassifier. * Introduce TermViewHighlight and TermViewHighlighter. The former refers to a range of text in a TermView's text buffer. It also contains a pointer to a TermViewHighlighter object, which specifies how the text range shall be rendered (colors and attributes). * TermView: - Add respective _{Add,Remove}Highlight() methods and adjust the code to support highlights. - Make the selection a TermViewHighlight. At least its visual aspect is now handled like other highlights. - Introduce an inner TextBufferSyncLocker. It is used instead of BAutolock when locking the text buffer to synchronize the visual buffer with it. After it unlocks it calls _VisibleTextBufferChanged(), if the visual text buffer has changed, which in turn calls a new callback on the active state. - Add WindowActivated() and ModifiersChanged() callbacks to the state interface. - Add new states HyperLinkState and HyperLinkMenuState which implement the new feature. Fix modifier issues --- src/apps/terminal/HyperLink.cpp | 45 ++ src/apps/terminal/HyperLink.h | 41 ++ src/apps/terminal/Jamfile | 2 + src/apps/terminal/TermConst.cpp | 4 + src/apps/terminal/TermConst.h | 4 + src/apps/terminal/TermView.cpp | 361 ++++++++++----- src/apps/terminal/TermView.h | 48 +- src/apps/terminal/TermViewHighlight.cpp | 19 + src/apps/terminal/TermViewHighlight.h | 83 ++++ src/apps/terminal/TermViewStates.cpp | 462 ++++++++++++++++++- src/apps/terminal/TermViewStates.h | 88 +++- src/apps/terminal/TerminalCharClassifier.cpp | 43 +- src/apps/terminal/TerminalCharClassifier.h | 19 +- 13 files changed, 1067 insertions(+), 152 deletions(-) create mode 100644 src/apps/terminal/HyperLink.cpp create mode 100644 src/apps/terminal/HyperLink.h create mode 100644 src/apps/terminal/TermViewHighlight.cpp create mode 100644 src/apps/terminal/TermViewHighlight.h diff --git a/src/apps/terminal/HyperLink.cpp b/src/apps/terminal/HyperLink.cpp new file mode 100644 index 0000000000..97e294f7eb --- /dev/null +++ b/src/apps/terminal/HyperLink.cpp @@ -0,0 +1,45 @@ +/* + * Copyright 2013, Ingo Weinhold, ingo_weinhold@gmx.de. + * Distributed under the terms of the MIT License. + */ + + +#include "HyperLink.h" + +#include +#include + +#include "TermConst.h" + + +HyperLink::HyperLink() + : + fAddress(), + fType(TYPE_URL) +{ +} + + +HyperLink::HyperLink(const BString& address, Type type, + const BString& baseAddress) + : + fAddress(address), + fBaseAddress(baseAddress.IsEmpty() ? address : baseAddress), + fType(type) +{ +} + + +status_t +HyperLink::Open() +{ + if (!IsValid()) + return B_BAD_VALUE; + + // open with the "open" program + BString address(fAddress); + address.CharacterEscape(kShellEscapeCharacters, '\\'); + BString commandLine; + commandLine.SetToFormat("/bin/open %s", address.String()); + return system(commandLine) == 0 ? B_OK : errno; +} diff --git a/src/apps/terminal/HyperLink.h b/src/apps/terminal/HyperLink.h new file mode 100644 index 0000000000..735c84c50f --- /dev/null +++ b/src/apps/terminal/HyperLink.h @@ -0,0 +1,41 @@ +/* + * Copyright 2013, Ingo Weinhold, ingo_weinhold@gmx.de. + * Distributed under the terms of the MIT License. + */ +#ifndef HYPER_LINK_H +#define HYPER_LINK_H + + +#include + + +class HyperLink { +public: + enum Type { + TYPE_URL, + TYPE_PATH, + TYPE_PATH_WITH_LINE, + TYPE_PATH_WITH_LINE_AND_COLUMN + }; + +public: + HyperLink(); + HyperLink(const BString& address, Type type, + const BString& baseAddress = BString()); + + bool IsValid() const { return !fAddress.IsEmpty(); } + + const BString& Address() const { return fAddress; } + const BString& BaseAddress() const { return fBaseAddress; } + Type GetType() const { return fType; } + + status_t Open(); + +private: + BString fAddress; + BString fBaseAddress; + Type fType; +}; + + +#endif // HYPER_LINK_H diff --git a/src/apps/terminal/Jamfile b/src/apps/terminal/Jamfile index 7a480d917c..98a062afa8 100644 --- a/src/apps/terminal/Jamfile +++ b/src/apps/terminal/Jamfile @@ -16,6 +16,7 @@ Application Terminal : FindWindow.cpp Globals.cpp HistoryBuffer.cpp + HyperLink.cpp InlineInput.cpp PatternEvaluator.cpp PrefHandler.cpp @@ -33,6 +34,7 @@ Application Terminal : TermParse.cpp TermScrollView.cpp TermView.cpp + TermViewHighlight.cpp TermViewStates.cpp TermWindow.cpp TitlePlaceholderMapper.cpp diff --git a/src/apps/terminal/TermConst.cpp b/src/apps/terminal/TermConst.cpp index 1b88bc0493..2e2c9cdf6a 100644 --- a/src/apps/terminal/TermConst.cpp +++ b/src/apps/terminal/TermConst.cpp @@ -31,3 +31,7 @@ const char* const kTooTipSetWindowTitlePlaceholders = B_TRANSLATE( "\t%p\t-\tThe name of the active process in the current tab.\n" "\t%t\t-\tThe title of the current tab.\n" "\t%%\t-\tThe character '%'."); + +const char* const kShellEscapeCharacters = " ~`#$&*()\\|[]{};'\"<>?!"; +const char* const kDefaultAdditionalWordCharacters = ":@-./_~"; +const char* const kURLAdditionalWordCharacters = ":/-._~[]?#@!$&'()*+,;="; diff --git a/src/apps/terminal/TermConst.h b/src/apps/terminal/TermConst.h index 942d9215d8..9d1c942f15 100644 --- a/src/apps/terminal/TermConst.h +++ b/src/apps/terminal/TermConst.h @@ -145,6 +145,10 @@ static const char* const PREF_WINDOW_TITLE = "Window title"; extern const char* const kTooTipSetTabTitlePlaceholders; extern const char* const kTooTipSetWindowTitlePlaceholders; +extern const char* const kShellEscapeCharacters; +extern const char* const kDefaultAdditionalWordCharacters; +extern const char* const kURLAdditionalWordCharacters; + // Cursor style enum { diff --git a/src/apps/terminal/TermView.cpp b/src/apps/terminal/TermView.cpp index b448017c37..16590e5fd7 100644 --- a/src/apps/terminal/TermView.cpp +++ b/src/apps/terminal/TermView.cpp @@ -16,7 +16,6 @@ #include "TermView.h" -#include #include #include #include @@ -102,9 +101,6 @@ static const bigtime_t kCursorBlinkInterval = 500000; static const rgb_color kBlackColor = { 0, 0, 0, 255 }; static const rgb_color kWhiteColor = { 255, 255, 255, 255 }; -static const char* kDefaultSpecialWordChars = ":@-./_~"; -static const char* kEscapeCharacters = " ~`#$&*()\\|[]{};'\"<>?!"; - // secondary mouse button drop const int32 kSecondaryMouseDropAction = 'SMDA'; @@ -125,40 +121,28 @@ restrict_value(const Type& value, const Type& min, const Type& max) } -// #pragma mark - CharClassifier +// #pragma mark - TextBufferSyncLocker -class TermView::CharClassifier : public TerminalCharClassifier { +class TermView::TextBufferSyncLocker { public: - CharClassifier(const char* specialWordChars) + TextBufferSyncLocker(TermView* view) + : + fView(view) { - const char* p = specialWordChars; - while (p != NULL && *p) { - int count = UTF8Char::ByteCount(*p); - if (count <= 0 || count > 4) - break; - fSpecialWordChars.push_back(UTF8Char(p, count)); - p += count; - } + fView->fTextBuffer->Lock(); } - virtual int Classify(const UTF8Char& character) + ~TextBufferSyncLocker() { - if (character.IsSpace()) - return CHAR_TYPE_SPACE; + fView->fTextBuffer->Unlock(); - if (character.IsAlNum()) - return CHAR_TYPE_WORD_CHAR; - - if (std::find(fSpecialWordChars.begin(), fSpecialWordChars.end(), - character) != fSpecialWordChars.end()) - return CHAR_TYPE_WORD_CHAR; - - return CHAR_TYPE_WORD_DELIMITER; + if (fView->fVisibleTextBufferChanged) + fView->_VisibleTextBufferChanged(); } private: - std::vector fSpecialWordChars; + TermView* fView; }; @@ -299,6 +283,7 @@ TermView::_InitObject(const ShellParameters& shellParameters) fCursor = TermPos(0, 0); fTextBuffer = NULL; fVisibleTextBuffer = NULL; + fVisibleTextBufferChanged = false; fScrollBar = NULL; fInline = NULL; fSelectForeColor = kWhiteColor; @@ -308,8 +293,8 @@ TermView::_InitObject(const ShellParameters& shellParameters) fScrolledSinceLastSync = 0; fSyncRunner = NULL; fConsiderClockedSync = false; - fSelStart = TermPos(-1, -1); - fSelEnd = TermPos(-1, -1); + fSelection.SetHighlighter(this); + fSelection.SetRange(TermPos(0, 0), TermPos(0, 0)); fPrevPos = TermPos(-1, - 1); fReportX10MouseEvent = false; fReportNormalMouseEvent = false; @@ -318,6 +303,8 @@ TermView::_InitObject(const ShellParameters& shellParameters) fMouseClipboard = be_clipboard; fDefaultState = new(std::nothrow) DefaultState(this); fSelectState = new(std::nothrow) SelectState(this); + fHyperLinkState = new(std::nothrow) HyperLinkState(this); + fHyperLinkMenuState = new(std::nothrow) HyperLinkMenuState(this); fActiveState = NULL; fTextBuffer = new(std::nothrow) TerminalBuffer; @@ -329,8 +316,8 @@ TermView::_InitObject(const ShellParameters& shellParameters) return B_NO_MEMORY; // TODO: Make the special word chars user-settable! - fCharClassifier = new(std::nothrow) CharClassifier( - kDefaultSpecialWordChars); + fCharClassifier = new(std::nothrow) DefaultCharClassifier( + kDefaultAdditionalWordCharacters); if (fCharClassifier == NULL) return B_NO_MEMORY; @@ -362,8 +349,12 @@ TermView::_InitObject(const ShellParameters& shellParameters) if (error < B_OK) return error; - if (fDefaultState == NULL || fSelectState == NULL) + fHighlights.AddItem(&fSelection); + + if (fDefaultState == NULL || fSelectState == NULL || fHyperLinkState == NULL + || fHyperLinkMenuState == NULL) { return B_NO_MEMORY; + } SetLowColor(fTextBackColor); SetViewColor(B_TRANSPARENT_32_BIT); @@ -383,6 +374,8 @@ TermView::~TermView() delete fDefaultState; delete fSelectState; + delete fHyperLinkState; + delete fHyperLinkMenuState; delete fSyncRunner; delete fAutoScrollRunner; delete fCharClassifier; @@ -457,6 +450,20 @@ TermView::Archive(BMessage* data, bool deep) const } +rgb_color +TermView::ForegroundColor() +{ + return fSelectForeColor; +} + + +rgb_color +TermView::BackgroundColor() +{ + return fSelectBackColor; +} + + inline int32 TermView::_LineAt(float y) { @@ -582,12 +589,13 @@ TermView::SetTermSize(int rows, int columns, bool notifyShell) // synchronize the visible text buffer { - BAutolock _(fTextBuffer); + TextBufferSyncLocker _(this); _SynchronizeWithTextBuffer(0, -1); int32 offset = _LineAt(0); fVisibleTextBuffer->SynchronizeWith(fTextBuffer, offset, offset, offset + rows + 2); + fVisibleTextBufferChanged = true; } if (notifyShell) @@ -784,7 +792,8 @@ TermView::Copy(BClipboard *clipboard) return; BString copyStr; - fTextBuffer->GetStringFromRegion(copyStr, fSelStart, fSelEnd); + fTextBuffer->GetStringFromRegion(copyStr, fSelection.Start(), + fSelection.End()); if (clipboard->Lock()) { BMessage *clipMsg = NULL; @@ -934,8 +943,11 @@ TermView::_Deactivate() //! Draw part of a line in the given view. void TermView::_DrawLinePart(int32 x1, int32 y1, uint32 attr, char *buf, - int32 width, bool mouse, bool cursor, BView *inView) + int32 width, Highlight* highlight, bool cursor, BView *inView) { + if (highlight != NULL) + attr = highlight->Highlighter()->AdjustTextAttributes(attr); + inView->SetFont(IS_BOLD(attr) && !fEmulateBold ? &fBoldFont : &fHalfFont); // Set pen point @@ -958,9 +970,9 @@ TermView::_DrawLinePart(int32 x1, int32 y1, uint32 attr, char *buf, if (cursor) { rgb_fore = fCursorForeColor; rgb_back = fCursorBackColor; - } else if (mouse) { - rgb_fore = fSelectForeColor; - rgb_back = fSelectBackColor; + } else if (highlight != NULL) { + rgb_fore = highlight->Highlighter()->ForegroundColor(); + rgb_back = highlight->Highlighter()->BackgroundColor(); } else { // Reverse attribute(If selected area, don't reverse color). if (IS_INVERSE(attr)) { @@ -1025,7 +1037,7 @@ TermView::_DrawCursor() } } - bool selected = _CheckSelectedRegion(TermPos(fCursor.x, fCursor.y)); + Highlight* highlight = _CheckHighlightRegion(TermPos(fCursor.x, fCursor.y)); if (fVisibleTextBuffer->GetChar(fCursor.y - firstVisible, fCursor.x, character, attr) == A_CHAR && (fCursorStyle == BLOCK_CURSOR || !cursorVisible)) { @@ -1037,11 +1049,11 @@ TermView::_DrawCursor() buffer[bytes] = '\0'; _DrawLinePart(fCursor.x * fFontWidth, (int32)rect.top, attr, buffer, - width, selected, cursorVisible, this); + width, highlight, cursorVisible, this); } else { - if (selected) - SetHighColor(fSelectBackColor); - else if (cursorVisible ) + if (highlight != NULL) + SetHighColor(highlight->Highlighter()->BackgroundColor()); + else if (cursorVisible) SetHighColor(fCursorBackColor ); else { uint32 count = 0; @@ -1144,6 +1156,7 @@ void TermView::AttachedToWindow() { fMouseButtons = 0; + fModifiers = modifiers(); // update the terminal size because it may have changed while the TermView // was detached from the window. On such conditions FrameResized was not @@ -1162,7 +1175,7 @@ TermView::AttachedToWindow() &message, 500000); { - BAutolock _(fTextBuffer); + TextBufferSyncLocker _(this); fTextBuffer->SetListener(thisMessenger); _SynchronizeWithTextBuffer(0, -1); } @@ -1240,15 +1253,15 @@ TermView::Draw(BRect updateRect) for (int32 i = k; i <= x2;) { int32 lastColumn = x2; - bool insideSelection = _CheckSelectedRegion(j, i, lastColumn); + Highlight* highlight = _CheckHighlightRegion(j, i, lastColumn); // This will clip lastColumn to the selection start or end // to ensure the selection is not drawn at the same time as // something else int32 count = fVisibleTextBuffer->GetString(j - firstVisible, i, lastColumn, buf, attr); -// debug_printf(" fVisibleTextBuffer->GetString(%ld, %ld, %ld) -> (%ld, \"%.*s\"), selected: %d\n", -// j - firstVisible, i, lastColumn, count, (int)count, buf, insideSelection); +// debug_printf(" fVisibleTextBuffer->GetString(%ld, %ld, %ld) -> (%ld, \"%.*s\"), highlight: %p\n", +// j - firstVisible, i, lastColumn, count, (int)count, buf, highlight); if (count == 0) { // No chars to draw : we just fill the rectangle with the @@ -1258,8 +1271,9 @@ TermView::Draw(BRect updateRect) fFontWidth * nextColumn - 1, 0); rect.bottom = rect.top + fFontHeight - 1; - rgb_color rgb_back = insideSelection - ? fSelectBackColor : fTextBackColor; + rgb_color rgb_back = highlight != NULL + ? highlight->Highlighter()->BackgroundColor() + : fTextBackColor; if (fTextBuffer->IsAlternateScreenActive()) { // alternate screen uses cell attributes @@ -1294,7 +1308,7 @@ TermView::Draw(BRect updateRect) count = FULL_WIDTH; _DrawLinePart(fFontWidth * i, (int32)_LineOffset(j), - attr, buf, count, insideSelection, false, this); + attr, buf, count, highlight, false, this); i += count; } } @@ -1365,6 +1379,15 @@ TermView::WindowActivated(bool active) if (fActive) _Deactivate(); } + + fActiveState->WindowActivated(active); + + if (active) { + int32 oldModifiers = fModifiers; + fModifiers = modifiers(); + if (fModifiers != oldModifiers) + fActiveState->ModifiersChanged(oldModifiers, fModifiers); + } } @@ -1576,6 +1599,15 @@ TermView::MessageReceived(BMessage *msg) break; } + case B_MODIFIERS_CHANGED: + { + int32 oldModifiers = fModifiers; + fModifiers = msg->GetInt32("modifiers", 0); + if (fModifiers != oldModifiers) + fActiveState->ModifiersChanged(oldModifiers, fModifiers); + break; + } + case B_INPUT_METHOD_EVENT: { int32 opcode; @@ -1677,7 +1709,7 @@ TermView::MessageReceived(BMessage *msg) break; case MSG_TERMINAL_BUFFER_CHANGED: { - BAutolock _(fTextBuffer); + TextBufferSyncLocker _(this); _SynchronizeWithTextBuffer(0, -1); break; } @@ -1842,7 +1874,7 @@ TermView::ScrollTo(BPoint where) //debug_printf("fVisibleTextBuffer->ScrollBy(%ld)\n", newFirstLine - oldFirstLine); fVisibleTextBuffer->ScrollBy(newFirstLine - oldFirstLine); } - BAutolock _(fTextBuffer); + TextBufferSyncLocker _(this); if (diff < 0) _SynchronizeWithTextBuffer(newFirstLine, oldFirstLine - 1); else @@ -2004,11 +2036,11 @@ TermView::_DoSecondaryMouseDropAction(BMessage* msg) int32 slash = string.FindLast("/"); string.Truncate(slash); } - string.CharacterEscape(kEscapeCharacters, '\\'); + string.CharacterEscape(kShellEscapeCharacters, '\\'); itemString += string; break; } - string.CharacterEscape(kEscapeCharacters, '\\'); + string.CharacterEscape(kShellEscapeCharacters, '\\'); itemString += string; } @@ -2035,7 +2067,7 @@ TermView::_DoFileDrop(entry_ref& ref) BPath path(&ent); BString string(path.Path()); - string.CharacterEscape(kEscapeCharacters, '\\'); + string.CharacterEscape(kShellEscapeCharacters, '\\'); _WritePTY(string.String(), string.Length()); } @@ -2099,7 +2131,9 @@ TermView::_SynchronizeWithTextBuffer(int32 visibleDirtyTop, // sync time not passed yet -- keep counting fScrolledSinceLastSync += linesScrolled; return; - } else if (fScrolledSinceLastSync + linesScrolled <= fRows) { + } + + if (fScrolledSinceLastSync + linesScrolled <= fRows) { // time's up, but not enough happened delete fSyncRunner; fSyncRunner = NULL; @@ -2111,6 +2145,8 @@ TermView::_SynchronizeWithTextBuffer(int32 visibleDirtyTop, fScrolledSinceLastSync = 0; } + fVisibleTextBufferChanged = true; + // Simple case first -- complete invalidation. if (info.invalidateAll) { Invalidate(); @@ -2194,15 +2230,23 @@ TermView::_SynchronizeWithTextBuffer(int32 visibleDirtyTop, fVisibleTextBuffer->ScrollBy(linesScrolled); } - // move selection - if (fSelStart != fSelEnd) { - fSelStart.y -= linesScrolled; - fSelEnd.y -= linesScrolled; - fInitialSelectionStart.y -= linesScrolled; - fInitialSelectionEnd.y -= linesScrolled; + // move highlights + for (int32 i = 0; Highlight* highlight = fHighlights.ItemAt(i); i++) { + if (highlight->IsEmpty()) + continue; - if (fSelStart.y < -historySize) - _Deselect(); + highlight->ScrollRange(linesScrolled); + if (highlight == &fSelection) { + fInitialSelectionStart.y -= linesScrolled; + fInitialSelectionEnd.y -= linesScrolled; + } + + if (highlight->Start().y < -historySize) { + if (highlight == &fSelection) + _Deselect(); + else + _ClearHighlight(highlight); + } } } @@ -2212,12 +2256,13 @@ TermView::_SynchronizeWithTextBuffer(int32 visibleDirtyTop, info.dirtyBottom); // clear the selection, if affected - if (fSelStart != fSelEnd) { + if (!fSelection.IsEmpty()) { // TODO: We're clearing the selection more often than necessary -- // to avoid that, we'd also need to track the x coordinates of the // dirty range. - int32 selectionBottom = fSelEnd.x > 0 ? fSelEnd.y : fSelEnd.y - 1; - if (fSelStart.y <= info.dirtyBottom + int32 selectionBottom = fSelection.End().x > 0 + ? fSelection.End().y : fSelection.End().y - 1; + if (fSelection.Start().y <= info.dirtyBottom && info.dirtyTop <= selectionBottom) { _Deselect(); } @@ -2247,6 +2292,17 @@ TermView::_SynchronizeWithTextBuffer(int32 visibleDirtyTop, } +void +TermView::_VisibleTextBufferChanged() +{ + if (!fVisibleTextBufferChanged) + return; + + fVisibleTextBufferChanged = false; + fActiveState->VisibleTextBufferChanged(); +} + + /*! Write strings to PTY device. If encoding system isn't UTF8, change encoding to UTF8 before writing PTY. */ @@ -2322,9 +2378,8 @@ TermView::MouseDown(BPoint where) BMessage* currentMessage = Window()->CurrentMessage(); int32 buttons = currentMessage->GetInt32("buttons", 0); - int32 modifiers = currentMessage->GetInt32("modifiers", 0); - fActiveState->MouseDown(where, buttons, modifiers); + fActiveState->MouseDown(where, buttons, fModifiers); fMouseButtons = buttons; fLastClickPoint = where; @@ -2334,7 +2389,7 @@ TermView::MouseDown(BPoint where) void TermView::MouseMoved(BPoint where, uint32 transit, const BMessage *message) { - fActiveState->MouseMoved(where, transit, message); + fActiveState->MouseMoved(where, transit, message, fModifiers); } @@ -2354,7 +2409,7 @@ void TermView::_Select(TermPos start, TermPos end, bool inclusive, bool setInitialSelection) { - BAutolock _(fTextBuffer); + TextBufferSyncLocker _(this); _SynchronizeWithTextBuffer(0, -1); @@ -2396,18 +2451,17 @@ TermView::_Select(TermPos start, TermPos end, bool inclusive, end.x = fColumns; } - if (fSelStart != fSelEnd) - _InvalidateTextRange(fSelStart, fSelEnd); + if (!fSelection.IsEmpty()) + _InvalidateTextRange(fSelection.Start(), fSelection.End()); - fSelStart = start; - fSelEnd = end; + fSelection.SetRange(start, end); if (setInitialSelection) { - fInitialSelectionStart = fSelStart; - fInitialSelectionEnd = fSelEnd; + fInitialSelectionStart = fSelection.Start(); + fInitialSelectionEnd = fSelection.End(); } - _InvalidateTextRange(fSelStart, fSelEnd); + _InvalidateTextRange(fSelection.Start(), fSelection.End()); } @@ -2419,8 +2473,8 @@ TermView::_ExtendSelection(TermPos pos, bool inclusive, if (!useInitialSelection && !_HasSelection()) return; - TermPos start = fSelStart; - TermPos end = fSelEnd; + TermPos start = fSelection.Start(); + TermPos end = fSelection.End(); if (useInitialSelection) { start = fInitialSelectionStart; @@ -2446,22 +2500,17 @@ void TermView::_Deselect() { //debug_printf("TermView::_Deselect(): has selection: %d\n", _HasSelection()); - if (!_HasSelection()) - return; - - _InvalidateTextRange(fSelStart, fSelEnd); - - fSelStart.SetTo(0, 0); - fSelEnd.SetTo(0, 0); - fInitialSelectionStart.SetTo(0, 0); - fInitialSelectionEnd.SetTo(0, 0); + if (_ClearHighlight(&fSelection)) { + fInitialSelectionStart.SetTo(0, 0); + fInitialSelectionEnd.SetTo(0, 0); + } } bool TermView::_HasSelection() const { - return fSelStart != fSelEnd; + return !fSelection.IsEmpty(); } @@ -2476,11 +2525,15 @@ TermView::_SelectWord(BPoint where, bool extend, bool useInitialSelection) return; if (extend) { - if (start < (useInitialSelection ? fInitialSelectionStart : fSelStart)) + if (start + < (useInitialSelection + ? fInitialSelectionStart : fSelection.Start())) { _ExtendSelection(start, false, useInitialSelection); - else if (end > (useInitialSelection ? fInitialSelectionEnd : fSelEnd)) + } else if (end + > (useInitialSelection + ? fInitialSelectionEnd : fSelection.End())) { _ExtendSelection(end, false, useInitialSelection); - else if (useInitialSelection) + } else if (useInitialSelection) _Select(start, end, false, false); } else _Select(start, end, false, !useInitialSelection); @@ -2494,47 +2547,101 @@ TermView::_SelectLine(BPoint where, bool extend, bool useInitialSelection) TermPos end = TermPos(0, start.y + 1); if (extend) { - if (start < (useInitialSelection ? fInitialSelectionStart : fSelStart)) + if (start + < (useInitialSelection + ? fInitialSelectionStart : fSelection.Start())) { _ExtendSelection(start, false, useInitialSelection); - else if (end > (useInitialSelection ? fInitialSelectionEnd : fSelEnd)) + } else if (end + > (useInitialSelection + ? fInitialSelectionEnd : fSelection.End())) { _ExtendSelection(end, false, useInitialSelection); - else if (useInitialSelection) + } else if (useInitialSelection) _Select(start, end, false, false); } else _Select(start, end, false, !useInitialSelection); } -bool -TermView::_CheckSelectedRegion(const TermPos &pos) const +void +TermView::_AddHighlight(Highlight* highlight) { - return pos >= fSelStart && pos < fSelEnd; + fHighlights.AddItem(highlight); + + if (!highlight->IsEmpty()) + _InvalidateTextRange(highlight->Start(), highlight->End()); +} + + +void +TermView::_RemoveHighlight(Highlight* highlight) +{ + fHighlights.RemoveItem(highlight); + + if (!highlight->IsEmpty()) + _InvalidateTextRange(highlight->Start(), highlight->End()); } bool -TermView::_CheckSelectedRegion(int32 row, int32 firstColumn, +TermView::_ClearHighlight(Highlight* highlight) +{ + if (highlight->IsEmpty()) + return false; + + _InvalidateTextRange(highlight->Start(), highlight->End()); + + highlight->SetRange(TermPos(0, 0), TermPos(0, 0)); + return true; +} + + +TermView::Highlight* +TermView::_CheckHighlightRegion(const TermPos &pos) const +{ + for (int32 i = 0; Highlight* highlight = fHighlights.ItemAt(i); i++) { + if (highlight->RangeContains(pos)) + return highlight; + } + + return NULL; +} + + +TermView::Highlight* +TermView::_CheckHighlightRegion(int32 row, int32 firstColumn, int32& lastColumn) const { - if (fSelStart == fSelEnd) - return false; + Highlight* nextHighlight = NULL; - if (row == fSelStart.y && firstColumn < fSelStart.x - && lastColumn >= fSelStart.x) { - // region starts before the selection, but intersects with it - lastColumn = fSelStart.x - 1; - return false; + for (int32 i = 0; Highlight* highlight = fHighlights.ItemAt(i); i++) { + if (highlight->IsEmpty()) + continue; + + if (row == highlight->Start().y && firstColumn < highlight->Start().x + && lastColumn >= highlight->Start().x) { + // region starts before the highlight, but intersects with it + if (nextHighlight == NULL + || highlight->Start().x < nextHighlight->Start().x) { + nextHighlight = highlight; + } + continue; + } + + if (row == highlight->End().y && firstColumn < highlight->End().x + && lastColumn >= highlight->End().x) { + // region starts in the highlight, but exceeds the end + lastColumn = highlight->End().x - 1; + return highlight; + } + + TermPos pos(firstColumn, row); + if (highlight->RangeContains(pos)) + return highlight; } - if (row == fSelEnd.y && firstColumn < fSelEnd.x - && lastColumn >= fSelEnd.x) { - // region starts in the selection, but exceeds the end - lastColumn = fSelEnd.x - 1; - return true; - } - - TermPos pos(firstColumn, row); - return pos >= fSelStart && pos < fSelEnd; + if (nextHighlight != NULL) + lastColumn = nextHighlight->Start().x - 1; + return NULL; } @@ -2560,15 +2667,15 @@ bool TermView::Find(const BString &str, bool forwardSearch, bool matchCase, bool matchWord) { - BAutolock _(fTextBuffer); + TextBufferSyncLocker _(this); _SynchronizeWithTextBuffer(0, -1); TermPos start; if (_HasSelection()) { if (forwardSearch) - start = fSelEnd; + start = fSelection.End(); else - start = fSelStart; + start = fSelection.Start(); } else { // search from the very beginning/end if (forwardSearch) @@ -2584,7 +2691,7 @@ TermView::Find(const BString &str, bool forwardSearch, bool matchCase, } _Select(matchStart, matchEnd, false, true); - _ScrollToRange(fSelStart, fSelEnd); + _ScrollToRange(fSelection.Start(), fSelection.End()); return true; } @@ -2596,7 +2703,7 @@ TermView::GetSelection(BString &str) { str.SetTo(""); BAutolock _(fTextBuffer); - fTextBuffer->GetStringFromRegion(str, fSelStart, fSelEnd); + fTextBuffer->GetStringFromRegion(str, fSelection.Start(), fSelection.End()); } @@ -2619,17 +2726,18 @@ TermView::InitiateDrag() BAutolock _(fTextBuffer); BString copyStr(""); - fTextBuffer->GetStringFromRegion(copyStr, fSelStart, fSelEnd); + fTextBuffer->GetStringFromRegion(copyStr, fSelection.Start(), + fSelection.End()); BMessage message(B_MIME_DATA); message.AddData("text/plain", B_MIME_TYPE, copyStr.String(), copyStr.Length()); - BPoint start = _ConvertFromTerminal(fSelStart); - BPoint end = _ConvertFromTerminal(fSelEnd); + BPoint start = _ConvertFromTerminal(fSelection.Start()); + BPoint end = _ConvertFromTerminal(fSelection.End()); BRect rect; - if (fSelStart.y == fSelEnd.y) + if (fSelection.Start().y == fSelection.End().y) rect.Set(start.x, start.y, end.x + fFontWidth, end.y + fFontHeight); else rect.Set(0, start.y, fColumns * fFontWidth, end.y + fFontHeight); @@ -2911,6 +3019,9 @@ TermView::Listener::NextTermView(TermView* view) } +// #pragma mark - + + #ifdef USE_DEBUG_SNAPSHOTS void diff --git a/src/apps/terminal/TermView.h b/src/apps/terminal/TermView.h index 429097db82..5fc9c34482 100644 --- a/src/apps/terminal/TermView.h +++ b/src/apps/terminal/TermView.h @@ -16,10 +16,12 @@ #include #include +#include #include #include #include "TermPos.h" +#include "TermViewHighlight.h" class ActiveProcessInfo; @@ -30,6 +32,7 @@ class BScrollView; class BString; class BStringView; class BasicTerminalBuffer; +class DefaultCharClassifier; class InlineInput; class ResizeWindow; class ShellInfo; @@ -38,10 +41,14 @@ class TermBuffer; class TerminalBuffer; class Shell; -class TermView : public BView { + +class TermView : public BView, private TermViewHighlighter { public: class Listener; + typedef TermViewHighlighter Highlighter; + typedef TermViewHighlight Highlight; + public: TermView(BRect frame, const ShellParameters& shellParameters, @@ -141,17 +148,29 @@ protected: const char* property); private: - class CharClassifier; + class TextBufferSyncLocker; + friend class TextBufferSyncLocker; class State; class StandardBaseState; class DefaultState; class SelectState; + class HyperLinkState; + class HyperLinkMenuState; friend class State; friend class StandardBaseState; friend class DefaultState; friend class SelectState; + friend class HyperLinkState; + friend class HyperLinkMenuState; + + typedef BObjectList HighlightList; + +private: + // TermViewHighlighter + virtual rgb_color ForegroundColor(); + virtual rgb_color BackgroundColor(); private: // point and text offset conversion @@ -174,8 +193,9 @@ private: void _SwitchCursorBlinking(bool blinkingOn); void _DrawLinePart(int32 x1, int32 y1, uint32 attr, - char* buffer, int32 width, bool mouse, - bool cursor, BView* inView); + char* buffer, int32 width, + Highlight* highlight, bool cursor, + BView* inView); void _DrawCursor(); void _InvalidateTextRange(TermPos start, TermPos end); @@ -193,6 +213,7 @@ private: void _SynchronizeWithTextBuffer( int32 visibleDirtyTop, int32 visibleDirtyBottom); + void _VisibleTextBufferChanged(); void _WritePTY(const char* text, int32 numBytes); @@ -209,8 +230,12 @@ private: void _SelectLine(BPoint where, bool extend, bool useInitialSelection); - bool _CheckSelectedRegion(const TermPos& pos) const; - bool _CheckSelectedRegion(int32 row, + void _AddHighlight(Highlight* highlight); + void _RemoveHighlight(Highlight* highlight); + bool _ClearHighlight(Highlight* highlight); + + Highlight* _CheckHighlightRegion(const TermPos& pos) const; + Highlight* _CheckHighlightRegion(int32 row, int32 firstColumn, int32& lastColumn) const; void _UpdateSIGWINCH(); @@ -237,7 +262,7 @@ private: BMessageRunner* fAutoScrollRunner; BMessageRunner* fResizeRunner; BStringView* fResizeView; - CharClassifier* fCharClassifier; + DefaultCharClassifier* fCharClassifier; // Font and Width BFont fHalfFont; @@ -272,6 +297,7 @@ private: // Object pointer. TerminalBuffer* fTextBuffer; BasicTerminalBuffer* fVisibleTextBuffer; + bool fVisibleTextBufferChanged; BScrollBar* fScrollBar; InlineInput* fInline; @@ -297,14 +323,16 @@ private: bool fConsiderClockedSync; // selection - TermPos fSelStart; - TermPos fSelEnd; + Highlight fSelection; TermPos fInitialSelectionStart; TermPos fInitialSelectionEnd; BPoint fLastClickPoint; + HighlightList fHighlights; + // mouse int32 fMouseButtons; + int32 fModifiers; TermPos fPrevPos; bool fReportX10MouseEvent; bool fReportNormalMouseEvent; @@ -315,6 +343,8 @@ private: // states DefaultState* fDefaultState; SelectState* fSelectState; + HyperLinkState* fHyperLinkState; + HyperLinkMenuState* fHyperLinkMenuState; State* fActiveState; }; diff --git a/src/apps/terminal/TermViewHighlight.cpp b/src/apps/terminal/TermViewHighlight.cpp new file mode 100644 index 0000000000..b14f3c9e6f --- /dev/null +++ b/src/apps/terminal/TermViewHighlight.cpp @@ -0,0 +1,19 @@ +/* + * Copyright 2013, Ingo Weinhold, ingo_weinhold@gmx.de. + * Distributed under the terms of the MIT License. + */ + + +#include "TermViewHighlight.h" + + +TermViewHighlighter::~TermViewHighlighter() +{ +} + + +uint32 +TermViewHighlighter::AdjustTextAttributes(uint32 attributes) +{ + return attributes; +} diff --git a/src/apps/terminal/TermViewHighlight.h b/src/apps/terminal/TermViewHighlight.h new file mode 100644 index 0000000000..1e9e5736bd --- /dev/null +++ b/src/apps/terminal/TermViewHighlight.h @@ -0,0 +1,83 @@ +/* + * Copyright 2013, Ingo Weinhold, ingo_weinhold@gmx.de. + * Distributed under the terms of the MIT License. + */ +#ifndef TERMVIEW_HIGHLIGHT_H +#define TERMVIEW_HIGHLIGHT_H + + +#include + +#include "TermPos.h" + + +class TermViewHighlighter { +public: + virtual ~TermViewHighlighter(); + + virtual rgb_color ForegroundColor() = 0; + virtual rgb_color BackgroundColor() = 0; + virtual uint32 AdjustTextAttributes(uint32 attributes); +}; + + +class TermViewHighlight { +public: + TermViewHighlight() + : + fHighlighter(NULL), + fStart(-1, -1), + fEnd(-1, -1) + { + } + + TermViewHighlighter* Highlighter() const + { + return fHighlighter; + } + + void SetHighlighter(TermViewHighlighter* highligher) + { + fHighlighter = highligher; + } + + const TermPos& Start() const + { + return fStart; + } + + const TermPos& End() const + { + return fEnd; + } + + bool IsEmpty() const + { + return fStart == fEnd; + } + + bool RangeContains(const TermPos& pos) const + { + return pos >= fStart && pos < fEnd; + } + + void SetRange(const TermPos& start, const TermPos& end) + { + fStart = start; + fEnd = end; + } + + void ScrollRange(int32 byLines) + { + fStart.y -= byLines; + fEnd.y -= byLines; + } + +private: + TermViewHighlighter* fHighlighter; + TermPos fStart; + TermPos fEnd; +}; + + +#endif // TERMVIEW_HIGHLIGHT_H diff --git a/src/apps/terminal/TermViewStates.cpp b/src/apps/terminal/TermViewStates.cpp index e74194dc18..8a85173bc5 100644 --- a/src/apps/terminal/TermViewStates.cpp +++ b/src/apps/terminal/TermViewStates.cpp @@ -16,17 +16,30 @@ #include "TermViewStates.h" +#include +#include + +#include +#include +#include +#include #include +#include #include #include #include #include "Shell.h" #include "TermConst.h" +#include "TerminalBuffer.h" #include "VTkeymap.h" #include "VTKeyTbl.h" +#undef B_TRANSLATION_CONTEXT +#define B_TRANSLATION_CONTEXT "Terminal TermView" + + // selection granularity enum { SELECT_CHARS, @@ -36,6 +49,13 @@ enum { static const uint32 kAutoScroll = 'AScr'; +static const uint32 kMessageOpenLink = 'OLnk'; +static const uint32 kMessageCopyLink = 'CLnk'; +static const uint32 kMessageMenuClosed = 'MClo'; + + +static const char* const kKnownURLProtocols = "http:https:ftp:mailto"; + // #pragma mark - State @@ -71,6 +91,12 @@ TermView::State::MessageReceived(BMessage* message) } +void +TermView::State::ModifiersChanged(int32 oldModifiers, int32 modifiers) +{ +} + + void TermView::State::KeyDown(const char* bytes, int32 numBytes) { @@ -85,7 +111,7 @@ TermView::State::MouseDown(BPoint where, int32 buttons, int32 modifiers) void TermView::State::MouseMoved(BPoint where, uint32 transit, - const BMessage* message) + const BMessage* message, int32 modifiers) { } @@ -96,6 +122,18 @@ TermView::State::MouseUp(BPoint where, int32 buttons) } +void +TermView::State::WindowActivated(bool active) +{ +} + + +void +TermView::State::VisibleTextBufferChanged() +{ +} + + // #pragma mark - StandardBaseState @@ -107,25 +145,22 @@ TermView::StandardBaseState::StandardBaseState(TermView* view) bool -TermView::StandardBaseState::_StandardMouseMoved(BPoint where) +TermView::StandardBaseState::_StandardMouseMoved(BPoint where, int32 modifiers) { if (!fView->fReportAnyMouseEvent && !fView->fReportButtonMouseEvent) return false; - int32 modifier; - fView->Window()->CurrentMessage()->FindInt32("modifiers", &modifier); - TermPos clickPos = fView->_ConvertToTerminal(where); if (fView->fReportButtonMouseEvent) { if (fView->fPrevPos.x != clickPos.x || fView->fPrevPos.y != clickPos.y) { - fView->_SendMouseEvent(fView->fMouseButtons, modifier, + fView->_SendMouseEvent(fView->fMouseButtons, modifiers, clickPos.x, clickPos.y, true); } fView->fPrevPos = clickPos; } else { - fView->_SendMouseEvent(fView->fMouseButtons, modifier, clickPos.x, + fView->_SendMouseEvent(fView->fMouseButtons, modifiers, clickPos.x, clickPos.y, true); } @@ -143,6 +178,13 @@ TermView::DefaultState::DefaultState(TermView* view) } +void +TermView::DefaultState::ModifiersChanged(int32 oldModifiers, int32 modifiers) +{ + _CheckEnterHyperLinkState(modifiers); +} + + void TermView::DefaultState::KeyDown(const char* bytes, int32 numBytes) { @@ -341,9 +383,32 @@ TermView::DefaultState::MouseDown(BPoint where, int32 buttons, int32 modifiers) void TermView::DefaultState::MouseMoved(BPoint where, uint32 transit, - const BMessage* message) + const BMessage* message, int32 modifiers) { - _StandardMouseMoved(where); + if (_CheckEnterHyperLinkState(modifiers)) + return; + + _StandardMouseMoved(where, modifiers); +} + + +void +TermView::DefaultState::WindowActivated(bool active) +{ + if (active) + _CheckEnterHyperLinkState(fView->fModifiers); +} + + +bool +TermView::DefaultState::_CheckEnterHyperLinkState(int32 modifiers) +{ + if ((modifiers & B_COMMAND_KEY) != 0 && fView->Window()->IsActive()) { + fView->_NextState(fView->fHyperLinkState); + return true; + } + + return false; } @@ -368,7 +433,7 @@ TermView::SelectState::Prepare(BPoint where, int32 modifiers) if (fView->_HasSelection()) { TermPos inPos = fView->_ConvertToTerminal(where); - if (fView->_CheckSelectedRegion(inPos)) { + if (fView->fSelection.RangeContains(inPos)) { if (modifiers & B_CONTROL_KEY) { BPoint p; uint32 bt; @@ -448,9 +513,9 @@ TermView::SelectState::MessageReceived(BMessage* message) void TermView::SelectState::MouseMoved(BPoint where, uint32 transit, - const BMessage* message) + const BMessage* message, int32 modifiers) { - if (_StandardMouseMoved(where)) + if (_StandardMouseMoved(where, modifiers)) return; if (fCheckMouseTracking) { @@ -561,3 +626,376 @@ TermView::SelectState::_AutoScrollUpdate() } } } + + +// #pragma mark - HyperLinkState + + +TermView::HyperLinkState::HyperLinkState(TermView* view) + : + State(view), + fURLCharClassifier(kURLAdditionalWordCharacters), + fPathComponentCharClassifier( + BString(kDefaultAdditionalWordCharacters).RemoveFirst("/")), + fHighlight(), + fHighlightActive(false) +{ + fHighlight.SetHighlighter(this); +} + + +void +TermView::HyperLinkState::Entered() +{ + _UpdateHighlight(); +} + + +void +TermView::HyperLinkState::Exited() +{ + _DeactivateHighlight(); +} + + +void +TermView::HyperLinkState::ModifiersChanged(int32 oldModifiers, int32 modifiers) +{ + if ((modifiers & B_COMMAND_KEY) == 0) + fView->_NextState(fView->fDefaultState); + else + _UpdateHighlight(); +} + + +void +TermView::HyperLinkState::MouseDown(BPoint where, int32 buttons, + int32 modifiers) +{ + TermPos start; + TermPos end; + HyperLink link; + bool pathPrefixOnly = (modifiers & B_SHIFT_KEY) != 0; + if (!_GetHyperLinkAt(where, pathPrefixOnly, link, start, end)) + return; + + if ((buttons & B_PRIMARY_MOUSE_BUTTON) != 0) { + link.Open(); + } else if ((buttons & B_SECONDARY_MOUSE_BUTTON) != 0) { + fView->fHyperLinkMenuState->Prepare(where, link); + fView->_NextState(fView->fHyperLinkMenuState); + } +} + + +void +TermView::HyperLinkState::MouseMoved(BPoint where, uint32 transit, + const BMessage* message, int32 modifiers) +{ + _UpdateHighlight(where, modifiers); +} + + +void +TermView::HyperLinkState::WindowActivated(bool active) +{ + if (!active) + fView->_NextState(fView->fDefaultState); +} + + +void +TermView::HyperLinkState::VisibleTextBufferChanged() +{ + _UpdateHighlight(); +} + + +rgb_color +TermView::HyperLinkState::ForegroundColor() +{ + return make_color(0, 0, 255); +} + + +rgb_color +TermView::HyperLinkState::BackgroundColor() +{ + return fView->fTextBackColor; +} + + +uint32 +TermView::HyperLinkState::AdjustTextAttributes(uint32 attributes) +{ + return attributes | UNDERLINE; +} + + +bool +TermView::HyperLinkState::_GetHyperLinkAt(BPoint where, bool pathPrefixOnly, + HyperLink& _link, TermPos& _start, TermPos& _end) +{ + TerminalBuffer* textBuffer = fView->fTextBuffer; + BAutolock textBufferLocker(textBuffer); + + TermPos pos = fView->_ConvertToTerminal(where); + + // try to get a URL first + BString text; + if (!textBuffer->FindWord(pos, &fURLCharClassifier, false, _start, _end)) + return false; + + text.Truncate(0); + textBuffer->GetStringFromRegion(text, _start, _end); + text.Trim(); + + // We're only happy, if it has a protocol part which we know. + int32 colonIndex = text.FindFirst(':'); + if (colonIndex >= 0) { + BString protocol(text, colonIndex); + if (strstr(kKnownURLProtocols, protocol) != NULL) { + _link = HyperLink(text, HyperLink::TYPE_URL); + return true; + } + } + + // no obvious URL -- try file name + if (!textBuffer->FindWord(pos, fView->fCharClassifier, false, _start, _end)) + return false; + + // In path-prefix-only mode we determine the end position anew by omitting + // the '/' in the allowed word chars. + if (pathPrefixOnly) { + TermPos componentStart; + TermPos componentEnd; + if (textBuffer->FindWord(pos, &fPathComponentCharClassifier, false, + componentStart, componentEnd)) { + _end = componentEnd; + } else { + // That means pos points to a '/'. We simply use the previous + // position. + _end = pos; + if (_start == _end) { + // Well, must be just "/". Advance to the next position. + if (!textBuffer->NextLinePos(_end, false)) + return false; + } + } + } + + text.Truncate(0); + textBuffer->GetStringFromRegion(text, _start, _end); + text.Trim(); + if (text.IsEmpty()) + return false; + + // check, whether the file exists + struct stat st; + if (lstat(text, &st) == 0) { + _link = HyperLink(text, HyperLink::TYPE_PATH); + return true; + } + + // As such this isn't an existing path. Try a few common alternative cases: + // * ":" + // * ":" + // * "::" + // * "::" + // * ":::" + + if (text.Length() <= 1) + return false; + + if (text[text.Length() - 1] == ':') { + text.Truncate(text.Length() - 1); + if (!textBuffer->PreviousLinePos(_end)) + return false; + + if (lstat(text, &st) == 0) { + _link = HyperLink(text, HyperLink::TYPE_PATH); + return true; + } + } + + BString path = text; + + for (int32 i = 0; i < 2; i++) { + int32 colonIndex = path.FindLast(':'); + if (colonIndex <= 0 || colonIndex == path.Length() - 1) + return false; + + char* numberEnd; + strtol(path.String() + colonIndex + 1, &numberEnd, 0); + if (*numberEnd != '\0') + return false; + + path.Truncate(colonIndex); + if (lstat(path, &st) == 0) { + _link = HyperLink(text, + i == 0 + ? HyperLink::TYPE_PATH_WITH_LINE + : HyperLink::TYPE_PATH_WITH_LINE_AND_COLUMN, + path); + return true; + } + } + + return false; +} + + +void +TermView::HyperLinkState::_UpdateHighlight() +{ + BPoint where; + uint32 buttons; + fView->GetMouse(&where, &buttons, false); + _UpdateHighlight(where, fView->fModifiers); +} + +void +TermView::HyperLinkState::_UpdateHighlight(BPoint where, int32 modifiers) +{ + TermPos start; + TermPos end; + HyperLink link; + bool pathPrefixOnly = (modifiers & B_SHIFT_KEY) != 0; + if (_GetHyperLinkAt(where, pathPrefixOnly, link, start, end)) + _ActivateHighlight(start, end); + else + _DeactivateHighlight(); +} + + +void +TermView::HyperLinkState::_ActivateHighlight(const TermPos& start, + const TermPos& end) +{ + if (fHighlightActive) { + if (fHighlight.Start() == start && fHighlight.End() == end) + return; + _DeactivateHighlight(); + } + + fHighlight.SetRange(start, end); + fView->_AddHighlight(&fHighlight); + BCursor cursor(B_CURSOR_ID_FOLLOW_LINK); + fView->SetViewCursor(&cursor); + fHighlightActive = true; +} + + +void +TermView::HyperLinkState::_DeactivateHighlight() +{ + if (fHighlightActive) { + fView->_RemoveHighlight(&fHighlight); + BCursor cursor(B_CURSOR_ID_SYSTEM_DEFAULT); + fView->SetViewCursor(&cursor); + fHighlightActive = false; + } +} + + +// #pragma mark - HyperLinkMenuState + + +class TermView::HyperLinkMenuState::PopUpMenu : public BPopUpMenu { +public: + PopUpMenu(const BMessenger& messageTarget) + : + BPopUpMenu("open hyperlink"), + fMessageTarget(messageTarget) + { + SetAsyncAutoDestruct(true); + } + + ~PopUpMenu() + { + fMessageTarget.SendMessage(kMessageMenuClosed); + } + +private: + BMessenger fMessageTarget; +}; + + +TermView::HyperLinkMenuState::HyperLinkMenuState(TermView* view) + : + State(view), + fLink() +{ +} + + +void +TermView::HyperLinkMenuState::Prepare(BPoint point, const HyperLink& link) +{ + fLink = link; + + // open context menu + PopUpMenu* menu = new PopUpMenu(fView); + BLayoutBuilder::Menu<> menuBuilder(menu); + switch (link.GetType()) { + case HyperLink::TYPE_URL: + menuBuilder + .AddItem(B_TRANSLATE("Open link"), kMessageOpenLink) + .AddItem(B_TRANSLATE("Copy link location"), kMessageCopyLink); + break; + + case HyperLink::TYPE_PATH: + case HyperLink::TYPE_PATH_WITH_LINE: + case HyperLink::TYPE_PATH_WITH_LINE_AND_COLUMN: + menuBuilder + .AddItem(B_TRANSLATE("Open path"), kMessageOpenLink) + .AddItem(B_TRANSLATE("Copy path"), kMessageCopyLink); + break; + } + menu->SetTargetForItems(fView); + menu->Go(fView->ConvertToScreen(point), true, true, true); +} + + +void +TermView::HyperLinkMenuState::Exited() +{ + fLink = HyperLink(); +} + + +bool +TermView::HyperLinkMenuState::MessageReceived(BMessage* message) +{ + switch (message->what) { + case kMessageOpenLink: + if (fLink.IsValid()) + fLink.Open(); + return true; + + case kMessageCopyLink: + if (fLink.IsValid()) { + if (!be_clipboard->Lock()) + return true; + + be_clipboard->Clear(); + + if (BMessage *data = be_clipboard->Data()) { + data->AddData("text/plain", B_MIME_TYPE, + fLink.Address().String(), + fLink.Address().Length()); + be_clipboard->Commit(); + } + + be_clipboard->Unlock(); + } + return true; + + case kMessageMenuClosed: + fView->_NextState(fView->fDefaultState); + return true; + } + + return false; +} + diff --git a/src/apps/terminal/TermViewStates.h b/src/apps/terminal/TermViewStates.h index 26ff862069..af8e2955a8 100644 --- a/src/apps/terminal/TermViewStates.h +++ b/src/apps/terminal/TermViewStates.h @@ -14,6 +14,8 @@ #define TERMVIEW_STATES_H +#include "HyperLink.h" +#include "TerminalCharClassifier.h" #include "TermView.h" @@ -28,14 +30,20 @@ public: virtual bool MessageReceived(BMessage* message); // returns true, if handled + virtual void ModifiersChanged(int32 oldModifiers, + int32 modifiers); virtual void KeyDown(const char* bytes, int32 numBytes); virtual void MouseDown(BPoint where, int32 buttons, int32 modifiers); virtual void MouseMoved(BPoint where, uint32 transit, - const BMessage* message); + const BMessage* message, int32 modifiers); virtual void MouseUp(BPoint where, int32 buttons); + virtual void WindowActivated(bool active); + + virtual void VisibleTextBufferChanged(); + protected: TermView* fView; }; @@ -46,7 +54,8 @@ public: StandardBaseState(TermView* view); protected: - bool _StandardMouseMoved(BPoint where); + bool _StandardMouseMoved(BPoint where, + int32 modifiers); }; @@ -54,12 +63,20 @@ class TermView::DefaultState : public TermView::StandardBaseState { public: DefaultState(TermView* view); + virtual void ModifiersChanged(int32 oldModifiers, + int32 modifiers); + virtual void KeyDown(const char* bytes, int32 numBytes); virtual void MouseDown(BPoint where, int32 buttons, int32 modifiers); virtual void MouseMoved(BPoint where, uint32 transit, - const BMessage* message); + const BMessage* message, int32 modifiers); + + virtual void WindowActivated(bool active); + +private: + bool _CheckEnterHyperLinkState(int32 modifiers); }; @@ -72,7 +89,7 @@ public: virtual bool MessageReceived(BMessage* message); virtual void MouseMoved(BPoint where, uint32 transit, - const BMessage* message); + const BMessage* message, int32 modifiers); virtual void MouseUp(BPoint where, int32 buttons); private: @@ -85,4 +102,67 @@ private: }; +class TermView::HyperLinkState : public TermView::State, + private TermViewHighlighter { +public: + HyperLinkState(TermView* view); + + virtual void Entered(); + virtual void Exited(); + + virtual void ModifiersChanged(int32 oldModifiers, + int32 modifiers); + + virtual void MouseDown(BPoint where, int32 buttons, + int32 modifiers); + virtual void MouseMoved(BPoint where, uint32 transit, + const BMessage* message, int32 modifiers); + + virtual void WindowActivated(bool active); + + virtual void VisibleTextBufferChanged(); + +private: + // TermViewHighlighter + virtual rgb_color ForegroundColor(); + virtual rgb_color BackgroundColor(); + virtual uint32 AdjustTextAttributes(uint32 attributes); + +private: + bool _GetHyperLinkAt(BPoint where, + bool pathPrefixOnly, HyperLink& _link, + TermPos& _start, TermPos& _end); + + void _UpdateHighlight(); + void _UpdateHighlight(BPoint where, int32 modifiers); + void _ActivateHighlight(const TermPos& start, + const TermPos& end); + void _DeactivateHighlight(); + +private: + DefaultCharClassifier fURLCharClassifier; + DefaultCharClassifier fPathComponentCharClassifier; + TermViewHighlight fHighlight; + bool fHighlightActive; +}; + + +class TermView::HyperLinkMenuState : public TermView::State { +public: + HyperLinkMenuState(TermView* view); + + void Prepare(BPoint point, const HyperLink& link); + + virtual void Exited(); + + virtual bool MessageReceived(BMessage* message); + +private: + class PopUpMenu; + +private: + HyperLink fLink; +}; + + #endif // TERMVIEW_STATES_H diff --git a/src/apps/terminal/TerminalCharClassifier.cpp b/src/apps/terminal/TerminalCharClassifier.cpp index acdd2f59fb..86a3c9c606 100644 --- a/src/apps/terminal/TerminalCharClassifier.cpp +++ b/src/apps/terminal/TerminalCharClassifier.cpp @@ -1,11 +1,52 @@ /* - * Copyright 2008, Ingo Weinhold, ingo_weinhold@gmx.de. + * Copyright 2008-2013, Ingo Weinhold, ingo_weinhold@gmx.de. * Distributed under the terms of the MIT License. */ #include "TerminalCharClassifier.h" +#include + +#include + + +// #pragma mark - TerminalCharClassifier + TerminalCharClassifier::~TerminalCharClassifier() { } + + +// #pragma mark - DefaultCharClassifier + + +DefaultCharClassifier::DefaultCharClassifier(const char* additionalWordChars) +{ + const char* p = additionalWordChars; + while (p != NULL && *p != '\0') { + int count = UTF8Char::ByteCount(*p); + if (count <= 0 || count > 4) + break; + fAdditionalWordChars.push_back(UTF8Char(p, count)); + p += count; + } +} + + +int +DefaultCharClassifier::Classify(const UTF8Char& character) +{ + if (character.IsSpace()) + return CHAR_TYPE_SPACE; + + if (character.IsAlNum()) + return CHAR_TYPE_WORD_CHAR; + + if (std::find(fAdditionalWordChars.begin(), fAdditionalWordChars.end(), + character) != fAdditionalWordChars.end()) { + return CHAR_TYPE_WORD_CHAR; + } + + return CHAR_TYPE_WORD_DELIMITER; +} diff --git a/src/apps/terminal/TerminalCharClassifier.h b/src/apps/terminal/TerminalCharClassifier.h index 236f31c3aa..de0d36411f 100644 --- a/src/apps/terminal/TerminalCharClassifier.h +++ b/src/apps/terminal/TerminalCharClassifier.h @@ -1,11 +1,16 @@ /* - * Copyright 2008, Ingo Weinhold, ingo_weinhold@gmx.de. + * Copyright 2008-2013, Ingo Weinhold, ingo_weinhold@gmx.de. * Distributed under the terms of the MIT License. */ #ifndef TERMINAL_CHAR_CLASSIFIER_H #define TERMINAL_CHAR_CLASSIFIER_H +#include + +#include "UTF8Char.h" + + enum { CHAR_TYPE_SPACE, CHAR_TYPE_WORD_CHAR, @@ -23,4 +28,16 @@ public: }; +class DefaultCharClassifier: public TerminalCharClassifier { +public: + DefaultCharClassifier( + const char* additionalWordChars); + + virtual int Classify(const UTF8Char& character); + +private: + std::vector fAdditionalWordChars; +}; + + #endif // TERMINAL_CHAR_CLASSIFIER_H