Files
zeal/src/libs/ui/docsetsdialog.cpp
T
2026-05-02 02:43:27 +03:00

903 lines
30 KiB
C++

// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>
// Copyright (C) 2013-2014 Jerzy Kozera
// SPDX-License-Identifier: GPL-3.0-or-later
#include "docsetsdialog.h"
#include "ui_docsetsdialog.h"
#include "docsetlistitemdelegate.h"
#include <core/application.h>
#include <core/filemanager.h>
#include <core/settings.h>
#include <registry/docset.h>
#include <registry/docsetregistry.h>
#include <registry/itemdatarole.h>
#include <util/humanizer.h>
#include <QClipboard>
#include <QDateTime>
#include <QDir>
#include <QInputDialog>
#include <QJsonArray>
#include <QJsonDocument>
#include <QLocale>
#include <QLoggingCategory>
#include <QMessageBox>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTemporaryFile>
#include <QUrl>
#include <memory>
namespace Zeal::WidgetUi {
namespace {
Q_LOGGING_CATEGORY(log, "zeal.widgetui.docsetsdialog")
enum DownloadType {
DownloadDashFeed,
DownloadDocset,
DownloadDocsetList
};
constexpr char ApiServerUrl[] = "https://api.zealdocs.org/v1";
constexpr char RedirectServerUrl[] = "https://go.zealdocs.org/d/%1/%2/latest";
// TODO: Each source plugin should have its own cache
constexpr char DocsetListCacheFileName[] = "com.kapeli.json";
// TODO: Make the timeout period configurable
constexpr int CacheTimeout = 24 * 60 * 60; // 24 hours in seconds
// QNetworkReply properties
constexpr char DocsetNameProperty[] = "docsetName";
constexpr char DownloadTypeProperty[] = "downloadType";
constexpr char ListItemIndexProperty[] = "listItem";
} // namespace
DocsetsDialog::DocsetsDialog(Core::Application *app, QWidget *parent)
: QDialog(parent)
, ui(new Ui::DocsetsDialog())
, m_application(app)
, m_docsetRegistry(app->docsetRegistry())
{
ui->setupUi(this);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
loadDocsetList();
m_isStorageReadOnly = !isDirWritable(m_application->settings()->docsetPath);
#ifdef Q_OS_MACOS
ui->availableDocsetList->setAttribute(Qt::WA_MacShowFocusRect, false);
ui->installedDocsetList->setAttribute(Qt::WA_MacShowFocusRect, false);
#endif
ui->statusLabel->clear(); // Clear text shown in the designer mode.
ui->storageStatusLabel->setVisible(m_isStorageReadOnly);
const QFileInfo fi(m_application->settings()->docsetPath);
ui->storageStatusLabel->setText(fi.exists() ? tr("<b>Docset storage is read only.</b>")
: tr("<b>Docset storage does not exist.</b>"));
connect(m_application, &Core::Application::extractionCompleted, this, &DocsetsDialog::extractionCompleted);
connect(m_application, &Core::Application::extractionError, this, &DocsetsDialog::extractionError);
connect(m_application, &Core::Application::extractionProgress, this, &DocsetsDialog::extractionProgress);
// Setup signals & slots
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton *button) {
if (button == ui->buttonBox->button(QDialogButtonBox::Cancel)) {
cancelDownloads();
return;
}
if (button == ui->buttonBox->button(QDialogButtonBox::Close)) {
close();
return;
}
});
setupInstalledDocsetsTab();
setupAvailableDocsetsTab();
if (m_isStorageReadOnly) {
disableControls();
}
}
DocsetsDialog::~DocsetsDialog()
{
delete ui;
}
void DocsetsDialog::addDashFeed()
{
QString clipboardText = QApplication::clipboard()->text();
if (!clipboardText.startsWith(QLatin1String("dash-feed://"))) {
clipboardText.clear();
}
QString feedUrl = QInputDialog::getText(this,
QStringLiteral("Zeal"),
tr("Feed URL:"),
QLineEdit::Normal,
clipboardText);
feedUrl = feedUrl.trimmed();
if (feedUrl.isEmpty()) {
return;
}
if (feedUrl.startsWith(QLatin1String("dash-feed://"))) {
feedUrl = feedUrl.remove(0, 12);
feedUrl = QUrl::fromPercentEncoding(feedUrl.toUtf8());
}
QNetworkReply *reply = download(QUrl(feedUrl));
reply->setProperty(DownloadTypeProperty, DownloadType::DownloadDashFeed);
}
void DocsetsDialog::updateSelectedDocsets()
{
const auto selectedRows = ui->installedDocsetList->selectionModel()->selectedRows();
for (const QModelIndex &index : selectedRows) {
if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) {
continue;
}
downloadDashDocset(index);
}
}
void DocsetsDialog::updateAllDocsets()
{
QAbstractItemModel *model = ui->installedDocsetList->model();
for (int i = 0; i < model->rowCount(); ++i) {
const QModelIndex index = model->index(i, 0);
if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) {
continue;
}
downloadDashDocset(index);
}
}
void DocsetsDialog::removeSelectedDocsets()
{
QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel();
if (!selectionModel->hasSelection()) {
return;
}
int ret;
const QModelIndexList selectedIndexes = selectionModel->selectedRows();
if (selectedIndexes.size() == 1) {
const QString docsetTitle = selectedIndexes.first().data().toString();
ret = QMessageBox::question(this, QStringLiteral("Zeal"), tr("Remove <b>%1</b> docset?").arg(docsetTitle));
} else {
ret = QMessageBox::question(this,
QStringLiteral("Zeal"),
tr("Remove <b>%n</b> docset(s)?", nullptr, selectedIndexes.size()));
}
if (ret == QMessageBox::No) {
return;
}
// Gather names first, because model indicies become invalid when docsets are removed.
QStringList names;
for (const QModelIndex &index : selectedIndexes) {
names.append(index.data(Registry::ItemDataRole::DocsetNameRole).toString());
}
for (const QString &name : names) {
removeDocset(name);
}
}
void DocsetsDialog::updateDocsetFilter(const QString &filterString)
{
const bool doSearch = !filterString.simplified().isEmpty();
for (int i = 0; i < ui->availableDocsetList->count(); ++i) {
QListWidgetItem *item = ui->availableDocsetList->item(i);
// Skip installed docsets
if (m_docsetRegistry->contains(item->data(Registry::ItemDataRole::DocsetNameRole).toString())) {
continue;
}
item->setHidden(doSearch && !item->text().contains(filterString, Qt::CaseInsensitive));
}
}
void DocsetsDialog::downloadSelectedDocsets()
{
QItemSelectionModel *selectionModel = ui->availableDocsetList->selectionModel();
const auto selectedRows = selectionModel->selectedRows();
for (const QModelIndex &index : selectedRows) {
selectionModel->select(index, QItemSelectionModel::Deselect);
// Do nothing if a download is already in progress.
if (index.data(DocsetListItemDelegate::ShowProgressRole).toBool()) {
continue;
}
QAbstractItemModel *model = ui->availableDocsetList->model();
model->setData(index, tr("Downloading: %p%"), DocsetListItemDelegate::FormatRole);
model->setData(index, 0, DocsetListItemDelegate::ValueRole);
model->setData(index, true, DocsetListItemDelegate::ShowProgressRole);
downloadDashDocset(index);
}
}
/*!
\internal
Should be connected to all \l QNetworkReply::finished signals in order to process possible
HTTP-redirects correctly.
*/
void DocsetsDialog::downloadCompleted()
{
QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply(qobject_cast<QNetworkReply *>(sender()));
m_replies.removeOne(reply.data());
if (reply->error() != QNetworkReply::NoError) {
if (reply->error() != QNetworkReply::OperationCanceledError) {
const QString msg = tr("Download failed!<br><br><b>Error:</b> %1<br><b>URL:</b> %2")
.arg(reply->errorString(), reply->request().url().toString());
const int ret = QMessageBox::warning(this,
QStringLiteral("Zeal"),
msg,
QMessageBox::Retry | QMessageBox::Cancel);
if (ret == QMessageBox::Retry) {
QNetworkReply *newReply = download(reply->request().url());
// Copy properties
newReply->setProperty(DocsetNameProperty, reply->property(DocsetNameProperty));
newReply->setProperty(DownloadTypeProperty, reply->property(DownloadTypeProperty));
newReply->setProperty(ListItemIndexProperty, reply->property(ListItemIndexProperty));
return;
}
bool ok;
QListWidgetItem *listItem = ui->availableDocsetList->item(
reply->property(ListItemIndexProperty).toInt(&ok));
if (ok && listItem) {
listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);
}
}
updateStatus();
return;
}
switch (reply->property(DownloadTypeProperty).toUInt()) {
case DownloadType::DownloadDocsetList: {
const QByteArray replyData = reply->readAll();
QScopedPointer<QFile> file(new QFile(cacheLocation(DocsetListCacheFileName)));
if (file->open(QIODevice::WriteOnly)) {
file->write(replyData);
file->close(); // Flush to ensure timestamp update on all systems.
}
updateDocsetListDownloadTimeLabel(QFileInfo(file->fileName()).lastModified());
QJsonParseError jsonError;
const QJsonDocument jsonDoc = QJsonDocument::fromJson(replyData, &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
qCWarning(log,
"Failed to parse docset list JSON at offset %lld: %s.",
static_cast<long long>(jsonError.offset),
qPrintable(jsonError.errorString()));
const QMessageBox::StandardButton rc = QMessageBox::warning(this,
QStringLiteral("Zeal"),
tr("Server returned a corrupted docset list."),
QMessageBox::Retry | QMessageBox::Cancel);
if (rc == QMessageBox::Retry) {
downloadDocsetList();
}
break;
}
processDocsetList(jsonDoc.array());
break;
}
case DownloadType::DownloadDashFeed: {
Registry::DocsetMetadata metadata = Registry::DocsetMetadata::fromDashFeed(reply->request().url(),
reply->readAll());
if (metadata.urls().isEmpty()) {
QMessageBox::warning(this, QStringLiteral("Zeal"), tr("Invalid docset feed!"));
break;
}
m_userFeeds[metadata.name()] = metadata;
Registry::Docset *docset = m_docsetRegistry->docset(metadata.name());
if (docset == nullptr) {
// Fetch docset only on first feed download,
// since further downloads are only update checks
QNetworkReply *mdReply = download(metadata.url());
mdReply->setProperty(DocsetNameProperty, metadata.name());
mdReply->setProperty(DownloadTypeProperty, DownloadType::DownloadDocset);
} else {
// Check for feed update
if (metadata.latestVersion() != docset->version() || metadata.revision() > docset->revision()) {
docset->hasUpdate = true;
if (!m_isStorageReadOnly) {
ui->updateAllDocsetsButton->setEnabled(true);
}
ui->installedDocsetList->reset();
}
}
break;
}
case DownloadType::DownloadDocset: {
const QString docsetName = reply->property(DocsetNameProperty).toString();
const QString docsetDirectoryName = docsetName + QLatin1String(".docset");
if (QDir(m_application->settings()->docsetPath).exists(docsetDirectoryName)) {
removeDocset(docsetName);
}
QTemporaryFile *tmpFile = m_tmpFiles[docsetName];
if (!tmpFile) {
tmpFile = new QTemporaryFile(QStringLiteral("%1/%2.XXXXXX.tmp")
.arg(Core::Application::cacheLocation(), docsetName),
this);
if (!tmpFile->open()) {
return;
}
m_tmpFiles.insert(docsetName, tmpFile);
}
while (reply->bytesAvailable()) {
tmpFile->write(reply->read(1024 * 1024)); // Use small chunks.
}
tmpFile->close();
QListWidgetItem *item = findDocsetListItem(docsetName);
if (item) {
item->setData(DocsetListItemDelegate::ValueRole, 0);
item->setData(DocsetListItemDelegate::FormatRole, tr("Installing: %p%"));
}
m_application->extract(tmpFile->fileName(), m_application->settings()->docsetPath, docsetDirectoryName);
break;
}
}
// If all enqueued downloads have finished executing.
updateStatus();
}
// creates a total download progress for multiple QNetworkReplies
void DocsetsDialog::downloadProgress(qint64 received, qint64 total)
{
// Don't show progress for non-docset pages
if (total == -1 || received < 10240) {
return;
}
auto *reply = qobject_cast<QNetworkReply *>(sender());
if (!reply || !reply->isOpen()) {
return;
}
if (reply->property(DownloadTypeProperty).toInt() == DownloadType::DownloadDocset) {
const QString docsetName = reply->property(DocsetNameProperty).toString();
QTemporaryFile *tmpFile = m_tmpFiles[docsetName];
if (!tmpFile) {
tmpFile = new QTemporaryFile(QStringLiteral("%1/%2.XXXXXX.tmp")
.arg(Core::Application::cacheLocation(), docsetName),
this);
if (!tmpFile->open()) {
return;
}
m_tmpFiles.insert(docsetName, tmpFile);
}
tmpFile->write(reply->read(received));
}
// Try to get the item associated to the request
QListWidgetItem *item = ui->availableDocsetList->item(reply->property(ListItemIndexProperty).toInt());
if (item) {
item->setData(DocsetListItemDelegate::ValueRole, percent(received, total));
}
}
void DocsetsDialog::extractionCompleted(const QString &filePath)
{
const QString docsetName = docsetNameForTmpFilePath(filePath);
const QDir dataDir(m_application->settings()->docsetPath);
const QString docsetPath = dataDir.filePath(docsetName + QLatin1String(".docset"));
// Write metadata about docset
Registry::DocsetMetadata metadata = m_availableDocsets.count(docsetName) ? m_availableDocsets[docsetName]
: m_userFeeds[docsetName];
metadata.save(docsetPath, metadata.latestVersion());
m_docsetRegistry->loadDocset(docsetPath);
QListWidgetItem *listItem = findDocsetListItem(docsetName);
if (listItem) {
listItem->setHidden(true);
listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);
}
delete m_tmpFiles.take(docsetName);
updateStatus();
}
void DocsetsDialog::extractionError(const QString &filePath, const QString &errorString)
{
const QString docsetName = docsetNameForTmpFilePath(filePath);
QMessageBox::warning(this,
QStringLiteral("Zeal"),
tr("Cannot extract docset <b>%1</b>: %2").arg(docsetName, errorString));
QListWidgetItem *listItem = findDocsetListItem(docsetName);
if (listItem) {
listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);
}
delete m_tmpFiles.take(docsetName);
}
void DocsetsDialog::extractionProgress(const QString &filePath, qint64 extracted, qint64 total)
{
const QString docsetName = docsetNameForTmpFilePath(filePath);
QListWidgetItem *listItem = findDocsetListItem(docsetName);
if (listItem) {
listItem->setData(DocsetListItemDelegate::ValueRole, percent(extracted, total));
}
}
void DocsetsDialog::loadDocsetList()
{
loadUserFeedList();
const QFileInfo fi(cacheLocation(DocsetListCacheFileName));
if (!fi.exists()) {
downloadDocsetList();
return;
}
const auto age = fi.lastModified().secsTo(QDateTime::currentDateTime());
if (age < 0 || age >= CacheTimeout) {
downloadDocsetList();
return;
}
QScopedPointer<QFile> file(new QFile(fi.filePath()));
if (!file->open(QIODevice::ReadOnly)) {
downloadDocsetList();
return;
}
QJsonParseError jsonError;
const QJsonDocument jsonDoc = QJsonDocument::fromJson(file->readAll(), &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
downloadDocsetList();
return;
}
updateDocsetListDownloadTimeLabel(fi.lastModified());
processDocsetList(jsonDoc.array());
}
void DocsetsDialog::setupInstalledDocsetsTab()
{
ui->installedDocsetList->setItemDelegate(new DocsetListItemDelegate(this));
ui->installedDocsetList->setModel(m_docsetRegistry->model());
ui->installedDocsetList->setItemsExpandable(false);
ui->installedDocsetList->setRootIsDecorated(false);
ui->installedDocsetList->header()->setStretchLastSection(true);
ui->installedDocsetList->header()->setSectionsMovable(false);
ui->installedDocsetList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
ui->installedDocsetList->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
if (m_isStorageReadOnly) {
return;
}
connect(ui->installedDocsetList, &QTreeView::activated, this, [this](const QModelIndex &index) {
if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) {
return;
}
downloadDashDocset(index);
});
QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel();
connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this, selectionModel]() {
ui->removeDocsetsButton->setEnabled(selectionModel->hasSelection());
const auto selectedRows = selectionModel->selectedRows();
for (const QModelIndex &index : selectedRows) {
if (index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) {
ui->updateSelectedDocsetsButton->setEnabled(true);
return;
}
}
ui->updateSelectedDocsetsButton->setEnabled(false);
});
connect(ui->addFeedButton, &QPushButton::clicked, this, &DocsetsDialog::addDashFeed);
connect(ui->updateSelectedDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::updateSelectedDocsets);
connect(ui->updateAllDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::updateAllDocsets);
connect(ui->removeDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::removeSelectedDocsets);
}
void DocsetsDialog::setupAvailableDocsetsTab()
{
using Registry::DocsetRegistry;
ui->availableDocsetList->setItemDelegate(new DocsetListItemDelegate(this));
connect(m_docsetRegistry, &DocsetRegistry::docsetUnloaded, this, [this](const QString &name) {
QListWidgetItem *item = findDocsetListItem(name);
if (!item) {
return;
}
item->setHidden(false);
});
connect(m_docsetRegistry, &DocsetRegistry::docsetLoaded, this, [this](const QString &name) {
QListWidgetItem *item = findDocsetListItem(name);
if (!item) {
return;
}
item->setHidden(true);
});
connect(ui->refreshButton, &QPushButton::clicked, this, &DocsetsDialog::downloadDocsetList);
connect(ui->docsetFilterInput, &QLineEdit::textEdited, this, &DocsetsDialog::updateDocsetFilter);
if (m_isStorageReadOnly) {
return;
}
connect(ui->availableDocsetList, &QListView::activated, this, [this](const QModelIndex &index) {
// TODO: Cancel download if it's already in progress.
if (index.data(DocsetListItemDelegate::ShowProgressRole).toBool()) {
return;
}
ui->availableDocsetList->selectionModel()->select(index, QItemSelectionModel::Deselect);
QAbstractItemModel *model = ui->availableDocsetList->model();
model->setData(index, tr("Downloading: %p%"), DocsetListItemDelegate::FormatRole);
model->setData(index, 0, DocsetListItemDelegate::ValueRole);
model->setData(index, true, DocsetListItemDelegate::ShowProgressRole);
downloadDashDocset(index);
});
QItemSelectionModel *selectionModel = ui->availableDocsetList->selectionModel();
connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this, selectionModel]() {
const auto selectedRows = selectionModel->selectedRows();
for (const QModelIndex &index : selectedRows) {
if (!index.data(DocsetListItemDelegate::ShowProgressRole).toBool()) {
ui->downloadDocsetsButton->setEnabled(true);
return;
}
}
ui->downloadDocsetsButton->setEnabled(false);
});
connect(ui->downloadDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::downloadSelectedDocsets);
}
void DocsetsDialog::enableControls()
{
if (m_isStorageReadOnly || !m_replies.isEmpty() || !m_tmpFiles.isEmpty()) {
return;
}
// Dialog buttons.
ui->buttonBox->setStandardButtons(QDialogButtonBox::Close);
// Available docsets
ui->refreshButton->setEnabled(true);
// Installed docsets
ui->addFeedButton->setEnabled(true);
QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel();
bool hasSelectedUpdates = false;
const auto selectedRows = selectionModel->selectedRows();
for (const QModelIndex &index : selectedRows) {
if (index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) {
hasSelectedUpdates = true;
break;
}
}
ui->updateSelectedDocsetsButton->setEnabled(hasSelectedUpdates);
ui->updateAllDocsetsButton->setEnabled(updatesAvailable());
ui->removeDocsetsButton->setEnabled(selectionModel->hasSelection());
}
void DocsetsDialog::disableControls()
{
// Dialog buttons.
if (!m_isStorageReadOnly) {
// Always show the close button if storage is read only.
ui->buttonBox->setStandardButtons(QDialogButtonBox::Cancel);
}
// Installed docsets
ui->addFeedButton->setEnabled(false);
ui->updateSelectedDocsetsButton->setEnabled(false);
ui->updateAllDocsetsButton->setEnabled(false);
ui->downloadDocsetsButton->setEnabled(false);
ui->removeDocsetsButton->setEnabled(false);
// Available docsets
ui->refreshButton->setEnabled(false);
}
QListWidgetItem *DocsetsDialog::findDocsetListItem(const QString &name) const
{
for (int i = 0; i < ui->availableDocsetList->count(); ++i) {
QListWidgetItem *item = ui->availableDocsetList->item(i);
if (item->data(Registry::ItemDataRole::DocsetNameRole).toString() == name) {
return item;
}
}
return nullptr;
}
bool DocsetsDialog::updatesAvailable() const
{
const auto docsets = m_docsetRegistry->docsets();
for (Registry::Docset *docset : docsets) {
if (docset->hasUpdate) {
return true;
}
}
return false;
}
QNetworkReply *DocsetsDialog::download(const QUrl &url)
{
QNetworkReply *reply = m_application->download(url);
connect(reply, &QNetworkReply::downloadProgress, this, &DocsetsDialog::downloadProgress);
connect(reply, &QNetworkReply::finished, this, &DocsetsDialog::downloadCompleted);
m_replies.append(reply);
disableControls();
updateStatus();
return reply;
}
void DocsetsDialog::cancelDownloads()
{
for (QNetworkReply *reply : std::as_const(m_replies)) {
// Hide progress bar
QListWidgetItem *listItem = ui->availableDocsetList->item(reply->property(ListItemIndexProperty).toInt());
if (listItem) {
listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);
}
if (reply->property(DownloadTypeProperty).toInt() == DownloadType::DownloadDocset) {
delete m_tmpFiles.take(reply->property(DocsetNameProperty).toString());
}
reply->abort();
}
updateStatus();
}
void DocsetsDialog::loadUserFeedList()
{
const auto docsets = m_docsetRegistry->docsets();
for (Registry::Docset *docset : docsets) {
if (!docset->feedUrl().isEmpty()) {
QNetworkReply *reply = download(QUrl(docset->feedUrl()));
reply->setProperty(DownloadTypeProperty, DownloadType::DownloadDashFeed);
}
}
}
void DocsetsDialog::downloadDocsetList()
{
ui->availableDocsetList->clear();
m_availableDocsets.clear();
QNetworkReply *reply = download(QUrl(ApiServerUrl + QLatin1String("/docsets")));
reply->setProperty(DownloadTypeProperty, DownloadType::DownloadDocsetList);
}
void DocsetsDialog::processDocsetList(const QJsonArray &list)
{
for (const QJsonValue &v : list) {
QJsonObject docsetJson = v.toObject();
Registry::DocsetMetadata metadata(docsetJson);
m_availableDocsets.insert({metadata.name(), metadata});
}
// TODO: Move into dedicated method
for (const auto &kv : m_availableDocsets) {
const auto &metadata = kv.second;
auto *listItem = new QListWidgetItem(metadata.icon(), metadata.title(), ui->availableDocsetList);
listItem->setData(Registry::ItemDataRole::DocsetNameRole, metadata.name());
if (!m_docsetRegistry->contains(metadata.name())) {
continue;
}
listItem->setHidden(true);
Registry::Docset *docset = m_docsetRegistry->docset(metadata.name());
if (metadata.latestVersion() != docset->version() || metadata.revision() > docset->revision()) {
docset->hasUpdate = true;
if (!m_isStorageReadOnly) {
ui->updateAllDocsetsButton->setEnabled(true);
}
}
}
ui->installedDocsetList->reset();
// Reapply the filter after repopulating the list.
updateDocsetFilter(ui->docsetFilterInput->text());
}
void DocsetsDialog::updateDocsetListDownloadTimeLabel(const QDateTime &modifiedTime)
{
if (!modifiedTime.isValid()) {
ui->lastUpdatedLabel->clear();
ui->lastUpdatedLabel->setToolTip(QString());
return;
}
ui->lastUpdatedLabel->setText(tr("Last updated %1.").arg(Util::Humanizer::fromNow(modifiedTime)));
const QString updateTime = modifiedTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
ui->lastUpdatedLabel->setToolTip(updateTime);
}
void DocsetsDialog::downloadDashDocset(const QModelIndex &index)
{
const QString name = index.data(Registry::ItemDataRole::DocsetNameRole).toString();
if (m_availableDocsets.count(name) == 0 && !m_userFeeds.contains(name)) {
return;
}
// Skip if an extraction is already in progress for this docset.
if (m_tmpFiles.contains(name)) {
return;
}
// Skip if a download is already in progress for this docset.
for (QNetworkReply *reply : std::as_const(m_replies)) {
if (reply->property(DownloadTypeProperty).toInt() == DownloadType::DownloadDocset
&& reply->property(DocsetNameProperty).toString() == name) {
return;
}
}
QUrl url;
if (!m_userFeeds.contains(name)) {
// No feed present means that this is a Kapeli docset
QString urlString = QString(RedirectServerUrl).arg("com.kapeli").arg(name);
url = QUrl(urlString);
} else {
url = m_userFeeds[name].url();
}
QNetworkReply *reply = download(url);
reply->setProperty(DocsetNameProperty, name);
reply->setProperty(DownloadTypeProperty, DownloadType::DownloadDocset);
reply->setProperty(ListItemIndexProperty, ui->availableDocsetList->row(findDocsetListItem(name)));
}
void DocsetsDialog::removeDocset(const QString &name)
{
if (!m_docsetRegistry->contains(name)) {
return;
}
const QString docsetPath = m_docsetRegistry->docset(name)->path();
m_docsetRegistry->unloadDocset(name);
if (!m_application->fileManager()->removeRecursively(docsetPath)) {
const QString error = tr("Cannot remove directory <b>%1</b>! It might be in use"
" by another process.")
.arg(docsetPath);
QMessageBox::warning(this, QStringLiteral("Zeal"), error);
return;
}
QListWidgetItem *listItem = findDocsetListItem(name);
if (listItem) {
listItem->setHidden(false);
}
}
void DocsetsDialog::updateStatus()
{
QString text;
if (!m_replies.isEmpty()) {
text = tr("Downloading: %n.", nullptr, m_replies.size());
}
if (!m_tmpFiles.isEmpty()) {
text += QLatin1String(" ") + tr("Installing: %n.", nullptr, m_tmpFiles.size());
}
ui->statusLabel->setText(text);
enableControls();
}
QString DocsetsDialog::docsetNameForTmpFilePath(const QString &filePath) const
{
for (auto it = m_tmpFiles.cbegin(), end = m_tmpFiles.cend(); it != end; ++it) {
if (it.value()->fileName() == filePath) {
return it.key();
}
}
return QString();
}
int DocsetsDialog::percent(qint64 fraction, qint64 total)
{
if (!total) {
return 0;
}
return static_cast<int>(fraction / static_cast<double>(total) * 100);
}
QString DocsetsDialog::cacheLocation(const QString &fileName)
{
return QDir(Core::Application::cacheLocation()).filePath(fileName);
}
bool DocsetsDialog::isDirWritable(const QString &path)
{
auto file = std::make_unique<QTemporaryFile>(path + QLatin1String("/.zeal_writable_check_XXXXXX.tmp"));
return file->open();
}
} // namespace Zeal::WidgetUi