diff --git a/src/emucore/FBSurface.cxx b/src/emucore/FBSurface.cxx index 61c26cf49..b4d793cad 100644 --- a/src/emucore/FBSurface.cxx +++ b/src/emucore/FBSurface.cxx @@ -383,7 +383,8 @@ int FBSurface::drawString(const GUI::Font& font, const string& s, void FBSurface::drawString(const GUI::Font& font, const string& s, int x, int y, int w, ColorId color, TextAlign align, - int deltax, bool useEllipsis, ColorId shadowColor) + int deltax, bool useEllipsis, ColorId shadowColor, + size_t linkStart, int linkLen, bool underline) { #ifdef GUI_SUPPORT const string ELLIPSIS = "\x1d"; // "..." @@ -424,16 +425,30 @@ void FBSurface::drawString(const GUI::Font& font, const string& s, x = x + w - width; x += deltax; + + int x0 = 0, x1 = 0; + for(i = 0; i < str.size(); ++i) { w = font.getCharWidth(str[i]); - if(x+w > rightX) + if(x + w > rightX) break; if(x >= leftX) - drawChar(font, str[i], x, y, color, shadowColor); + { + if(i == linkStart) + x0 = x; + else if(i < linkStart + linkLen) + x1 = x + w; + drawChar(font, str[i], x, y, + (i >= linkStart && i < linkStart + linkLen) ? kTextColorEm : color, + shadowColor); + } x += w; } + if(underline && x1 > 0) + hLine(x0, y + font.getFontHeight() - 1, x1, kTextColorEm); + #endif } diff --git a/src/emucore/FBSurface.hxx b/src/emucore/FBSurface.hxx index 2e8bf383d..902a7094f 100644 --- a/src/emucore/FBSurface.hxx +++ b/src/emucore/FBSurface.hxx @@ -245,7 +245,8 @@ class FBSurface virtual void drawString( const GUI::Font& font, const string& s, int x, int y, int w, ColorId color, TextAlign align = TextAlign::Left, - int deltax = 0, bool useEllipsis = true, ColorId shadowColor = kNone); + int deltax = 0, bool useEllipsis = true, ColorId shadowColor = kNone, + size_t linkStart = string::npos, int linkLen = 0, bool underline = false); /** Splits a given string to a given width considering whitespaces. diff --git a/src/gui/AboutDialog.cxx b/src/gui/AboutDialog.cxx index 743c966bd..c49f7d780 100644 --- a/src/gui/AboutDialog.cxx +++ b/src/gui/AboutDialog.cxx @@ -21,6 +21,7 @@ #include "Widget.hxx" #include "Font.hxx" #include "WhatsNewDialog.hxx" +#include "MediaFactory.hxx" #include "AboutDialog.hxx" @@ -80,8 +81,10 @@ AboutDialog::AboutDialog(OSystem& osystem, DialogContainer& parent, xpos = HBORDER * 2; ypos += lineHeight + VGAP * 2; for(int i = 0; i < myLinesPerPage; i++) { - myDesc.push_back(new StaticTextWidget(this, font, xpos, ypos, _w - xpos * 2, - fontHeight, "", TextAlign::Left)); + StaticTextWidget* s = new StaticTextWidget(this, font, xpos, ypos, _w - xpos * 2, + fontHeight, "", TextAlign::Left, kNone); + s->setID(i); + myDesc.push_back(s); myDescStr.emplace_back(""); ypos += fontHeight; } @@ -162,7 +165,7 @@ void AboutDialog::updateStrings(int page, int lines, string& title) title = "Cast of thousands"; ADD_ATEXT("\\L\\c0""Special thanks to AtariAge for introducing the"); ADD_ATEXT("\\L\\c0""Atari 2600 to a whole new generation."); - ADD_ATEXT("\\L\\c2"" http://www.atariage.com"); + ADD_ATEXT("\\L http://www.atariage.com"); ADD_ALINE(); ADD_ATEXT("\\L\\c0""Finally, a huge thanks to the original Atari 2600"); ADD_ATEXT("\\L\\c0""VCS team for giving us the magic, and to the"); @@ -242,6 +245,7 @@ void AboutDialog::displayInfo() myDesc[i]->setAlign(align); myDesc[i]->setTextColor(color); myDesc[i]->setLabel(str); + myDesc[i]->setUrl(); // extract URL from label } // Redraw entire dialog @@ -280,7 +284,52 @@ void AboutDialog::handleCommand(CommandSender* sender, int cmd, int data, int id myWhatsNewDialog->open(); break; + case StaticTextWidget::kOpenUrlCmd: + { + const string url = myDesc[id]->getUrl(); // getUrl(myDescStr[id]); + + if(url != EmptyString) + MediaFactory::openURL(url); + break; + } + default: Dialog::handleCommand(sender, cmd, data, 0); } } + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +const string AboutDialog::getUrl(const string& str) const +{ + bool isUrl = false; + int start = 0, len = 0; + + for(int i = 0; i < str.size(); ++i) + { + string remainder = str.substr(i); + char ch = str[i]; + + if(!isUrl + && (BSPF::startsWithIgnoreCase(remainder, "http://") + || BSPF::startsWithIgnoreCase(remainder, "https://") + || BSPF::startsWithIgnoreCase(remainder, "www."))) + { + isUrl = true; + start = i; + } + + // hack, change mode without changing string length + if(isUrl) + { + if((ch == ' ' || ch == ')' || ch == '>')) + isUrl = false; + else + len++; + } + } + if(len) + return str.substr(start, len); + else + return EmptyString; +} + diff --git a/src/gui/AboutDialog.hxx b/src/gui/AboutDialog.hxx index d15b45475..c6fe79bfc 100644 --- a/src/gui/AboutDialog.hxx +++ b/src/gui/AboutDialog.hxx @@ -38,6 +38,7 @@ class AboutDialog : public Dialog void handleCommand(CommandSender* sender, int cmd, int data, int id) override; void updateStrings(int page, int lines, string& title); void displayInfo(); + const string getUrl(const string& text) const; void loadConfig() override { displayInfo(); } diff --git a/src/gui/Widget.cxx b/src/gui/Widget.cxx index 16db4c89a..20168b95a 100644 --- a/src/gui/Widget.cxx +++ b/src/gui/Widget.cxx @@ -462,6 +462,7 @@ StaticTextWidget::StaticTextWidget(GuiObject* boss, const GUI::Font& font, const string& text, TextAlign align, ColorId shadowColor) : Widget(boss, font, x, y, w, h), + CommandSender(boss), _label{text}, _align{align} { @@ -472,6 +473,7 @@ StaticTextWidget::StaticTextWidget(GuiObject* boss, const GUI::Font& font, _textcolor = kTextColor; _textcolorhi = kTextColor; _shadowcolor = shadowColor; + _cmd = 0; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -500,29 +502,116 @@ void StaticTextWidget::setLabel(const string& label) } } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void StaticTextWidget::setLink(size_t start, int len, bool underline) +{ + if(_linkStart != start || _linkLen != len || _linkUnderline != underline) + { + _linkStart = start; + _linkLen = len; + _linkUnderline = underline; + setCmd(len ? kClickedCmd : 0); + setDirty(); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +bool StaticTextWidget::setUrl(const string& url, const string& label) +{ + size_t start = string::npos; + int len = 0; + const string text = label != EmptyString ? label : url; + + + if(text != EmptyString) + { + // determine position of label + if((start = BSPF::findIgnoreCase(_label, text)) != string::npos) + { + len = int(text.size()); + _url = url; + } + } + else + { + // extract URL from _label + start = BSPF::findIgnoreCase(_label, "http://"); + + if(start == string::npos) + start = BSPF::findIgnoreCase(_label, "https://"); + if(start == string::npos) + start = BSPF::findIgnoreCase(_label, "www."); + + + if(start != string::npos) + { + // find end of URL + for(int i = int(start); i < _label.size(); ++i) + { + char ch = _label[i]; + + if(ch == ' ' || ch == ')' || ch == '>') + { + len = i - int(start); + _url = _label.substr(start, len); + break; + } + } + if(!len) + { + len = int(_label.size() - start); + _url = _label.substr(start); + } + } + } + + if(len) + { + setLink(start, len, true); + setCmd(kOpenUrlCmd); + return true; + } + else + { + setLink(); // clear link + _url = EmptyString; + return false; + } +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void StaticTextWidget::handleMouseEntered() { if(isEnabled()) - // Mouse focus for tooltips must not change dirty status - setFlags(Widget::FLAG_MOUSE_FOCUS, false); + setFlags(Widget::FLAG_HILITED | Widget::FLAG_MOUSE_FOCUS, _linkLen); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void StaticTextWidget::handleMouseLeft() { if(isEnabled()) - // Mouse focus for tooltips must not change dirty status - clearFlags(Widget::FLAG_MOUSE_FOCUS, false); + clearFlags(Widget::FLAG_HILITED | Widget::FLAG_MOUSE_FOCUS, _linkLen); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void StaticTextWidget::handleMouseUp(int x, int y, MouseButton b, int clickCount) +{ + if(_cmd && isEnabled() && x >= 0 && x < _w && y >= 0 && y < _h) + { + clearFlags(Widget::FLAG_HILITED); + sendCommand(_cmd, 0, _id); + } +} + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - void StaticTextWidget::drawWidget(bool hilite) { FBSurface& s = _boss->dialog().surface(); s.drawString(_font, _label, _x, _y, _w, - isEnabled() ? _textcolor : kColor, _align, 0, true, _shadowcolor); + isEnabled() ? _textcolor : kColor, _align, 0, true, + _shadowcolor, _linkStart, _linkLen, _linkUnderline && hilite); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -530,7 +619,6 @@ ButtonWidget::ButtonWidget(GuiObject* boss, const GUI::Font& font, int x, int y, int w, int h, const string& label, int cmd, bool repeat) : StaticTextWidget(boss, font, x, y, w, h, label, TextAlign::Center), - CommandSender(boss), _cmd{cmd}, _repeat{repeat} { diff --git a/src/gui/Widget.hxx b/src/gui/Widget.hxx index 169c41876..f21ef91cd 100644 --- a/src/gui/Widget.hxx +++ b/src/gui/Widget.hxx @@ -191,8 +191,14 @@ class Widget : public GuiObject }; /* StaticTextWidget */ -class StaticTextWidget : public Widget +class StaticTextWidget : public Widget, public CommandSender { + public: + enum { + kClickedCmd = 'STcl', + kOpenUrlCmd = 'STou' + }; + public: StaticTextWidget(GuiObject* boss, const GUI::Font& font, int x, int y, int w, int h, @@ -204,8 +210,7 @@ class StaticTextWidget : public Widget ColorId shadowColor = kNone); ~StaticTextWidget() override = default; - void handleMouseEntered() override; - void handleMouseLeft() override; + void setCmd(int cmd) { _cmd = cmd; } void setValue(int value); void setLabel(const string& label); @@ -213,13 +218,26 @@ class StaticTextWidget : public Widget const string& getLabel() const { return _label; } bool isEditable() const { return _editable; } + void setLink(size_t start = string::npos, int len = 0, bool underline = false); + bool setUrl(const string& url = EmptyString, const string& label = EmptyString); + const string& getUrl() const { return _url; }; + protected: + void handleMouseEntered() override; + void handleMouseLeft() override; + void handleMouseUp(int x, int y, MouseButton b, int clickCount) override; + void drawWidget(bool hilite) override; protected: string _label; bool _editable{false}; TextAlign _align{TextAlign::Left}; + int _cmd{0}; + size_t _linkStart{string::npos}; + int _linkLen{0}; + bool _linkUnderline{false}; + string _url; private: // Following constructors and assignment operators not supported @@ -231,7 +249,7 @@ class StaticTextWidget : public Widget }; /* ButtonWidget */ -class ButtonWidget : public StaticTextWidget, public CommandSender +class ButtonWidget : public StaticTextWidget { public: ButtonWidget(GuiObject* boss, const GUI::Font& font,