diff --git a/src/watchfacereloader.cpp b/src/watchfacereloader.cpp index 0d761464..e4748071 100644 --- a/src/watchfacereloader.cpp +++ b/src/watchfacereloader.cpp @@ -8,33 +8,144 @@ #include #include +#include +#include +#include #include +namespace { +constexpr int kSettleIntervalMs = 800; +constexpr int kMaxSettleTries = 6; +constexpr int kMaxLoadAttempts = 8; +} + WatchfaceReloader::WatchfaceReloader(QQmlEngine *engine, QObject *parent) : QObject(parent) , m_engine(engine) { - // Same location the settings watchface store installs into. - m_dir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + // Same locations the settings watchface store installs into. + m_watchfaceDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/asteroid-launcher/watchfaces"); - QDir().mkpath(m_dir); - m_watcher.addPath(m_dir); + m_fontsDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + + QStringLiteral("/.fonts"); + + QDir().mkpath(m_watchfaceDir); + QDir().mkpath(m_fontsDir); + m_watcher.addPath(m_watchfaceDir); + m_watcher.addPath(m_fontsDir); + + // Register everything already in the user font folder. These fonts exist + // nowhere else, and fontconfig only knows them after an fc-cache run that + // nothing guarantees has happened — on a freshly flashed device it never + // has — so store faces would render with fallback fonts after any session + // restart. Registering an already-known family is harmless. + scanFonts(); + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &WatchfaceReloader::onDirectoryChanged); + + // A font pushed by an external writer (adb, scp) may still be mid-write + // when the folder-changed signal fires, so addApplicationFont reads a + // truncated file. Re-scan a short moment later, re-arming while a font on + // disk remains unloaded, so it is registered and applied live once its + // write finishes. (Store installs write to the final path in one shot, so + // in practice this chain serves the external writers.) + m_settleTimer.setSingleShot(true); + m_settleTimer.setInterval(kSettleIntervalMs); + connect(&m_settleTimer, &QTimer::timeout, this, [this]() { + if (scanFonts() > 0) + clearCacheAndReload(); + if (m_pendingFonts > 0 && ++m_settleTries < kMaxSettleTries) + m_settleTimer.start(); + else + m_settleTries = 0; + }); } -void WatchfaceReloader::onDirectoryChanged(const QString &) +int WatchfaceReloader::scanFonts() +{ + static const QStringList kFontFilters{ + QStringLiteral("*.ttf"), QStringLiteral("*.otf"), QStringLiteral("*.ttc")}; + + int added = 0; + m_pendingFonts = 0; + const QFileInfoList files = + QDir(m_fontsDir).entryInfoList(kFontFilters, QDir::Files, QDir::NoSort); + for (const QFileInfo &fi : files) { + const QString path = fi.absoluteFilePath(); + const auto it = m_fonts.constFind(path); + if (it != m_fonts.constEnd()) { + if (it->size == fi.size() && it->mtime == fi.lastModified()) + continue; + // Replaced in place (store re-download, script push): the database + // still serves the old file's glyphs under this family. Drop and + // re-register below. + QFontDatabase::removeApplicationFont(it->id); + m_fonts.remove(path); + m_attempts.remove(path); + } + if (m_attempts.value(path, 0) >= kMaxLoadAttempts) + continue; + const int id = QFontDatabase::addApplicationFont(path); + if (id != -1) { + m_fonts.insert(path, { id, fi.size(), fi.lastModified() }); + m_attempts.remove(path); + ++added; + } else { + // Truncated mid-write or genuinely broken; the settle chain + // retries until the attempt budget rules it broken. + ++m_attempts[path]; + ++m_pendingFonts; + } + } + + // Files that vanished release their database entry, so an uninstalled + // face's font does not linger for the life of the process. + for (auto rec = m_fonts.begin(); rec != m_fonts.end();) { + if (!QFile::exists(rec.key())) { + QFontDatabase::removeApplicationFont(rec.value().id); + rec = m_fonts.erase(rec); + } else { + ++rec; + } + } + return added; +} + +void WatchfaceReloader::clearCacheAndReload() { if (!m_engine) return; - // Clearing the component cache forces the next QML load to re-read the // directory, so a just-installed watchface becomes loadable immediately. m_engine->clearComponentCache(); emit reloadNeeded(); +} + +void WatchfaceReloader::onDirectoryChanged(const QString &path) +{ + // Route by folder: dropping the component cache tears down and reloads the + // active watchface, so unrelated font-folder churn must not trigger it, + // and a watchface install must not pay a font scan. + if (path == m_fontsDir) { + if (scanFonts() > 0) + clearCacheAndReload(); + if (m_pendingFonts > 0) { + m_settleTries = 0; + m_settleTimer.start(); + } + } else { + clearCacheAndReload(); + } // QFileSystemWatcher drops a path that momentarily disappears (some tools - // replace directories atomically); re-add it so we keep watching. - if (!m_watcher.directories().contains(m_dir) && QDir(m_dir).exists()) - m_watcher.addPath(m_dir); + // replace directories atomically); re-create and re-add so we keep + // watching. + const QStringList watched = m_watcher.directories(); + for (const QString &dir : {m_watchfaceDir, m_fontsDir}) { + if (!watched.contains(dir)) { + QDir().mkpath(dir); + m_watcher.addPath(dir); + } + } } diff --git a/src/watchfacereloader.h b/src/watchfacereloader.h index 225c14bb..d4009693 100644 --- a/src/watchfacereloader.h +++ b/src/watchfacereloader.h @@ -7,22 +7,39 @@ #ifndef WATCHFACERELOADER_H #define WATCHFACERELOADER_H -#include +#include #include +#include +#include #include +#include class QQmlEngine; /*! - * \brief Drops the QML component cache when a watchface is added at runtime. + * \brief Reloads watchfaces and their bundled fonts added at runtime. + * + * Two Qt caches keep a just-installed watchface from showing without a restart: + * + * 1. QQmlTypeLoader reads a directory's listing the first time it resolves a + * file there and then reuses that cached listing for the life of the process, + * with no invalidation. A watchface downloaded into the user watchface folder + * after that first read is invisible to the loader ("File name case mismatch") + * until the component cache is dropped. + * 2. QFontDatabase is populated from fontconfig once at startup. A font a + * watchface bundles into the user font folder afterwards is unknown to the + * running process, so the watchface renders with a fallback family until the + * session is restarted. * - * Qt's QQmlTypeLoader reads a directory's listing the first time it resolves a - * file there and then reuses that cached listing for the life of the process, - * with no invalidation. A watchface downloaded into the user watchface folder - * after that first read is therefore invisible to the loader ("File name case - * mismatch") until the cache is dropped. This watches the folder and clears the - * component cache when it changes so newly installed watchfaces load live, - * without restarting the launcher. + * This watches both folders and routes each change by its path: a watchface + * change drops the QML component cache; a font change registers new or replaced + * fonts straight into the running font database with + * QFontDatabase::addApplicationFont() (no fc-cache, no restart) and reloads only + * when a font actually arrived. The user fonts are also registered at startup: + * they exist only in the user folder, and fontconfig knows them only after an + * fc-cache run nothing guarantees has happened (a fresh install never ran one), + * so relying on fontconfig would leave every store face on fallback fonts after + * a session restart. */ class WatchfaceReloader : public QObject { @@ -39,9 +56,33 @@ private slots: void onDirectoryChanged(const QString &path); private: + //! One pass over the user font folder: register fonts not yet in the + //! running database, re-register files replaced in place (same path, new + //! size or mtime), and drop database entries for files that vanished. + //! Returns how many fonts newly registered; sets m_pendingFonts to the + //! number present on disk but not (yet) loadable. + int scanFonts(); + + void clearCacheAndReload(); + + struct FontRecord { + int id = -1; + qint64 size = 0; + QDateTime mtime; + }; + QQmlEngine *m_engine; QFileSystemWatcher m_watcher; - QString m_dir; + QTimer m_settleTimer; + int m_settleTries = 0; + int m_pendingFonts = 0; + QString m_watchfaceDir; + QString m_fontsDir; + QHash m_fonts; + //! Load attempts per path; a file that keeps failing (corrupt or an + //! unsupported format) stops being retried once its budget is spent, so it + //! cannot re-arm the settle chain on every later folder change. + QHash m_attempts; }; #endif // WATCHFACERELOADER_H