]> git.lizzy.rs Git - minetest.git/blob - src/gui/guiChatConsole.cpp
Make chat web links clickable (#11092)
[minetest.git] / src / gui / guiChatConsole.cpp
1 /*
2 Minetest
3 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include "IrrCompileConfig.h"
21 #include "guiChatConsole.h"
22 #include "chat.h"
23 #include "client/client.h"
24 #include "debug.h"
25 #include "gettime.h"
26 #include "client/keycode.h"
27 #include "settings.h"
28 #include "porting.h"
29 #include "client/tile.h"
30 #include "client/fontengine.h"
31 #include "log.h"
32 #include "gettext.h"
33 #include <string>
34
35 #if USE_FREETYPE
36         #include "irrlicht_changes/CGUITTFont.h"
37 #endif
38
39 inline u32 clamp_u8(s32 value)
40 {
41         return (u32) MYMIN(MYMAX(value, 0), 255);
42 }
43
44 inline bool isInCtrlKeys(const irr::EKEY_CODE& kc)
45 {
46         return kc == KEY_LCONTROL || kc == KEY_RCONTROL || kc == KEY_CONTROL;
47 }
48
49 GUIChatConsole::GUIChatConsole(
50                 gui::IGUIEnvironment* env,
51                 gui::IGUIElement* parent,
52                 s32 id,
53                 ChatBackend* backend,
54                 Client* client,
55                 IMenuManager* menumgr
56 ):
57         IGUIElement(gui::EGUIET_ELEMENT, env, parent, id,
58                         core::rect<s32>(0,0,100,100)),
59         m_chat_backend(backend),
60         m_client(client),
61         m_menumgr(menumgr),
62         m_animate_time_old(porting::getTimeMs())
63 {
64         // load background settings
65         s32 console_alpha = g_settings->getS32("console_alpha");
66         m_background_color.setAlpha(clamp_u8(console_alpha));
67
68         // load the background texture depending on settings
69         ITextureSource *tsrc = client->getTextureSource();
70         if (tsrc->isKnownSourceImage("background_chat.jpg")) {
71                 m_background = tsrc->getTexture("background_chat.jpg");
72                 m_background_color.setRed(255);
73                 m_background_color.setGreen(255);
74                 m_background_color.setBlue(255);
75         } else {
76                 v3f console_color = g_settings->getV3F("console_color");
77                 m_background_color.setRed(clamp_u8(myround(console_color.X)));
78                 m_background_color.setGreen(clamp_u8(myround(console_color.Y)));
79                 m_background_color.setBlue(clamp_u8(myround(console_color.Z)));
80         }
81
82         u16 chat_font_size = g_settings->getU16("chat_font_size");
83         m_font = g_fontengine->getFont(chat_font_size != 0 ?
84                 chat_font_size : FONT_SIZE_UNSPECIFIED, FM_Mono);
85
86         if (!m_font) {
87                 errorstream << "GUIChatConsole: Unable to load mono font" << std::endl;
88         } else {
89                 core::dimension2d<u32> dim = m_font->getDimension(L"M");
90                 m_fontsize = v2u32(dim.Width, dim.Height);
91                 m_font->grab();
92         }
93         m_fontsize.X = MYMAX(m_fontsize.X, 1);
94         m_fontsize.Y = MYMAX(m_fontsize.Y, 1);
95
96         // set default cursor options
97         setCursor(true, true, 2.0, 0.1);
98
99         // track ctrl keys for mouse event
100         m_is_ctrl_down = false;
101         m_cache_clickable_chat_weblinks = g_settings->getBool("clickable_chat_weblinks");
102 }
103
104 GUIChatConsole::~GUIChatConsole()
105 {
106         if (m_font)
107                 m_font->drop();
108 }
109
110 void GUIChatConsole::openConsole(f32 scale)
111 {
112         assert(scale > 0.0f && scale <= 1.0f);
113
114         m_open = true;
115         m_desired_height_fraction = scale;
116         m_desired_height = scale * m_screensize.Y;
117         reformatConsole();
118         m_animate_time_old = porting::getTimeMs();
119         IGUIElement::setVisible(true);
120         Environment->setFocus(this);
121         m_menumgr->createdMenu(this);
122 }
123
124 bool GUIChatConsole::isOpen() const
125 {
126         return m_open;
127 }
128
129 bool GUIChatConsole::isOpenInhibited() const
130 {
131         return m_open_inhibited > 0;
132 }
133
134 void GUIChatConsole::closeConsole()
135 {
136         m_open = false;
137         Environment->removeFocus(this);
138         m_menumgr->deletingMenu(this);
139 }
140
141 void GUIChatConsole::closeConsoleAtOnce()
142 {
143         closeConsole();
144         m_height = 0;
145         recalculateConsolePosition();
146 }
147
148 void GUIChatConsole::replaceAndAddToHistory(const std::wstring &line)
149 {
150         ChatPrompt& prompt = m_chat_backend->getPrompt();
151         prompt.addToHistory(prompt.getLine());
152         prompt.replace(line);
153 }
154
155
156 void GUIChatConsole::setCursor(
157         bool visible, bool blinking, f32 blink_speed, f32 relative_height)
158 {
159         if (visible)
160         {
161                 if (blinking)
162                 {
163                         // leave m_cursor_blink unchanged
164                         m_cursor_blink_speed = blink_speed;
165                 }
166                 else
167                 {
168                         m_cursor_blink = 0x8000;  // on
169                         m_cursor_blink_speed = 0.0;
170                 }
171         }
172         else
173         {
174                 m_cursor_blink = 0;  // off
175                 m_cursor_blink_speed = 0.0;
176         }
177         m_cursor_height = relative_height;
178 }
179
180 void GUIChatConsole::draw()
181 {
182         if(!IsVisible)
183                 return;
184
185         video::IVideoDriver* driver = Environment->getVideoDriver();
186
187         // Check screen size
188         v2u32 screensize = driver->getScreenSize();
189         if (screensize != m_screensize)
190         {
191                 // screen size has changed
192                 // scale current console height to new window size
193                 if (m_screensize.Y != 0)
194                         m_height = m_height * screensize.Y / m_screensize.Y;
195                 m_screensize = screensize;
196                 m_desired_height = m_desired_height_fraction * m_screensize.Y;
197                 reformatConsole();
198         }
199
200         // Animation
201         u64 now = porting::getTimeMs();
202         animate(now - m_animate_time_old);
203         m_animate_time_old = now;
204
205         // Draw console elements if visible
206         if (m_height > 0)
207         {
208                 drawBackground();
209                 drawText();
210                 drawPrompt();
211         }
212
213         gui::IGUIElement::draw();
214 }
215
216 void GUIChatConsole::reformatConsole()
217 {
218         s32 cols = m_screensize.X / m_fontsize.X - 2; // make room for a margin (looks better)
219         s32 rows = m_desired_height / m_fontsize.Y - 1; // make room for the input prompt
220         if (cols <= 0 || rows <= 0)
221                 cols = rows = 0;
222         recalculateConsolePosition();
223         m_chat_backend->reformat(cols, rows);
224 }
225
226 void GUIChatConsole::recalculateConsolePosition()
227 {
228         core::rect<s32> rect(0, 0, m_screensize.X, m_height);
229         DesiredRect = rect;
230         recalculateAbsolutePosition(false);
231 }
232
233 void GUIChatConsole::animate(u32 msec)
234 {
235         // animate the console height
236         s32 goal = m_open ? m_desired_height : 0;
237
238         // Set invisible if close animation finished (reset by openConsole)
239         // This function (animate()) is never called once its visibility becomes false so do not
240         //              actually set visible to false before the inhibited period is over
241         if (!m_open && m_height == 0 && m_open_inhibited == 0)
242                 IGUIElement::setVisible(false);
243
244         if (m_height != goal)
245         {
246                 s32 max_change = msec * m_screensize.Y * (m_height_speed / 1000.0);
247                 if (max_change == 0)
248                         max_change = 1;
249
250                 if (m_height < goal)
251                 {
252                         // increase height
253                         if (m_height + max_change < goal)
254                                 m_height += max_change;
255                         else
256                                 m_height = goal;
257                 }
258                 else
259                 {
260                         // decrease height
261                         if (m_height > goal + max_change)
262                                 m_height -= max_change;
263                         else
264                                 m_height = goal;
265                 }
266
267                 recalculateConsolePosition();
268         }
269
270         // blink the cursor
271         if (m_cursor_blink_speed != 0.0)
272         {
273                 u32 blink_increase = 0x10000 * msec * (m_cursor_blink_speed / 1000.0);
274                 if (blink_increase == 0)
275                         blink_increase = 1;
276                 m_cursor_blink = ((m_cursor_blink + blink_increase) & 0xffff);
277         }
278
279         // decrease open inhibit counter
280         if (m_open_inhibited > msec)
281                 m_open_inhibited -= msec;
282         else
283                 m_open_inhibited = 0;
284 }
285
286 void GUIChatConsole::drawBackground()
287 {
288         video::IVideoDriver* driver = Environment->getVideoDriver();
289         if (m_background != NULL)
290         {
291                 core::rect<s32> sourcerect(0, -m_height, m_screensize.X, 0);
292                 driver->draw2DImage(
293                         m_background,
294                         v2s32(0, 0),
295                         sourcerect,
296                         &AbsoluteClippingRect,
297                         m_background_color,
298                         false);
299         }
300         else
301         {
302                 driver->draw2DRectangle(
303                         m_background_color,
304                         core::rect<s32>(0, 0, m_screensize.X, m_height),
305                         &AbsoluteClippingRect);
306         }
307 }
308
309 void GUIChatConsole::drawText()
310 {
311         if (m_font == NULL)
312                 return;
313
314         ChatBuffer& buf = m_chat_backend->getConsoleBuffer();
315         for (u32 row = 0; row < buf.getRows(); ++row)
316         {
317                 const ChatFormattedLine& line = buf.getFormattedLine(row);
318                 if (line.fragments.empty())
319                         continue;
320
321                 s32 line_height = m_fontsize.Y;
322                 s32 y = row * line_height + m_height - m_desired_height;
323                 if (y + line_height < 0)
324                         continue;
325
326                 for (const ChatFormattedFragment &fragment : line.fragments) {
327                         s32 x = (fragment.column + 1) * m_fontsize.X;
328                         core::rect<s32> destrect(
329                                 x, y, x + m_fontsize.X * fragment.text.size(), y + m_fontsize.Y);
330
331 #if USE_FREETYPE
332                         if (m_font->getType() == irr::gui::EGFT_CUSTOM) {
333                                 // Draw colored text if FreeType is enabled
334                                 irr::gui::CGUITTFont *tmp = dynamic_cast<irr::gui::CGUITTFont *>(m_font);
335                                 tmp->draw(
336                                         fragment.text,
337                                         destrect,
338                                         false,
339                                         false,
340                                         &AbsoluteClippingRect);
341                         } else 
342 #endif
343                         {
344                                 // Otherwise use standard text
345                                 m_font->draw(
346                                         fragment.text.c_str(),
347                                         destrect,
348                                         video::SColor(255, 255, 255, 255),
349                                         false,
350                                         false,
351                                         &AbsoluteClippingRect);
352                         }
353                 }
354         }
355 }
356
357 void GUIChatConsole::drawPrompt()
358 {
359         if (!m_font)
360                 return;
361
362         u32 row = m_chat_backend->getConsoleBuffer().getRows();
363         s32 line_height = m_fontsize.Y;
364         s32 y = row * line_height + m_height - m_desired_height;
365
366         ChatPrompt& prompt = m_chat_backend->getPrompt();
367         std::wstring prompt_text = prompt.getVisiblePortion();
368
369         // FIXME Draw string at once, not character by character
370         // That will only work with the cursor once we have a monospace font
371         for (u32 i = 0; i < prompt_text.size(); ++i)
372         {
373                 wchar_t ws[2] = {prompt_text[i], 0};
374                 s32 x = (1 + i) * m_fontsize.X;
375                 core::rect<s32> destrect(
376                         x, y, x + m_fontsize.X, y + m_fontsize.Y);
377                 m_font->draw(
378                         ws,
379                         destrect,
380                         video::SColor(255, 255, 255, 255),
381                         false,
382                         false,
383                         &AbsoluteClippingRect);
384         }
385
386         // Draw the cursor during on periods
387         if ((m_cursor_blink & 0x8000) != 0)
388         {
389                 s32 cursor_pos = prompt.getVisibleCursorPosition();
390                 if (cursor_pos >= 0)
391                 {
392                         s32 cursor_len = prompt.getCursorLength();
393                         video::IVideoDriver* driver = Environment->getVideoDriver();
394                         s32 x = (1 + cursor_pos) * m_fontsize.X;
395                         core::rect<s32> destrect(
396                                 x,
397                                 y + m_fontsize.Y * (1.0 - m_cursor_height),
398                                 x + m_fontsize.X * MYMAX(cursor_len, 1),
399                                 y + m_fontsize.Y * (cursor_len ? m_cursor_height+1 : 1)
400                         );
401                         video::SColor cursor_color(255,255,255,255);
402                         driver->draw2DRectangle(
403                                 cursor_color,
404                                 destrect,
405                                 &AbsoluteClippingRect);
406                 }
407         }
408
409 }
410
411 bool GUIChatConsole::OnEvent(const SEvent& event)
412 {
413
414         ChatPrompt &prompt = m_chat_backend->getPrompt();
415
416         if (event.EventType == EET_KEY_INPUT_EVENT && !event.KeyInput.PressedDown)
417         {
418                 // CTRL up
419                 if (isInCtrlKeys(event.KeyInput.Key))
420                 {
421                         m_is_ctrl_down = false;
422                 }
423         }
424         else if(event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown)
425         {
426                 // CTRL down
427                 if (isInCtrlKeys(event.KeyInput.Key)) {
428                         m_is_ctrl_down = true;
429                 }
430
431                 // Key input
432                 if (KeyPress(event.KeyInput) == getKeySetting("keymap_console")) {
433                         closeConsole();
434
435                         // inhibit open so the_game doesn't reopen immediately
436                         m_open_inhibited = 50;
437                         m_close_on_enter = false;
438                         return true;
439                 }
440
441                 if (event.KeyInput.Key == KEY_ESCAPE) {
442                         closeConsoleAtOnce();
443                         m_close_on_enter = false;
444                         // inhibit open so the_game doesn't reopen immediately
445                         m_open_inhibited = 1; // so the ESCAPE button doesn't open the "pause menu"
446                         return true;
447                 }
448                 else if(event.KeyInput.Key == KEY_PRIOR)
449                 {
450                         m_chat_backend->scrollPageUp();
451                         return true;
452                 }
453                 else if(event.KeyInput.Key == KEY_NEXT)
454                 {
455                         m_chat_backend->scrollPageDown();
456                         return true;
457                 }
458                 else if(event.KeyInput.Key == KEY_RETURN)
459                 {
460                         prompt.addToHistory(prompt.getLine());
461                         std::wstring text = prompt.replace(L"");
462                         m_client->typeChatMessage(text);
463                         if (m_close_on_enter) {
464                                 closeConsoleAtOnce();
465                                 m_close_on_enter = false;
466                         }
467                         return true;
468                 }
469                 else if(event.KeyInput.Key == KEY_UP)
470                 {
471                         // Up pressed
472                         // Move back in history
473                         prompt.historyPrev();
474                         return true;
475                 }
476                 else if(event.KeyInput.Key == KEY_DOWN)
477                 {
478                         // Down pressed
479                         // Move forward in history
480                         prompt.historyNext();
481                         return true;
482                 }
483                 else if(event.KeyInput.Key == KEY_LEFT || event.KeyInput.Key == KEY_RIGHT)
484                 {
485                         // Left/right pressed
486                         // Move/select character/word to the left depending on control and shift keys
487                         ChatPrompt::CursorOp op = event.KeyInput.Shift ?
488                                 ChatPrompt::CURSOROP_SELECT :
489                                 ChatPrompt::CURSOROP_MOVE;
490                         ChatPrompt::CursorOpDir dir = event.KeyInput.Key == KEY_LEFT ?
491                                 ChatPrompt::CURSOROP_DIR_LEFT :
492                                 ChatPrompt::CURSOROP_DIR_RIGHT;
493                         ChatPrompt::CursorOpScope scope = event.KeyInput.Control ?
494                                 ChatPrompt::CURSOROP_SCOPE_WORD :
495                                 ChatPrompt::CURSOROP_SCOPE_CHARACTER;
496                         prompt.cursorOperation(op, dir, scope);
497                         return true;
498                 }
499                 else if(event.KeyInput.Key == KEY_HOME)
500                 {
501                         // Home pressed
502                         // move to beginning of line
503                         prompt.cursorOperation(
504                                 ChatPrompt::CURSOROP_MOVE,
505                                 ChatPrompt::CURSOROP_DIR_LEFT,
506                                 ChatPrompt::CURSOROP_SCOPE_LINE);
507                         return true;
508                 }
509                 else if(event.KeyInput.Key == KEY_END)
510                 {
511                         // End pressed
512                         // move to end of line
513                         prompt.cursorOperation(
514                                 ChatPrompt::CURSOROP_MOVE,
515                                 ChatPrompt::CURSOROP_DIR_RIGHT,
516                                 ChatPrompt::CURSOROP_SCOPE_LINE);
517                         return true;
518                 }
519                 else if(event.KeyInput.Key == KEY_BACK)
520                 {
521                         // Backspace or Ctrl-Backspace pressed
522                         // delete character / word to the left
523                         ChatPrompt::CursorOpScope scope =
524                                 event.KeyInput.Control ?
525                                 ChatPrompt::CURSOROP_SCOPE_WORD :
526                                 ChatPrompt::CURSOROP_SCOPE_CHARACTER;
527                         prompt.cursorOperation(
528                                 ChatPrompt::CURSOROP_DELETE,
529                                 ChatPrompt::CURSOROP_DIR_LEFT,
530                                 scope);
531                         return true;
532                 }
533                 else if(event.KeyInput.Key == KEY_DELETE)
534                 {
535                         // Delete or Ctrl-Delete pressed
536                         // delete character / word to the right
537                         ChatPrompt::CursorOpScope scope =
538                                 event.KeyInput.Control ?
539                                 ChatPrompt::CURSOROP_SCOPE_WORD :
540                                 ChatPrompt::CURSOROP_SCOPE_CHARACTER;
541                         prompt.cursorOperation(
542                                 ChatPrompt::CURSOROP_DELETE,
543                                 ChatPrompt::CURSOROP_DIR_RIGHT,
544                                 scope);
545                         return true;
546                 }
547                 else if(event.KeyInput.Key == KEY_KEY_A && event.KeyInput.Control)
548                 {
549                         // Ctrl-A pressed
550                         // Select all text
551                         prompt.cursorOperation(
552                                 ChatPrompt::CURSOROP_SELECT,
553                                 ChatPrompt::CURSOROP_DIR_LEFT, // Ignored
554                                 ChatPrompt::CURSOROP_SCOPE_LINE);
555                         return true;
556                 }
557                 else if(event.KeyInput.Key == KEY_KEY_C && event.KeyInput.Control)
558                 {
559                         // Ctrl-C pressed
560                         // Copy text to clipboard
561                         if (prompt.getCursorLength() <= 0)
562                                 return true;
563                         std::wstring wselected = prompt.getSelection();
564                         std::string selected = wide_to_utf8(wselected);
565                         Environment->getOSOperator()->copyToClipboard(selected.c_str());
566                         return true;
567                 }
568                 else if(event.KeyInput.Key == KEY_KEY_V && event.KeyInput.Control)
569                 {
570                         // Ctrl-V pressed
571                         // paste text from clipboard
572                         if (prompt.getCursorLength() > 0) {
573                                 // Delete selected section of text
574                                 prompt.cursorOperation(
575                                         ChatPrompt::CURSOROP_DELETE,
576                                         ChatPrompt::CURSOROP_DIR_LEFT, // Ignored
577                                         ChatPrompt::CURSOROP_SCOPE_SELECTION);
578                         }
579                         IOSOperator *os_operator = Environment->getOSOperator();
580                         const c8 *text = os_operator->getTextFromClipboard();
581                         if (!text)
582                                 return true;
583                         std::basic_string<unsigned char> str((const unsigned char*)text);
584                         prompt.input(std::wstring(str.begin(), str.end()));
585                         return true;
586                 }
587                 else if(event.KeyInput.Key == KEY_KEY_X && event.KeyInput.Control)
588                 {
589                         // Ctrl-X pressed
590                         // Cut text to clipboard
591                         if (prompt.getCursorLength() <= 0)
592                                 return true;
593                         std::wstring wselected = prompt.getSelection();
594                         std::string selected = wide_to_utf8(wselected);
595                         Environment->getOSOperator()->copyToClipboard(selected.c_str());
596                         prompt.cursorOperation(
597                                 ChatPrompt::CURSOROP_DELETE,
598                                 ChatPrompt::CURSOROP_DIR_LEFT, // Ignored
599                                 ChatPrompt::CURSOROP_SCOPE_SELECTION);
600                         return true;
601                 }
602                 else if(event.KeyInput.Key == KEY_KEY_U && event.KeyInput.Control)
603                 {
604                         // Ctrl-U pressed
605                         // kill line to left end
606                         prompt.cursorOperation(
607                                 ChatPrompt::CURSOROP_DELETE,
608                                 ChatPrompt::CURSOROP_DIR_LEFT,
609                                 ChatPrompt::CURSOROP_SCOPE_LINE);
610                         return true;
611                 }
612                 else if(event.KeyInput.Key == KEY_KEY_K && event.KeyInput.Control)
613                 {
614                         // Ctrl-K pressed
615                         // kill line to right end
616                         prompt.cursorOperation(
617                                 ChatPrompt::CURSOROP_DELETE,
618                                 ChatPrompt::CURSOROP_DIR_RIGHT,
619                                 ChatPrompt::CURSOROP_SCOPE_LINE);
620                         return true;
621                 }
622                 else if(event.KeyInput.Key == KEY_TAB)
623                 {
624                         // Tab or Shift-Tab pressed
625                         // Nick completion
626                         std::list<std::string> names = m_client->getConnectedPlayerNames();
627                         bool backwards = event.KeyInput.Shift;
628                         prompt.nickCompletion(names, backwards);
629                         return true;
630                 } else if (!iswcntrl(event.KeyInput.Char) && !event.KeyInput.Control) {
631                         prompt.input(event.KeyInput.Char);
632                         return true;
633                 }
634         }
635         else if(event.EventType == EET_MOUSE_INPUT_EVENT)
636         {
637                 if (event.MouseInput.Event == EMIE_MOUSE_WHEEL)
638                 {
639                         s32 rows = myround(-3.0 * event.MouseInput.Wheel);
640                         m_chat_backend->scroll(rows);
641                 }
642                 // Middle click or ctrl-click opens weblink, if enabled in config
643                 else if(m_cache_clickable_chat_weblinks && (
644                                 event.MouseInput.Event == EMIE_MMOUSE_PRESSED_DOWN ||
645                                 (event.MouseInput.Event == EMIE_LMOUSE_PRESSED_DOWN && m_is_ctrl_down)
646                                 ))
647                 {
648                         // If clicked within console output region
649                         if (event.MouseInput.Y / m_fontsize.Y < (m_height / m_fontsize.Y) - 1 )
650                         {
651                                 // Translate pixel position to font position
652                                 middleClick(event.MouseInput.X / m_fontsize.X, event.MouseInput.Y / m_fontsize.Y);
653                         }
654                 }
655         }
656 #if (IRRLICHT_VERSION_MT_REVISION >= 2)
657         else if(event.EventType == EET_STRING_INPUT_EVENT)
658         {
659                 prompt.input(std::wstring(event.StringInput.Str->c_str()));
660                 return true;
661         }
662 #endif
663
664         return Parent ? Parent->OnEvent(event) : false;
665 }
666
667 void GUIChatConsole::setVisible(bool visible)
668 {
669         m_open = visible;
670         IGUIElement::setVisible(visible);
671         if (!visible) {
672                 m_height = 0;
673                 recalculateConsolePosition();
674         }
675 }
676
677 void GUIChatConsole::middleClick(s32 col, s32 row)
678 {
679         // Prevent accidental rapid clicking
680         static u64 s_oldtime = 0;
681         u64 newtime = porting::getTimeMs();
682
683         // 0.6 seconds should suffice
684         if (newtime - s_oldtime < 600)
685                 return;
686         s_oldtime = newtime;
687
688         const std::vector<ChatFormattedFragment> &
689                         frags = m_chat_backend->getConsoleBuffer().getFormattedLine(row).fragments;
690         std::string weblink = ""; // from frag meta
691
692         // Identify targetted fragment, if exists
693         int indx = frags.size() - 1;
694         if (indx < 0) {
695                 // Invalid row, frags is empty
696                 return;
697         }
698         // Scan from right to left, offset by 1 font space because left margin
699         while (indx > -1 && (u32)col < frags[indx].column + 1) {
700                 --indx;
701         }
702         if (indx > -1) {
703                 weblink = frags[indx].weblink;
704                 // Note if(indx < 0) then a frag somehow had a corrupt column field
705         }
706
707         /*
708         // Debug help. Please keep this in case adjustments are made later.
709         std::string ws;
710         ws = "Middleclick: (" + std::to_string(col) + ',' + std::to_string(row) + ')' + " frags:";
711         // show all frags <position>(<length>) for the clicked row
712         for (u32 i=0;i<frags.size();++i) {
713                 if (indx == int(i))
714                         // tag the actual clicked frag
715                         ws += '*';
716                 ws += std::to_string(frags.at(i).column) + '('
717                         + std::to_string(frags.at(i).text.size()) + "),";
718         }
719         actionstream << ws << std::endl;
720         */
721
722         // User notification
723         if (weblink.size() != 0) {
724                 std::ostringstream msg;
725                 msg << " * ";
726                 if (porting::open_url(weblink)) {
727                         msg << gettext("Opening webpage");
728                 }
729                 else {
730                         msg << gettext("Failed to open webpage");
731                 }
732                 msg << " '" << weblink << "'";
733                 msg.flush();
734                 m_chat_backend->addUnparsedMessage(utf8_to_wide(msg.str()));
735         }
736 }