forked from qt-creator/qt-creator
ExtensionManager: Add support for (animated) Images in description
Change-Id: I3f651e3bae5d1934b55185cca7ade49b21ffba3d Reviewed-by: Alessandro Portale <alessandro.portale@qt.io>
This commit is contained in:
@@ -35,9 +35,11 @@
|
|||||||
#include <utils/temporarydirectory.h>
|
#include <utils/temporarydirectory.h>
|
||||||
#include <utils/utilsicons.h>
|
#include <utils/utilsicons.h>
|
||||||
|
|
||||||
|
#include <QAbstractTextDocumentLayout>
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
|
#include <QCache>
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
@@ -46,8 +48,9 @@
|
|||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QProgressDialog>
|
#include <QProgressDialog>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QTextDocument>
|
|
||||||
#include <QTextBlock>
|
#include <QTextBlock>
|
||||||
|
#include <QTextBrowser>
|
||||||
|
#include <QTextDocument>
|
||||||
|
|
||||||
using namespace Core;
|
using namespace Core;
|
||||||
using namespace Utils;
|
using namespace Utils;
|
||||||
@@ -377,6 +380,171 @@ private:
|
|||||||
QWidget *m_container = nullptr;
|
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
|
class ExtensionManagerWidget final : public Core::ResizeSignallingWidget
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -393,7 +561,7 @@ private:
|
|||||||
HeadingWidget *m_headingWidget;
|
HeadingWidget *m_headingWidget;
|
||||||
QWidget *m_primaryContent;
|
QWidget *m_primaryContent;
|
||||||
QWidget *m_secondaryContent;
|
QWidget *m_secondaryContent;
|
||||||
QLabel *m_description;
|
QTextBrowser *m_description;
|
||||||
QLabel *m_dateUpdatedTitle;
|
QLabel *m_dateUpdatedTitle;
|
||||||
QLabel *m_dateUpdated;
|
QLabel *m_dateUpdated;
|
||||||
QLabel *m_tagsTitle;
|
QLabel *m_tagsTitle;
|
||||||
@@ -418,19 +586,23 @@ ExtensionManagerWidget::ExtensionManagerWidget()
|
|||||||
m_secondaryDescriptionWidget = new CollapsingWidget;
|
m_secondaryDescriptionWidget = new CollapsingWidget;
|
||||||
|
|
||||||
m_headingWidget = new HeadingWidget;
|
m_headingWidget = new HeadingWidget;
|
||||||
m_description = new QLabel;
|
m_description = new QTextBrowser;
|
||||||
m_description->setWordWrap(true);
|
m_description->setDocument(new AnimatedDocument(m_description));
|
||||||
m_description->setTextInteractionFlags(Qt::TextBrowserInteraction);
|
m_description->setFrameStyle(QFrame::NoFrame);
|
||||||
m_description->setOpenExternalLinks(true);
|
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;
|
using namespace Layouting;
|
||||||
auto primary = new QWidget;
|
auto primary = new QWidget;
|
||||||
const auto spL = spacing(SpacingTokens::VPaddingL);
|
const auto spL = spacing(SpacingTokens::VPaddingL);
|
||||||
|
// clang-format off
|
||||||
Column {
|
Column {
|
||||||
m_description,
|
m_description,
|
||||||
st,
|
|
||||||
noMargin, spacing(SpacingTokens::ExVPaddingGapXl),
|
noMargin, spacing(SpacingTokens::ExVPaddingGapXl),
|
||||||
}.attachTo(primary);
|
}.attachTo(primary);
|
||||||
|
// clang-format on
|
||||||
m_primaryContent = toScrollableColumn(primary);
|
m_primaryContent = toScrollableColumn(primary);
|
||||||
|
|
||||||
m_dateUpdatedTitle = sectionTitle(h6TF, Tr::tr("Last Update"));
|
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()) {
|
for (QTextBlock block = doc.begin(); block != doc.end(); block = block.next()) {
|
||||||
QTextBlockFormat blockFormat = block.blockFormat();
|
QTextBlockFormat blockFormat = block.blockFormat();
|
||||||
|
// Leave images as they are.
|
||||||
|
if (block.text().contains(QChar::ObjectReplacementCharacter))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (blockFormat.hasProperty(QTextFormat::HeadingLevel))
|
if (blockFormat.hasProperty(QTextFormat::HeadingLevel))
|
||||||
blockFormat.setTopMargin(SpacingTokens::ExVPaddingGapXl);
|
blockFormat.setTopMargin(SpacingTokens::ExVPaddingGapXl);
|
||||||
else
|
else
|
||||||
|
Reference in New Issue
Block a user