UrlTextView.cpp 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. /*
  2. * Copyright 2021, Jaidyn Levesque <jadedctrl@teknik.io>
  3. * All rights reserved. Distributed under the terms of the MIT license.
  4. */
  5. #include "UrlTextView.h"
  6. #include <ctype.h>
  7. #include <Cursor.h>
  8. #include <Locale.h>
  9. #include <MenuItem.h>
  10. #include <PopUpMenu.h>
  11. #include <TextView.h>
  12. #include <Window.h>
  13. const uint32 kSearchDdg = 'RVse';
  14. const uint32 kSearchDict = 'RVdc';
  15. UrlTextView::UrlTextView(const char* name)
  16. :
  17. UrlTextView(name, NULL, NULL, B_WILL_DRAW)
  18. {
  19. }
  20. UrlTextView::UrlTextView(const char* name, const BFont* initialFont,
  21. const rgb_color* initialColor, uint32 flags)
  22. :
  23. BTextView(name, initialFont, initialColor, flags),
  24. fUrlCursor(new BCursor(B_CURSOR_ID_FOLLOW_LINK)),
  25. fMouseDown(false),
  26. fSelecting(false)
  27. {
  28. MakeEditable(false);
  29. SetStylable(true);
  30. AdoptSystemColors();
  31. SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
  32. BFont font;
  33. if (initialFont != NULL)
  34. font = BFont(initialFont);
  35. rgb_color color = ui_color(B_PANEL_TEXT_COLOR);
  36. if (initialColor != NULL)
  37. color = *initialColor;
  38. text_run normalRun = { 0, font, color };
  39. fNormalRun = { 1, {normalRun} };
  40. BFont urlFont;
  41. urlFont.SetFace(B_REGULAR_FACE | B_UNDERSCORE_FACE);
  42. text_run urlRun = { 0, urlFont, ui_color(B_LINK_TEXT_COLOR) };
  43. fUrlRun = { 1, {urlRun} };
  44. }
  45. void
  46. UrlTextView::MessageReceived(BMessage* msg)
  47. {
  48. switch (msg->what)
  49. {
  50. case kSearchDdg:
  51. case kSearchDict:
  52. {
  53. int32 start = 0;
  54. int32 end = 0;
  55. GetSelection(&start, &end);
  56. if (start == end)
  57. break;
  58. char* buffer = new char[end - start];
  59. GetText(start, end - start, buffer);
  60. // Build query
  61. BString query;
  62. if (msg->what == kSearchDict)
  63. query = "https://%lang%.wiktionary.org/w/index.php?search=%q%";
  64. else
  65. query = "https://duckduckgo.com/?q=%q%";
  66. BLanguage lang;
  67. if (BLocale().GetLanguage(&lang) == B_OK)
  68. query.ReplaceAll("%lang%", lang.Code());
  69. else
  70. query.ReplaceAll("%lang", "eo");
  71. query.ReplaceAll("%q%", BUrl::UrlEncode(BString(buffer)));
  72. // Send query
  73. BUrl url(query.String());
  74. if (url.IsValid())
  75. url.OpenWithPreferredApplication(true);
  76. break;
  77. }
  78. default:
  79. BTextView::MessageReceived(msg);
  80. }
  81. }
  82. void
  83. UrlTextView::MouseDown(BPoint where)
  84. {
  85. uint32 buttons = 0;
  86. Window()->CurrentMessage()->FindInt32("buttons", (int32*)&buttons);
  87. if (buttons & B_SECONDARY_MOUSE_BUTTON)
  88. _RightClickPopUp(where)->Go(ConvertToScreen(where), true, false);
  89. else
  90. BTextView::MouseDown(where);
  91. if ((buttons & B_PRIMARY_MOUSE_BUTTON) && OverUrl(where)) {
  92. fMouseDown = true;
  93. BUrl url = UrlAt(where);
  94. if (url.IsValid() == true) {
  95. fLastClicked = url;
  96. }
  97. }
  98. }
  99. void
  100. UrlTextView::MouseUp(BPoint where)
  101. {
  102. BTextView::MouseUp(where);
  103. if (fMouseDown && fSelecting == false && fLastClicked.IsValid() == true) {
  104. fLastClicked.OpenWithPreferredApplication(true);
  105. fLastClicked = BUrl();
  106. }
  107. fMouseDown = false;
  108. fSelecting = false;
  109. }
  110. void
  111. UrlTextView::MouseMoved(BPoint where, uint32 code, const BMessage* drag)
  112. {
  113. if (fSelecting == true)
  114. return;
  115. if (code == B_INSIDE_VIEW)
  116. if (OverUrl(where) == true)
  117. SetViewCursor(fUrlCursor);
  118. else
  119. SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
  120. }
  121. void
  122. UrlTextView::Select(int32 startOffset, int32 endOffset)
  123. {
  124. BTextView::Select(startOffset, endOffset);
  125. if (startOffset < endOffset) {
  126. fSelecting = true;
  127. }
  128. }
  129. void
  130. UrlTextView::Insert(const char* text, const text_run_array* runs)
  131. {
  132. BString buf(text);
  133. int32 specStart = 0;
  134. int32 specEnd = 0;
  135. int32 lastEnd = 0;
  136. int32 length = buf.CountChars();
  137. if (runs == NULL)
  138. runs = &fNormalRun;
  139. while (_FindUrlString(buf, &specStart, &specEnd, lastEnd) == true) {
  140. if (lastEnd < specStart) {
  141. BString normie;
  142. buf.CopyCharsInto(normie, lastEnd, specStart - lastEnd);
  143. BTextView::Insert(TextLength(), normie.String(), normie.Length(),
  144. runs);
  145. }
  146. BString special;
  147. buf.CopyCharsInto(special, specStart, specEnd - specStart);
  148. BTextView::Insert(TextLength(), special.String(), special.Length(),
  149. &fUrlRun);
  150. lastEnd = specEnd;
  151. }
  152. if (lastEnd < length) {
  153. BString remaining;
  154. buf.CopyCharsInto(remaining, lastEnd, length - lastEnd);
  155. BTextView::Insert(TextLength(), remaining.String(), remaining.Length(),
  156. runs);
  157. }
  158. }
  159. void
  160. UrlTextView::SetText(const char* text, const text_run_array* runs)
  161. {
  162. BTextView::SetText("");
  163. Insert(text, runs);
  164. }
  165. BString
  166. UrlTextView::WordAt(BPoint point)
  167. {
  168. int32 start = 0;
  169. int32 end = 0;
  170. BString word;
  171. FindWordAround(OffsetAt(point), &start, &end, &word);
  172. return word;
  173. }
  174. void
  175. UrlTextView::FindWordAround(int32 offset, int32* start, int32* end, BString* _word)
  176. {
  177. int32 lineOffset = OffsetAt(LineAt(offset));
  178. const char* lineBuff = GetLine(LineAt(offset));
  179. BString line(lineBuff);
  180. delete lineBuff;
  181. int32 wordStart = line.FindLast(" ", offset - lineOffset) + 1;
  182. int32 wordEnd = line.FindFirst(" ", offset - lineOffset);
  183. if (wordStart == B_ERROR)
  184. wordStart = 0;
  185. if (wordEnd == B_ERROR)
  186. wordEnd = line.CountChars();
  187. *start = lineOffset + wordStart;
  188. *end = lineOffset + wordEnd;
  189. if (_word != NULL)
  190. line.CopyCharsInto(*_word, wordStart, wordEnd - wordStart);
  191. }
  192. const char*
  193. UrlTextView::GetLine(int32 line)
  194. {
  195. int32 length = 0;
  196. int32 startOffset = OffsetAt(line);
  197. int32 maxLength = TextLength() - startOffset;
  198. while (length < maxLength && ByteAt(startOffset + length) != '\n')
  199. length++;
  200. char* buffer = new char[length];
  201. GetText(startOffset, length, buffer);
  202. return buffer;
  203. }
  204. BUrl
  205. UrlTextView::UrlAt(BPoint where)
  206. {
  207. int32 lineNo = LineAt(where);
  208. BString urlStr, line = GetLine(lineNo);
  209. BUrl url;
  210. int32 clickedOffset = OffsetAt(where) - OffsetAt(lineNo);
  211. int32 offset = line.FindLast(" ", clickedOffset);
  212. if (offset == B_ERROR)
  213. offset = 0;
  214. int32 start;
  215. int32 end;
  216. if (_FindUrlString(line, &start, &end, offset) == true) {
  217. line.CopyCharsInto(urlStr, start, end - start);
  218. url.SetUrlString(urlStr);
  219. }
  220. return url;
  221. }
  222. bool
  223. UrlTextView::OverText(BPoint where)
  224. {
  225. int32 offset = OffsetAt(where);
  226. BPoint point = PointAt(offset);
  227. if (point.x + 10 < where.x)
  228. return false;
  229. return true;
  230. }
  231. bool
  232. UrlTextView::OverUrl(BPoint where)
  233. {
  234. if (OverText(where) == false)
  235. return false;
  236. int32 offset = OffsetAt(where);
  237. text_run_array* rArray = RunArray(offset, offset + 1);
  238. text_run run = rArray->runs[0];
  239. text_run urlRun = fUrlRun.runs[0];
  240. if (run.font.Face() == urlRun.font.Face()
  241. && (run.color == urlRun.color))
  242. return true;
  243. return false;
  244. }
  245. BPopUpMenu*
  246. UrlTextView::_RightClickPopUp(BPoint where)
  247. {
  248. BPopUpMenu* menu = new BPopUpMenu("rightClickPopUp");
  249. BMenuItem* ddgSearch =
  250. new BMenuItem("Search" B_UTF8_ELLIPSIS, new BMessage(kSearchDdg));
  251. BMenuItem* dictSearch =
  252. new BMenuItem("Dictionary" B_UTF8_ELLIPSIS, new BMessage(kSearchDict));
  253. BMenuItem* copy =
  254. new BMenuItem("Copy", new BMessage(B_COPY), 'C', B_COMMAND_KEY);
  255. BMenuItem* selectAll = new BMenuItem("Select all",
  256. new BMessage(B_SELECT_ALL), 'A', B_COMMAND_KEY);
  257. // Try and ensure we have something selected
  258. int32 start = 0;
  259. int32 end = 0;
  260. GetSelection(&start, &end);
  261. if (start == end && OverText(where) == true) {
  262. start = 0;
  263. end = 0;
  264. FindWordAround(OffsetAt(where), &start, &end);
  265. Select(start, end);
  266. }
  267. copy->SetEnabled(start < end);
  268. dictSearch->SetEnabled(start < end);
  269. ddgSearch->SetEnabled(start < end);
  270. menu->AddItem(ddgSearch);
  271. menu->AddItem(dictSearch);
  272. menu->AddSeparatorItem();
  273. menu->AddItem(copy);
  274. menu->AddItem(selectAll);
  275. menu->SetTargetForItems(this);
  276. return menu;
  277. }
  278. bool
  279. UrlTextView::_FindUrlString(BString text, int32* start, int32* end, int32 offset)
  280. {
  281. int32 urlOffset = text.FindFirst("://", offset);
  282. int32 urlStart = text.FindLast(" ", urlOffset) + 1;
  283. int32 urlEnd = text.FindFirst(" ", urlOffset);
  284. if (urlOffset == B_ERROR)
  285. return false;
  286. if (urlStart == B_ERROR) urlStart = 0;
  287. if (urlEnd == B_ERROR) urlEnd = text.CountChars();
  288. // Find first char of protocol
  289. for (int32 i = urlStart; i < urlOffset; i++)
  290. if (_IsValidUrlChar(text.ByteAt(i)) == true) {
  291. urlStart = i;
  292. break;
  293. }
  294. // Find last char of URL
  295. for (int32 i = urlOffset; i < urlEnd; i++)
  296. if (_IsValidUrlChar(text.ByteAt(i)) == false) {
  297. urlEnd = i;
  298. break;
  299. }
  300. *start = urlStart;
  301. *end = urlEnd;
  302. return true;
  303. }
  304. bool
  305. UrlTextView::_IsValidUrlChar(char c)
  306. {
  307. return (isalpha(c) || isdigit(c) || c == ':' || c == '%' || c == ':'
  308. || c == '/' || c == '?' || c == '#' || c == '[' || c == ']' || c == '@'
  309. || c == '!' || c == '$' || c == '&' || c == '(' || c == ')' || c == '*'
  310. || c == '*' || c == '+' || c == ',' || c == ';' || c == '=' || c == '-'
  311. || c == '.' || c == '_' || c == '~' || c == '\'');
  312. }