aseprite/src/app/ui/news_listbox.cpp

364 lines
8.9 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Aseprite
// Copyright (C) 2020-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/ui/news_listbox.h"
#include "app/app.h"
#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
#include "app/res/http_loader.h"
#include "app/ui/skin/skin_theme.h"
#include "app/xml_document.h"
#include "base/fs.h"
#include "base/string.h"
#include "base/time.h"
#include "text/font_metrics.h"
#include "ui/link_label.h"
#include "ui/message.h"
#include "ui/paint_event.h"
#include "ui/size_hint_event.h"
#include "ui/view.h"
#include "ver/info.h"
#include "tinyxml2.h"
#include <cctype>
#include <sstream>
namespace app {
using namespace app::skin;
using namespace tinyxml2;
using namespace ui;
namespace {
std::string convert_html_entity(const std::string& e)
{
if (e.size() >= 3 && e[0] == '#' && std::isdigit(e[1])) {
long unicodeChar;
if (e[2] == 'x')
unicodeChar = std::strtol(e.c_str() + 1, nullptr, 16);
else
unicodeChar = std::strtol(e.c_str() + 1, nullptr, 10);
if (unicodeChar == 0x2018)
return "\x60";
if (unicodeChar == 0x2019)
return "'";
else {
std::wstring wstr(1, (wchar_t)unicodeChar);
return base::to_utf8(wstr);
}
}
else if (e == "lt")
return "<";
else if (e == "gt")
return ">";
else if (e == "amp")
return "&";
return "";
}
std::string parse_html(const std::string& str)
{
bool paraOpen = true;
std::string result;
size_t i = 0;
while (i < str.size()) {
// Ignore content between <...> symbols
if (str[i] == '<') {
size_t j = ++i;
while (i < str.size() && str[i] != '>')
++i;
if (i < str.size()) {
ASSERT(str[i] == '>');
std::string tag = str.substr(j, i - j);
if (tag == "li") {
if (!paraOpen && result.back() != '\n')
result.push_back('\n');
result.push_back((char)0xc2);
result.push_back((char)0xb7); // middle dot
result.push_back(' ');
paraOpen = false;
}
else if (tag == "p" || tag == "ul" || tag[0] == 'h') {
if (!paraOpen && result.back() != '\n')
result.push_back('\n');
paraOpen = true;
}
++i;
}
}
else if (str[i] == '&') {
size_t j = ++i;
while (i < str.size() && str[i] != ';')
++i;
if (i < str.size()) {
ASSERT(str[i] == ';');
std::string entity = str.substr(j, i - j);
result += convert_html_entity(entity);
++i;
}
paraOpen = false;
}
// Replace "right single quotation mark" = "" = 0x2019 = 0xe2
// 0x80 0x99 (utf8) with ASCII char "'", useful for news phrases
// like "What's new? ..." or "We're ..." and to avoid
// anti-aliasing (using a TTF font) as the Aseprite font doesn't
// contain this character yet.
else if (i + 2 < str.size() && ((unsigned char)str[i]) == 0xe2 &&
((unsigned char)str[i + 1]) == 0x80 && ((unsigned char)str[i + 2]) == 0x99) {
result.push_back('\'');
i += 3;
paraOpen = false;
}
else {
auto character = str[i++];
if (character == '\n') {
// Rely only on paragraphs for newlines, otherwise we render them as just spaces because
// sometimes they show up in the middle of sentences.
result.push_back(' ');
}
else {
result.push_back(character);
}
paraOpen = false;
}
}
return result;
}
} // namespace
class NewsItem : public LinkLabel {
public:
NewsItem(const std::string& link, const std::string& title, const std::string& desc)
: LinkLabel(link, title)
, m_title(title)
, m_desc(desc)
{
}
protected:
void onSizeHint(SizeHintEvent& ev) override
{
auto theme = SkinTheme::get(this);
ui::Style* style = theme->styles.newsItem();
gfx::Size sz = theme->calcSizeHint(this, style);
if (!m_desc.empty()) {
// Title + number of total lines
sz.h *= 1 + std::count(m_desc.begin(), m_desc.end(), '\n');
}
ev.setSizeHint(gfx::Size(0, sz.h));
}
void onPaint(PaintEvent& ev) override
{
auto theme = SkinTheme::get(this);
Graphics* g = ev.graphics();
gfx::Rect bounds = clientBounds();
ui::Style* style = theme->styles.newsItem();
ui::Style* styleDetail = theme->styles.newsItemDetail();
gfx::Size textSize = theme->calcSizeHint(this, style);
gfx::Rect textBounds(bounds.x, bounds.y, bounds.w, textSize.h);
gfx::Rect detailsBounds(bounds.x, bounds.y + textSize.h, bounds.w, bounds.h - textSize.h);
gfx::Border border = theme->calcBorder(this, style);
gfx::Border borderDetail = theme->calcBorder(this, styleDetail);
if (!m_titleBlob)
m_titleBlob = text::TextBlob::MakeWithShaper(theme->fontMgr(), font(), m_title);
PaintWidgetPartInfo info(this);
info.baseline = border.top();
if (m_titleBlob) {
info.textBlob = m_titleBlob;
info.baseline += m_titleBlob->baseline();
theme->paintWidgetPart(g, style, bounds, info);
info.baseline += border.bottom();
info.text = &m_desc;
info.textBlob = nullptr;
info.baseline += borderDetail.top() + m_titleBlob->baseline();
theme->paintWidgetPart(g, styleDetail, detailsBounds, info);
}
}
private:
std::string m_title;
std::string m_desc;
text::TextBlobRef m_titleBlob;
};
class ProblemsItem : public NewsItem {
public:
ProblemsItem() : NewsItem("", Strings::news_listbox_problem_loading(), "") {}
protected:
void onClick() override { static_cast<NewsListBox*>(parent())->reload(); }
};
NewsListBox::NewsListBox() : m_timer(250, this), m_loader(nullptr)
{
m_timer.Tick.connect(&NewsListBox::onTick, this);
std::string cache = Preferences::instance().news.cacheFile();
if (!cache.empty() && base::is_file(cache) && validCache(cache))
parseFile(cache);
else
reload();
}
NewsListBox::~NewsListBox()
{
if (m_timer.isRunning())
m_timer.stop();
delete m_loader;
m_loader = nullptr;
}
void NewsListBox::reload()
{
if (m_loader || m_timer.isRunning())
return;
while (auto child = lastChild())
removeChild(child);
View* view = View::getView(this);
if (view)
view->updateView();
m_loader = new HttpLoader(get_app_news_rss_url());
m_timer.start();
}
bool NewsListBox::onProcessMessage(ui::Message* msg)
{
switch (msg->type()) {
case kCloseMessage:
if (m_loader)
m_loader->abort();
break;
}
return ListBox::onProcessMessage(msg);
}
void NewsListBox::onTick()
{
if (!m_loader || !m_loader->isDone())
return;
std::string fn = m_loader->filename();
delete m_loader;
m_loader = nullptr;
m_timer.stop();
if (fn.empty()) {
addChild(new ProblemsItem());
View::getView(this)->updateView();
return;
}
parseFile(fn);
}
void NewsListBox::parseFile(const std::string& filename)
{
View* view = View::getView(this);
XMLDocumentRef doc;
try {
doc = open_xml(filename);
}
catch (...) {
addChild(new ProblemsItem());
if (view)
view->updateView();
return;
}
XMLHandle handle(doc.get());
XMLElement* itemXml = handle.FirstChildElement("rss")
.FirstChildElement("channel")
.FirstChildElement("item")
.ToElement();
int count = 0;
while (itemXml) {
XMLElement* titleXml = itemXml->FirstChildElement("title");
XMLElement* descXml = itemXml->FirstChildElement("description");
XMLElement* linkXml = itemXml->FirstChildElement("link");
if (titleXml && titleXml->GetText() && descXml && descXml->GetText() && linkXml &&
linkXml->GetText()) {
std::string link = linkXml->GetText();
std::string title = titleXml->GetText();
std::string desc = parse_html(descXml->GetText());
// Limit the description text to 4 lines
std::string::size_type i = 0;
int j = 0;
while (true) {
i = desc.find('\n', i);
if (i == std::string::npos)
break;
i++;
j++;
if (j == 5)
desc = desc.substr(0, i);
}
addChild(new NewsItem(link, title, desc));
if (++count == 4)
break;
}
itemXml = itemXml->NextSiblingElement();
}
XMLElement* linkXml = handle.FirstChildElement("rss")
.FirstChildElement("channel")
.FirstChildElement("link")
.ToElement();
if (linkXml && linkXml->GetText())
addChild(new NewsItem(linkXml->GetText(), Strings::news_listbox_more(), ""));
if (view)
view->updateView();
// Save as cached news
Preferences::instance().news.cacheFile(filename);
}
bool NewsListBox::validCache(const std::string& filename)
{
base::Time now = base::current_time(), time = base::get_modification_time(filename);
now.dateOnly();
time.dateOnly();
return (now == time);
}
} // namespace app