mirror of
https://github.com/zealdocs/zeal.git
synced 2026-08-29 08:34:50 +08:00
Compare commits
3 Commits
0dfb36c837
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 454ad0b009 | |||
| e025a0a0dc | |||
| 50f3ddb6bc |
@@ -63,6 +63,8 @@ path = [
|
||||
"assets/freedesktop/*-apps-zeal.png",
|
||||
"assets/freedesktop/sc-apps-zeal.svg",
|
||||
"assets/freedesktop/sc-apps-zeal-tray.svg",
|
||||
"assets/freedesktop/sc-apps-zeal-tray-light.svg",
|
||||
"assets/freedesktop/sc-apps-zeal-tray-dark.svg",
|
||||
"src/app/resources/zeal.icns",
|
||||
"src/app/resources/zeal.ico",
|
||||
]
|
||||
|
||||
@@ -20,6 +20,8 @@ if(UNIX AND NOT APPLE)
|
||||
"512-apps-zeal.png"
|
||||
"sc-apps-zeal.svg"
|
||||
"sc-apps-zeal-tray.svg"
|
||||
"sc-apps-zeal-tray-light.svg"
|
||||
"sc-apps-zeal-tray-dark.svg"
|
||||
DESTINATION ${KDE_INSTALL_ICONDIR}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<title>Zeal</title>
|
||||
<desc>Offline documentation browser — dark monochrome Z silhouette for light system tray panels.</desc>
|
||||
<g fill="#000000" transform="translate(8.446 25.000) scale(0.02514 -0.02514)">
|
||||
<path d="M58.667,0L58.667,112.667L376.833,591.167L71,591.167L71,716L534,716L534,603.333L216.333,124.833L541.333,124.833L541.333,0L58.667,0Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<title>Zeal</title>
|
||||
<desc>Offline documentation browser — light monochrome Z silhouette for dark system tray panels.</desc>
|
||||
<g fill="#ffffff" transform="translate(8.446 25.000) scale(0.02514 -0.02514)">
|
||||
<path d="M58.667,0L58.667,112.667L376.833,591.167L71,591.167L71,716L534,716L534,603.333L216.333,124.833L541.333,124.833L541.333,0L58.667,0Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
@@ -1,7 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<title>Zeal</title>
|
||||
<desc>Offline documentation browser — monochrome Z silhouette for system tray and menu bar icons.</desc>
|
||||
<g transform="translate(8.446 25.000) scale(0.02514 -0.02514)" fill="#000000">
|
||||
<!-- Desktops that recolor tray icons replace this rule with the panel's text
|
||||
color. Those that do not fall back to white, which suits the dark panels
|
||||
such desktops overwhelmingly ship. -->
|
||||
<style id="current-color-scheme" type="text/css">
|
||||
.ColorScheme-Text { color: #ffffff; }
|
||||
</style>
|
||||
<g class="ColorScheme-Text" fill="currentColor" transform="translate(8.446 25.000) scale(0.02514 -0.02514)">
|
||||
<path d="M58.667,0L58.667,112.667L376.833,591.167L71,591.167L71,716L534,716L534,603.333L216.333,124.833L541.333,124.833L541.333,0L58.667,0Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 434 B After Width: | Height: | Size: 775 B |
@@ -2,6 +2,8 @@
|
||||
<qresource prefix="/">
|
||||
<file alias="zeal.svg">../../assets/freedesktop/sc-apps-zeal.svg</file>
|
||||
<file alias="zeal-tray.svg">../../assets/freedesktop/sc-apps-zeal-tray.svg</file>
|
||||
<file alias="zeal-tray-light.svg">../../assets/freedesktop/sc-apps-zeal-tray-light.svg</file>
|
||||
<file alias="zeal-tray-dark.svg">../../assets/freedesktop/sc-apps-zeal-tray-dark.svg</file>
|
||||
</qresource>
|
||||
<qresource prefix="/browser">
|
||||
<file alias="new-tab.html">resources/browser/new-tab.html</file>
|
||||
|
||||
@@ -47,6 +47,7 @@ Settings::Settings(QObject *parent)
|
||||
{
|
||||
qRegisterMetaType<ContentAppearance>("ContentAppearance");
|
||||
qRegisterMetaType<ExternalLinkPolicy>("ExternalLinkPolicy");
|
||||
qRegisterMetaType<TrayIconStyle>("TrayIconStyle");
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
// When the OS color scheme changes, reapply the color scheme.
|
||||
@@ -140,6 +141,8 @@ void Settings::load()
|
||||
showSystrayIcon = settings->value(QStringLiteral("show_systray_icon"), true).toBool();
|
||||
minimizeToSystray = settings->value(QStringLiteral("minimize_to_systray"), false).toBool();
|
||||
hideOnClose = settings->value(QStringLiteral("hide_on_close"), false).toBool();
|
||||
trayIconStyle = settings->value(QStringLiteral("systray_icon_style"), QVariant::fromValue(TrayIconStyle::Automatic))
|
||||
.value<TrayIconStyle>();
|
||||
|
||||
settings->beginGroup(GroupUI);
|
||||
hideMenuBar = settings->value(QStringLiteral("hide_menu_bar"), false).toBool();
|
||||
@@ -295,6 +298,7 @@ void Settings::save()
|
||||
settings->setValue(QStringLiteral("show_systray_icon"), showSystrayIcon);
|
||||
settings->setValue(QStringLiteral("minimize_to_systray"), minimizeToSystray);
|
||||
settings->setValue(QStringLiteral("hide_on_close"), hideOnClose);
|
||||
settings->setValue(QStringLiteral("systray_icon_style"), QVariant::fromValue(trayIconStyle));
|
||||
|
||||
settings->beginGroup(GroupUI);
|
||||
settings->setValue(QStringLiteral("hide_menu_bar"), hideMenuBar);
|
||||
@@ -450,3 +454,17 @@ QDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::ExternalLinkPolic
|
||||
policy = static_cast<Zeal::Core::Settings::ExternalLinkPolicy>(value);
|
||||
return in;
|
||||
}
|
||||
|
||||
QDataStream &operator<<(QDataStream &out, Zeal::Core::Settings::TrayIconStyle style)
|
||||
{
|
||||
out << static_cast<std::underlying_type_t<Zeal::Core::Settings::TrayIconStyle>>(style);
|
||||
return out;
|
||||
}
|
||||
|
||||
QDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::TrayIconStyle &style)
|
||||
{
|
||||
std::underlying_type_t<Zeal::Core::Settings::TrayIconStyle> value = 0;
|
||||
in >> value;
|
||||
style = static_cast<Zeal::Core::Settings::TrayIconStyle>(value);
|
||||
return in;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,15 @@ public:
|
||||
bool minimizeToSystray;
|
||||
bool hideOnClose;
|
||||
|
||||
enum class TrayIconStyle : unsigned int {
|
||||
Automatic = 0,
|
||||
Colorful,
|
||||
MonochromeLight,
|
||||
MonochromeDark
|
||||
};
|
||||
Q_ENUM(TrayIconStyle)
|
||||
TrayIconStyle trayIconStyle = TrayIconStyle::Automatic;
|
||||
|
||||
// Global Shortcuts
|
||||
QKeySequence showShortcut;
|
||||
// TODO: QKeySequence searchSelectedTextShortcut;
|
||||
@@ -148,7 +157,11 @@ QDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::ContentAppearance
|
||||
QDataStream &operator<<(QDataStream &out, Zeal::Core::Settings::ExternalLinkPolicy policy);
|
||||
QDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::ExternalLinkPolicy &policy);
|
||||
|
||||
QDataStream &operator<<(QDataStream &out, Zeal::Core::Settings::TrayIconStyle style);
|
||||
QDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::TrayIconStyle &style);
|
||||
|
||||
Q_DECLARE_METATYPE(Zeal::Core::Settings::ContentAppearance)
|
||||
Q_DECLARE_METATYPE(Zeal::Core::Settings::ExternalLinkPolicy)
|
||||
Q_DECLARE_METATYPE(Zeal::Core::Settings::TrayIconStyle)
|
||||
|
||||
#endif // ZEAL_CORE_SETTINGS_H
|
||||
|
||||
+117
-67
@@ -30,6 +30,7 @@
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QPushButton>
|
||||
#include <QStringList>
|
||||
#include <QTemporaryFile>
|
||||
#include <QUrl>
|
||||
|
||||
@@ -43,13 +44,6 @@ Q_LOGGING_CATEGORY(log, "zeal.widgetui.docsetsdialog")
|
||||
|
||||
using Qt::Literals::StringLiterals::operator""_L1;
|
||||
|
||||
enum class DownloadType {
|
||||
DashFeed,
|
||||
Docset,
|
||||
DocsetList,
|
||||
TarixIndex
|
||||
};
|
||||
|
||||
constexpr auto ApiServerUrl = "https://api.zealdocs.org/v1"_L1;
|
||||
constexpr auto RedirectServerUrl = "https://go.zealdocs.org/d/%1/%2/latest"_L1;
|
||||
// TODO: Each source plugin should have its own cache
|
||||
@@ -69,15 +63,8 @@ constexpr const char *TarixRetryProperty = "tarixRetry";
|
||||
|
||||
constexpr int MaxTarixIndexRetries = 2;
|
||||
|
||||
void setDownloadType(QNetworkReply *reply, DownloadType type)
|
||||
{
|
||||
reply->setProperty(DownloadTypeProperty, static_cast<int>(type));
|
||||
}
|
||||
|
||||
DownloadType downloadType(const QNetworkReply *reply)
|
||||
{
|
||||
return static_cast<DownloadType>(reply->property(DownloadTypeProperty).toInt());
|
||||
}
|
||||
// The download servers rate limit clients.
|
||||
constexpr int MaxConcurrentDownloads = 6;
|
||||
|
||||
// An empty name, or one with path separators, could escape the cache and storage directories.
|
||||
bool isDocsetNameSafe(const QString &docsetName)
|
||||
@@ -169,8 +156,7 @@ void DocsetsDialog::addDashFeed()
|
||||
feedUrl = QUrl::fromPercentEncoding(feedUrl.toUtf8());
|
||||
}
|
||||
|
||||
QNetworkReply *reply = download(QUrl(feedUrl));
|
||||
setDownloadType(reply, DownloadType::DashFeed);
|
||||
enqueueDownload({.url = QUrl(feedUrl), .type = DownloadType::DashFeed});
|
||||
}
|
||||
|
||||
void DocsetsDialog::updateSelectedDocsets()
|
||||
@@ -266,7 +252,7 @@ void DocsetsDialog::downloadSelectedDocsets()
|
||||
}
|
||||
|
||||
QAbstractItemModel *model = ui->availableDocsetList->model();
|
||||
model->setData(index, tr("Downloading: %p%"), DocsetListItemDelegate::FormatRole);
|
||||
model->setData(index, tr("Queued"), DocsetListItemDelegate::FormatRole);
|
||||
model->setData(index, 0, DocsetListItemDelegate::ValueRole);
|
||||
model->setData(index, true, DocsetListItemDelegate::ShowProgressRole);
|
||||
|
||||
@@ -308,17 +294,30 @@ void DocsetsDialog::downloadCompleted()
|
||||
|
||||
m_replies.removeOne(reply.data());
|
||||
|
||||
processDownload(reply.data());
|
||||
startPendingDownloads();
|
||||
}
|
||||
|
||||
void DocsetsDialog::processDownload(QNetworkReply *reply)
|
||||
{
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
if (downloadType(reply.data()) == DownloadType::TarixIndex) {
|
||||
if (downloadType(reply) == DownloadType::TarixIndex) {
|
||||
if (reply->error() != QNetworkReply::OperationCanceledError) {
|
||||
onTarixIndexFailed(reply.data());
|
||||
onTarixIndexFailed(reply);
|
||||
}
|
||||
|
||||
updateStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply->error() != QNetworkReply::OperationCanceledError) {
|
||||
const auto type = downloadType(reply);
|
||||
const QString docsetName = reply->property(DocsetNameProperty).toString();
|
||||
|
||||
// Downloads cannot be resumed, so a retry would append to whatever was received.
|
||||
if (type == DownloadType::Docset) {
|
||||
delete m_tmpFiles.take(docsetName);
|
||||
}
|
||||
|
||||
const QString msg = tr("Download failed!<br><br><b>Error:</b> %1<br><b>URL:</b> %2")
|
||||
.arg(reply->errorString().toHtmlEscaped(),
|
||||
reply->request().url().toString().toHtmlEscaped());
|
||||
@@ -328,31 +327,26 @@ void DocsetsDialog::downloadCompleted()
|
||||
QMessageBox::Retry | QMessageBox::Cancel);
|
||||
|
||||
if (ret == QMessageBox::Retry) {
|
||||
QNetworkReply *newReply = download(reply->request().url());
|
||||
|
||||
// Copy properties
|
||||
newReply->setProperty(DocsetNameProperty, reply->property(DocsetNameProperty));
|
||||
setDownloadType(newReply, downloadType(reply.data()));
|
||||
newReply->setProperty(ListItemIndexProperty, reply->property(ListItemIndexProperty));
|
||||
enqueueDownload({.url = reply->request().url(),
|
||||
.type = type,
|
||||
.docsetName = docsetName,
|
||||
.listItemIndex = reply->property(ListItemIndexProperty).toInt()});
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
QListWidgetItem *listItem = ui->availableDocsetList->item(
|
||||
reply->property(ListItemIndexProperty).toInt(&ok));
|
||||
if (ok && listItem != nullptr) {
|
||||
QListWidgetItem *listItem = ui->availableDocsetList->item(reply->property(ListItemIndexProperty).toInt());
|
||||
if (listItem != nullptr) {
|
||||
listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto type = downloadType(reply.data());
|
||||
const auto type = downloadType(reply);
|
||||
switch (type) {
|
||||
case DownloadType::DocsetList:
|
||||
processDocsetListReply(reply.data());
|
||||
processDocsetListReply(reply);
|
||||
break;
|
||||
|
||||
case DownloadType::DashFeed: {
|
||||
@@ -369,9 +363,10 @@ void DocsetsDialog::downloadCompleted()
|
||||
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());
|
||||
setDownloadType(mdReply, DownloadType::Docset);
|
||||
enqueueDownload({.url = metadata.url(),
|
||||
.type = DownloadType::Docset,
|
||||
.docsetName = metadata.name(),
|
||||
.listItemIndex = ui->availableDocsetList->row(findDocsetListItem(metadata.name()))});
|
||||
} else {
|
||||
// Check for feed update
|
||||
if (metadata.latestVersion() != docset->version() || metadata.revision() > docset->revision()) {
|
||||
@@ -509,9 +504,6 @@ void DocsetsDialog::downloadCompleted()
|
||||
qCWarning(log, "Unknown download type %d.", static_cast<int>(type));
|
||||
break;
|
||||
}
|
||||
|
||||
// If all enqueued downloads have finished executing.
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
// creates a total download progress for multiple QNetworkReplies
|
||||
@@ -747,7 +739,7 @@ void DocsetsDialog::setupAvailableDocsetsTab()
|
||||
ui->availableDocsetList->selectionModel()->select(index, QItemSelectionModel::Deselect);
|
||||
|
||||
QAbstractItemModel *model = ui->availableDocsetList->model();
|
||||
model->setData(index, tr("Downloading: %p%"), DocsetListItemDelegate::FormatRole);
|
||||
model->setData(index, tr("Queued"), DocsetListItemDelegate::FormatRole);
|
||||
model->setData(index, 0, DocsetListItemDelegate::ValueRole);
|
||||
model->setData(index, true, DocsetListItemDelegate::ShowProgressRole);
|
||||
|
||||
@@ -804,12 +796,12 @@ void DocsetsDialog::updateAvailableDocsetsEmptyState()
|
||||
m_availableDocsetsEmptyState->setText(tr("No available docsets"));
|
||||
}
|
||||
|
||||
m_availableDocsetsEmptyState->setEmpty(!hasVisibleDocsets && m_replies.isEmpty());
|
||||
m_availableDocsetsEmptyState->setEmpty(!hasVisibleDocsets && m_replies.isEmpty() && m_pendingDownloads.isEmpty());
|
||||
}
|
||||
|
||||
void DocsetsDialog::enableControls()
|
||||
{
|
||||
if (m_isStorageReadOnly || !m_replies.isEmpty() || !m_tmpFiles.isEmpty()) {
|
||||
if (m_isStorageReadOnly || !m_replies.isEmpty() || !m_pendingDownloads.isEmpty() || !m_tmpFiles.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -875,22 +867,70 @@ bool DocsetsDialog::updatesAvailable() const
|
||||
});
|
||||
}
|
||||
|
||||
QNetworkReply *DocsetsDialog::download(const QUrl &url)
|
||||
DocsetsDialog::DownloadType DocsetsDialog::downloadType(const QNetworkReply *reply)
|
||||
{
|
||||
QNetworkReply *reply = m_application->download(url);
|
||||
return static_cast<DownloadType>(reply->property(DownloadTypeProperty).toInt());
|
||||
}
|
||||
|
||||
void DocsetsDialog::enqueueDownload(const DownloadRequest &request)
|
||||
{
|
||||
// Other work waits on metadata requests, so they are served before the queued archives.
|
||||
if (request.type == DownloadType::Docset) {
|
||||
m_pendingDownloads.append(request);
|
||||
} else {
|
||||
m_pendingDownloads.prepend(request);
|
||||
}
|
||||
|
||||
disableControls();
|
||||
startPendingDownloads();
|
||||
}
|
||||
|
||||
void DocsetsDialog::startDownload(const DownloadRequest &request)
|
||||
{
|
||||
QNetworkReply *reply = m_application->download(request.url);
|
||||
reply->setProperty(DownloadTypeProperty, static_cast<int>(request.type));
|
||||
reply->setProperty(DocsetNameProperty, request.docsetName);
|
||||
reply->setProperty(ListItemIndexProperty, request.listItemIndex);
|
||||
reply->setProperty(TarixRetryProperty, request.tarixRetry);
|
||||
|
||||
connect(reply, &QNetworkReply::downloadProgress, this, &DocsetsDialog::downloadProgress);
|
||||
connect(reply, &QNetworkReply::finished, this, &DocsetsDialog::downloadCompleted);
|
||||
m_replies.append(reply);
|
||||
|
||||
disableControls();
|
||||
updateStatus();
|
||||
QListWidgetItem *listItem = ui->availableDocsetList->item(request.listItemIndex);
|
||||
if (listItem != nullptr && listItem->data(DocsetListItemDelegate::ShowProgressRole).toBool()) {
|
||||
listItem->setData(DocsetListItemDelegate::FormatRole, tr("Downloading: %p%"));
|
||||
}
|
||||
}
|
||||
|
||||
return reply;
|
||||
void DocsetsDialog::startPendingDownloads()
|
||||
{
|
||||
while (!m_pendingDownloads.isEmpty() && m_replies.size() < MaxConcurrentDownloads) {
|
||||
startDownload(m_pendingDownloads.takeFirst());
|
||||
}
|
||||
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
void DocsetsDialog::cancelDownloads()
|
||||
{
|
||||
for (QNetworkReply *reply : std::as_const(m_replies)) {
|
||||
for (const DownloadRequest &request : std::as_const(m_pendingDownloads)) {
|
||||
QListWidgetItem *listItem = ui->availableDocsetList->item(request.listItemIndex);
|
||||
if (listItem != nullptr) {
|
||||
listItem->setData(DocsetListItemDelegate::ShowProgressRole, false);
|
||||
}
|
||||
|
||||
// The archive is already downloaded, so nothing below will release it.
|
||||
if (request.type == DownloadType::TarixIndex) {
|
||||
delete m_tmpFiles.take(request.docsetName);
|
||||
}
|
||||
}
|
||||
|
||||
m_pendingDownloads.clear();
|
||||
|
||||
// Aborting emits finished(), which removes the reply from m_replies, so iterate over a copy.
|
||||
const QList<QNetworkReply *> replies = m_replies;
|
||||
for (QNetworkReply *reply : replies) {
|
||||
// Hide progress bar
|
||||
QListWidgetItem *listItem = ui->availableDocsetList->item(reply->property(ListItemIndexProperty).toInt());
|
||||
if (listItem != nullptr) {
|
||||
@@ -913,8 +953,7 @@ void DocsetsDialog::loadUserFeedList()
|
||||
const auto docsets = m_docsetRegistry->docsets();
|
||||
for (const Registry::Docset *docset : docsets) {
|
||||
if (!docset->feedUrl().isEmpty()) {
|
||||
QNetworkReply *reply = download(QUrl(docset->feedUrl()));
|
||||
setDownloadType(reply, DownloadType::DashFeed);
|
||||
enqueueDownload({.url = QUrl(docset->feedUrl()), .type = DownloadType::DashFeed});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -924,8 +963,7 @@ void DocsetsDialog::downloadDocsetList()
|
||||
ui->availableDocsetList->clear();
|
||||
m_availableDocsets.clear();
|
||||
|
||||
QNetworkReply *reply = download(QUrl(ApiServerUrl + QLatin1String("/docsets")));
|
||||
setDownloadType(reply, DownloadType::DocsetList);
|
||||
enqueueDownload({.url = QUrl(ApiServerUrl + QLatin1String("/docsets")), .type = DownloadType::DocsetList});
|
||||
}
|
||||
|
||||
void DocsetsDialog::processDocsetListReply(QNetworkReply *reply)
|
||||
@@ -1054,6 +1092,14 @@ void DocsetsDialog::downloadDashDocset(const QModelIndex &index)
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if a download is already queued for this docset.
|
||||
const bool isQueued = std::ranges::any_of(m_pendingDownloads, [&name](const DownloadRequest &request) {
|
||||
return request.type == DownloadType::Docset && request.docsetName == name;
|
||||
});
|
||||
if (isQueued) {
|
||||
return;
|
||||
}
|
||||
|
||||
QUrl url;
|
||||
if (!m_userFeeds.contains(name)) {
|
||||
// No feed present means that this is a Kapeli docset
|
||||
@@ -1071,19 +1117,19 @@ void DocsetsDialog::downloadDashDocset(const QModelIndex &index)
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkReply *reply = download(url);
|
||||
reply->setProperty(DocsetNameProperty, name);
|
||||
setDownloadType(reply, DownloadType::Docset);
|
||||
reply->setProperty(ListItemIndexProperty, ui->availableDocsetList->row(findDocsetListItem(name)));
|
||||
enqueueDownload({.url = url,
|
||||
.type = DownloadType::Docset,
|
||||
.docsetName = name,
|
||||
.listItemIndex = ui->availableDocsetList->row(findDocsetListItem(name))});
|
||||
}
|
||||
|
||||
void DocsetsDialog::downloadTarixIndex(const QString &docsetName, const QUrl &indexUrl, int attempt)
|
||||
{
|
||||
QNetworkReply *reply = download(indexUrl);
|
||||
reply->setProperty(DocsetNameProperty, docsetName);
|
||||
reply->setProperty(TarixRetryProperty, attempt);
|
||||
reply->setProperty(ListItemIndexProperty, ui->availableDocsetList->row(findDocsetListItem(docsetName)));
|
||||
setDownloadType(reply, DownloadType::TarixIndex);
|
||||
enqueueDownload({.url = indexUrl,
|
||||
.type = DownloadType::TarixIndex,
|
||||
.docsetName = docsetName,
|
||||
.listItemIndex = ui->availableDocsetList->row(findDocsetListItem(docsetName)),
|
||||
.tarixRetry = attempt});
|
||||
}
|
||||
|
||||
void DocsetsDialog::onTarixIndexFailed(QNetworkReply *reply)
|
||||
@@ -1164,17 +1210,21 @@ bool DocsetsDialog::removeDocset(const QString &name)
|
||||
|
||||
void DocsetsDialog::updateStatus()
|
||||
{
|
||||
QString text;
|
||||
QStringList parts;
|
||||
|
||||
if (!m_replies.isEmpty()) {
|
||||
text = tr("Downloading: %n.", nullptr, static_cast<int>(m_replies.size()));
|
||||
parts << tr("Downloading: %n.", nullptr, static_cast<int>(m_replies.size()));
|
||||
}
|
||||
|
||||
if (!m_pendingDownloads.isEmpty()) {
|
||||
parts << tr("Queued: %n.", nullptr, static_cast<int>(m_pendingDownloads.size()));
|
||||
}
|
||||
|
||||
if (!m_tmpFiles.isEmpty()) {
|
||||
text += QLatin1String(" ") + tr("Installing: %n.", nullptr, static_cast<int>(m_tmpFiles.size()));
|
||||
parts << tr("Installing: %n.", nullptr, static_cast<int>(m_tmpFiles.size()));
|
||||
}
|
||||
|
||||
ui->statusLabel->setText(text);
|
||||
ui->statusLabel->setText(parts.join(QLatin1Char(' ')));
|
||||
updateAvailableDocsetsEmptyState();
|
||||
|
||||
enableControls();
|
||||
|
||||
@@ -10,13 +10,14 @@
|
||||
|
||||
#include <QDialog>
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QUrl>
|
||||
|
||||
class QDateTime;
|
||||
class QListWidgetItem;
|
||||
class QNetworkReply;
|
||||
class QTemporaryFile;
|
||||
class QUrl;
|
||||
|
||||
namespace Zeal {
|
||||
|
||||
@@ -45,6 +46,23 @@ public:
|
||||
~DocsetsDialog() override;
|
||||
|
||||
private:
|
||||
enum class DownloadType {
|
||||
DashFeed,
|
||||
Docset,
|
||||
DocsetList,
|
||||
TarixIndex
|
||||
};
|
||||
|
||||
struct DownloadRequest
|
||||
{
|
||||
QUrl url = {};
|
||||
DownloadType type = DownloadType::Docset;
|
||||
QString docsetName = {};
|
||||
// Row in the available docsets list, or -1 if there is no matching entry.
|
||||
int listItemIndex = -1;
|
||||
int tarixRetry = 0;
|
||||
};
|
||||
|
||||
void addDashFeed();
|
||||
void updateSelectedDocsets();
|
||||
void updateAllDocsets();
|
||||
@@ -54,6 +72,7 @@ private:
|
||||
void downloadSelectedDocsets();
|
||||
|
||||
void downloadCompleted();
|
||||
void processDownload(QNetworkReply *reply);
|
||||
void downloadProgress(qint64 received, qint64 total);
|
||||
|
||||
void extractionCompleted(const QString &filePath);
|
||||
@@ -71,6 +90,7 @@ private:
|
||||
bool m_isStorageReadOnly = false;
|
||||
|
||||
QList<QNetworkReply *> m_replies;
|
||||
QList<DownloadRequest> m_pendingDownloads;
|
||||
|
||||
// TODO: Create a special model
|
||||
Util::CaseInsensitiveMap<Registry::DocsetMetadata> m_availableDocsets;
|
||||
@@ -90,7 +110,9 @@ private:
|
||||
QListWidgetItem *findDocsetListItem(const QString &name) const;
|
||||
bool updatesAvailable() const;
|
||||
|
||||
QNetworkReply *download(const QUrl &url);
|
||||
void enqueueDownload(const DownloadRequest &request);
|
||||
void startDownload(const DownloadRequest &request);
|
||||
void startPendingDownloads();
|
||||
void cancelDownloads();
|
||||
|
||||
void loadUserFeedList();
|
||||
@@ -117,6 +139,8 @@ private:
|
||||
// FIXME: Come up with a better approach
|
||||
QString docsetNameForTmpFilePath(const QString &filePath) const;
|
||||
|
||||
static DownloadType downloadType(const QNetworkReply *reply);
|
||||
|
||||
static inline int percent(qint64 fraction, qint64 total);
|
||||
|
||||
static QString cacheLocation(const QString &fileName);
|
||||
|
||||
@@ -44,6 +44,23 @@ SettingsDialog::SettingsDialog(QWidget *parent)
|
||||
}
|
||||
});
|
||||
|
||||
// System tray
|
||||
using TrayIconStyle = Core::Settings::TrayIconStyle;
|
||||
ui->trayIconStyleComboBox->addItem(tr("Automatic"), QVariant::fromValue(TrayIconStyle::Automatic));
|
||||
ui->trayIconStyleComboBox->addItem(tr("Colorful"), QVariant::fromValue(TrayIconStyle::Colorful));
|
||||
ui->trayIconStyleComboBox->addItem(tr("Monochrome Light"), QVariant::fromValue(TrayIconStyle::MonochromeLight));
|
||||
ui->trayIconStyleComboBox->addItem(tr("Monochrome Dark"), QVariant::fromValue(TrayIconStyle::MonochromeDark));
|
||||
ui->trayIconStyleComboBox->setToolTip(
|
||||
tr("Automatic lets the desktop tint a monochrome icon to match the panel. Choose a fixed style if the icon is "
|
||||
"hard to see."));
|
||||
|
||||
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
||||
// The platform dictates the icon: a template image on macOS, the full-color
|
||||
// window icon on Windows.
|
||||
ui->trayIconStyleLabel->hide();
|
||||
ui->trayIconStyleComboBox->hide();
|
||||
#endif
|
||||
|
||||
// Fonts
|
||||
ui->defaultFontComboBox->addItem(tr("Serif"), QStringLiteral("serif"));
|
||||
ui->defaultFontComboBox->addItem(tr("Sans-serif"), QStringLiteral("sans-serif"));
|
||||
@@ -166,6 +183,9 @@ void SettingsDialog::loadSettings()
|
||||
ui->checkForUpdateCheckBox->setChecked(settings->checkForUpdate);
|
||||
|
||||
ui->systrayGroupBox->setChecked(settings->showSystrayIcon);
|
||||
// Fall back to the first entry if the stored style is not a known value.
|
||||
ui->trayIconStyleComboBox->setCurrentIndex(
|
||||
qMax(0, ui->trayIconStyleComboBox->findData(QVariant::fromValue(settings->trayIconStyle))));
|
||||
ui->minimizeToSystrayCheckBox->setChecked(settings->minimizeToSystray);
|
||||
ui->hideToSystrayCheckBox->setChecked(settings->hideOnClose);
|
||||
|
||||
@@ -262,6 +282,7 @@ void SettingsDialog::saveSettings()
|
||||
settings->checkForUpdate = ui->checkForUpdateCheckBox->isChecked();
|
||||
|
||||
settings->showSystrayIcon = ui->systrayGroupBox->isChecked();
|
||||
settings->trayIconStyle = ui->trayIconStyleComboBox->currentData().value<Core::Settings::TrayIconStyle>();
|
||||
settings->minimizeToSystray = ui->minimizeToSystrayCheckBox->isChecked();
|
||||
settings->hideOnClose = ui->hideToSystrayCheckBox->isChecked();
|
||||
|
||||
|
||||
@@ -62,6 +62,23 @@
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout_5">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="trayIconStyleLabel">
|
||||
<property name="text">
|
||||
<string>Icon style:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="trayIconStyleComboBox"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="minimizeToSystrayCheckBox">
|
||||
<property name="text">
|
||||
|
||||
@@ -20,6 +20,32 @@
|
||||
|
||||
namespace Zeal::WidgetUi {
|
||||
|
||||
#if !defined(Q_OS_MACOS) && !defined(Q_OS_WIN)
|
||||
namespace {
|
||||
QIcon themedTrayIcon(Core::Settings::TrayIconStyle style)
|
||||
{
|
||||
// Every variant is resolved by theme name so that the tray host receives an
|
||||
// icon name rather than a pixmap. That is what lets desktops recolor the
|
||||
// automatic variant, and lets icon themes override any of them.
|
||||
switch (style) {
|
||||
case Core::Settings::TrayIconStyle::Colorful:
|
||||
return QIcon::fromTheme(QStringLiteral("zeal"), QIcon(QStringLiteral(":/zeal.svg")));
|
||||
case Core::Settings::TrayIconStyle::MonochromeLight:
|
||||
return QIcon::fromTheme(QStringLiteral("zeal-tray-light"), QIcon(QStringLiteral(":/zeal-tray-light.svg")));
|
||||
case Core::Settings::TrayIconStyle::MonochromeDark:
|
||||
return QIcon::fromTheme(QStringLiteral("zeal-tray-dark"), QIcon(QStringLiteral(":/zeal-tray-dark.svg")));
|
||||
case Core::Settings::TrayIconStyle::Automatic:
|
||||
break;
|
||||
}
|
||||
|
||||
// Carries a color scheme stylesheet, so desktops that recolor tray icons tint
|
||||
// it to match the panel. Those that do not fall back to its white default,
|
||||
// hence the explicit choices above (#1950).
|
||||
return QIcon::fromTheme(QStringLiteral("zeal-tray"), QIcon(QStringLiteral(":/zeal-tray.svg")));
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
WindowManager::WindowManager(Core::Application *application, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_application(application)
|
||||
@@ -117,10 +143,17 @@ MainWindow *WindowManager::activeWindow() const
|
||||
|
||||
void WindowManager::applySettings()
|
||||
{
|
||||
if (m_settings->isTrayActive()) {
|
||||
if (!m_settings->isTrayActive()) {
|
||||
removeTrayIcon();
|
||||
return;
|
||||
}
|
||||
|
||||
// createTrayIcon() applies the icon itself, so only an already visible icon
|
||||
// needs a refresh, e.g. after the style setting changed.
|
||||
if (m_trayIcon == nullptr) {
|
||||
createTrayIcon();
|
||||
} else {
|
||||
removeTrayIcon();
|
||||
updateTrayIcon();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,18 +164,7 @@ void WindowManager::createTrayIcon()
|
||||
}
|
||||
|
||||
m_trayIcon = new QSystemTrayIcon(this);
|
||||
#ifdef Q_OS_MACOS
|
||||
// macOS menu-bar items render as template images: monochrome silhouettes
|
||||
// tinted by the system to match light/dark mode and the active accent.
|
||||
QIcon trayIcon(QStringLiteral(":/zeal-tray.svg"));
|
||||
trayIcon.setIsMask(true);
|
||||
#elif defined(Q_OS_WIN)
|
||||
// Windows tray takes the icon as-is — reuse the full-color window icon.
|
||||
const QIcon trayIcon = qApp->windowIcon();
|
||||
#else
|
||||
const QIcon trayIcon = QIcon::fromTheme(QStringLiteral("zeal-tray"), QIcon(QStringLiteral(":/zeal-tray.svg")));
|
||||
#endif
|
||||
m_trayIcon->setIcon(trayIcon);
|
||||
updateTrayIcon();
|
||||
m_trayIcon->setToolTip(QStringLiteral("Zeal"));
|
||||
|
||||
connect(m_trayIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) {
|
||||
@@ -183,6 +205,26 @@ void WindowManager::createTrayIcon()
|
||||
m_trayIcon->show();
|
||||
}
|
||||
|
||||
void WindowManager::updateTrayIcon()
|
||||
{
|
||||
if (m_trayIcon == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
// macOS menu-bar items render as template images: monochrome silhouettes
|
||||
// tinted by the system to match light/dark mode and the active accent.
|
||||
QIcon trayIcon(QStringLiteral(":/zeal-tray.svg"));
|
||||
trayIcon.setIsMask(true);
|
||||
#elif defined(Q_OS_WIN)
|
||||
// Windows tray takes the icon as-is — reuse the full-color window icon.
|
||||
const QIcon trayIcon = qApp->windowIcon();
|
||||
#else
|
||||
const QIcon trayIcon = themedTrayIcon(m_settings->trayIconStyle);
|
||||
#endif
|
||||
m_trayIcon->setIcon(trayIcon);
|
||||
}
|
||||
|
||||
void WindowManager::removeTrayIcon()
|
||||
{
|
||||
if (m_trayIcon == nullptr) {
|
||||
|
||||
@@ -41,6 +41,7 @@ private:
|
||||
|
||||
void applySettings();
|
||||
void createTrayIcon();
|
||||
void updateTrayIcon();
|
||||
void removeTrayIcon();
|
||||
|
||||
Core::Application *m_application = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user