diff options
Diffstat (limited to 'src/plugins/extensionmanager/extensionmanagerwidget.cpp')
-rw-r--r-- | src/plugins/extensionmanager/extensionmanagerwidget.cpp | 188 |
1 files changed, 182 insertions, 6 deletions
diff --git a/src/plugins/extensionmanager/extensionmanagerwidget.cpp b/src/plugins/extensionmanager/extensionmanagerwidget.cpp index 34c96de5f0b..377e63fd4e4 100644 --- a/src/plugins/extensionmanager/extensionmanagerwidget.cpp +++ b/src/plugins/extensionmanager/extensionmanagerwidget.cpp @@ -35,9 +35,11 @@ #include <utils/temporarydirectory.h> #include <utils/utilsicons.h> +#include <QAbstractTextDocumentLayout> #include <QAction> #include <QApplication> #include <QBuffer> +#include <QCache> #include <QCheckBox> #include <QHBoxLayout> #include <QImageReader> @@ -46,8 +48,9 @@ #include <QPainter> #include <QProgressDialog> #include <QScrollArea> -#include <QTextDocument> #include <QTextBlock> +#include <QTextBrowser> +#include <QTextDocument> using namespace Core; using namespace Utils; @@ -377,6 +380,171 @@ private: QWidget *m_container = nullptr; }; +class AnimatedImageHandler : public QObject, public QTextObjectInterface +{ + Q_OBJECT + Q_INTERFACES(QTextObjectInterface) + + class Entry + { + public: + Entry(const QByteArray &data) + { + if (data.isEmpty()) + return; + + buffer.setData(data); + movie.setDevice(&buffer); + } + + QBuffer buffer; + QMovie movie; + }; + +public: + AnimatedImageHandler( + QObject *parent, + std::function<void()> redraw, + std::function<void(const QString &name)> scheduleLoad) + : QObject(parent) + , m_redraw(redraw) + , m_scheduleLoad(scheduleLoad) + , m_entries(1024 * 1024 * 10) // 10 MB max image cache size + {} + + virtual QSizeF intrinsicSize( + QTextDocument *doc, int posInDocument, const QTextFormat &format) override + { + Q_UNUSED(doc); + Q_UNUSED(posInDocument); + QString name = format.toImageFormat().name(); + + Entry *entry = m_entries.object(name); + + if (entry && entry->movie.isValid()) { + if (!entry->movie.frameRect().isValid()) + entry->movie.jumpToFrame(0); + return entry->movie.frameRect().size(); + } else if (!entry) { + m_scheduleLoad(name); + } + + return Utils::Icons::UNKNOWN_FILE.icon().actualSize(QSize(16, 16)); + } + + void drawObject( + QPainter *painter, + const QRectF &rect, + QTextDocument *document, + int posInDocument, + const QTextFormat &format) override + { + Q_UNUSED(document); + Q_UNUSED(posInDocument); + + Entry *entry = m_entries.object(format.toImageFormat().name()); + + if (entry) { + if (entry->movie.isValid()) + painter->drawImage(rect, entry->movie.currentImage()); + else + painter->drawPixmap(rect.toRect(), m_brokenImage.pixmap(rect.size().toSize())); + return; + } + + painter->drawPixmap( + rect.toRect(), Utils::Icons::UNKNOWN_FILE.icon().pixmap(rect.size().toSize())); + } + + void set(const QString &name, QByteArray data) + { + if (data.size() > m_entries.maxCost()) + data.clear(); + + std::unique_ptr<Entry> entry = std::make_unique<Entry>(data); + + if (entry->movie.frameCount() > 1) { + connect(&entry->movie, &QMovie::frameChanged, this, [this]() { m_redraw(); }); + entry->movie.start(); + } + if (m_entries.insert(name, entry.release(), qMax(1, data.size()))) + m_redraw(); + } + +private: + std::function<void()> m_redraw; + std::function<void(const QString &)> m_scheduleLoad; + QCache<QString, Entry> m_entries; + + const Icon ErrorCloseIcon = Utils::Icon({{":/utils/images/close.png", Theme::IconsErrorColor}}); + + const QIcon m_brokenImage = Icon::combinedIcon( + {Utils::Icons::UNKNOWN_FILE.icon(), ErrorCloseIcon.icon()}); +}; + +class AnimatedDocument : public QTextDocument +{ +public: + AnimatedDocument(QObject *parent = nullptr) + : QTextDocument(parent) + , m_imageHandler( + this, + [this]() { documentLayout()->update(); }, + [this](const QString &name) { scheduleLoad(QUrl(name)); }) + { + connect(this, &QTextDocument::documentLayoutChanged, this, [this]() { + documentLayout()->registerHandler(QTextFormat::ImageObject, &m_imageHandler); + }); + + connect(this, &QTextDocument::contentsChanged, this, [this]() { + if (m_imageLoaderTree.isRunning()) + m_imageLoaderTree.cancel(); + + if (urlsToLoad.isEmpty()) + return; + + using namespace Tasking; + + const LoopList iterator(urlsToLoad); + + auto onQuerySetup = [iterator](NetworkQuery &query) { + query.setRequest(QNetworkRequest(*iterator)); + query.setNetworkAccessManager(NetworkAccessManager::instance()); + }; + + auto onQueryDone = [this](const NetworkQuery &query, DoneWith result) { + if (result == DoneWith::Success) + m_imageHandler.set(query.reply()->url().toString(), query.reply()->readAll()); + else { + m_imageHandler.set(query.reply()->url().toString(), {}); + } + }; + + // clang-format off + Group group { + For(iterator) >> Do { + continueOnError, + NetworkQueryTask{onQuerySetup, onQueryDone}, + }, + onGroupDone([this]() { + urlsToLoad.clear(); + markContentsDirty(0, this->characterCount()); + }) + }; + // clang-format on + + m_imageLoaderTree.start(group); + }); + } + + void scheduleLoad(const QUrl &url) { urlsToLoad.append(url); } + +private: + AnimatedImageHandler m_imageHandler; + QList<QUrl> urlsToLoad; + Tasking::TaskTreeRunner m_imageLoaderTree; +}; + class ExtensionManagerWidget final : public Core::ResizeSignallingWidget { public: @@ -393,7 +561,7 @@ private: HeadingWidget *m_headingWidget; QWidget *m_primaryContent; QWidget *m_secondaryContent; - QLabel *m_description; + QTextBrowser *m_description; QLabel *m_dateUpdatedTitle; QLabel *m_dateUpdated; QLabel *m_tagsTitle; @@ -418,19 +586,23 @@ ExtensionManagerWidget::ExtensionManagerWidget() m_secondaryDescriptionWidget = new CollapsingWidget; m_headingWidget = new HeadingWidget; - m_description = new QLabel; - m_description->setWordWrap(true); - m_description->setTextInteractionFlags(Qt::TextBrowserInteraction); + m_description = new QTextBrowser; + m_description->setDocument(new AnimatedDocument(m_description)); + m_description->setFrameStyle(QFrame::NoFrame); m_description->setOpenExternalLinks(true); + QPalette browserPal = m_description->palette(); + browserPal.setColor(QPalette::Base, creatorColor(Theme::Token_Background_Default)); + m_description->setPalette(browserPal); using namespace Layouting; auto primary = new QWidget; const auto spL = spacing(SpacingTokens::VPaddingL); + // clang-format off Column { m_description, - st, noMargin, spacing(SpacingTokens::ExVPaddingGapXl), }.attachTo(primary); + // clang-format on m_primaryContent = toScrollableColumn(primary); m_dateUpdatedTitle = sectionTitle(h6TF, Tr::tr("Last Update")); @@ -527,6 +699,10 @@ static QString markdownToHtml(const QString &markdown) for (QTextBlock block = doc.begin(); block != doc.end(); block = block.next()) { QTextBlockFormat blockFormat = block.blockFormat(); + // Leave images as they are. + if (block.text().contains(QChar::ObjectReplacementCharacter)) + continue; + if (blockFormat.hasProperty(QTextFormat::HeadingLevel)) blockFormat.setTopMargin(SpacingTokens::ExVPaddingGapXl); else |