diff --git a/CMakeLists.txt b/CMakeLists.txt index 059ed2645..959724230 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,16 +65,19 @@ setup_third_party( GIT_REPOSITORY https://github.com/Awesome-Embedded-Learning-Studio/QuarkWidgets.git GIT_TAG v0.1.0) +# Standalone desktop apps. Each ships a standalone executable launched via +# QProcess; the calculator parser lib (cfdesktop_calculator_parser) is also +# reused in-process by the desktop builtin_apps target, so apps must configure +# before desktop. +log_module_start("apps") +add_subdirectory(apps) +log_module_end("apps") + log_module_start("desktop") add_subdirectory(desktop) # Log base module end log_module_end("desktop") -# Standalone desktop apps (launched via QProcess, not linked into the shell). -log_module_start("apps") -add_subdirectory(apps) -log_module_end("apps") - log_module_start("example") add_subdirectory("example") log_module_end("example") diff --git a/apps/calculator/app.json b/apps/calculator/app.json index bf5bab80c..c3ca60fbc 100644 --- a/apps/calculator/app.json +++ b/apps/calculator/app.json @@ -1,5 +1,6 @@ { "app_id": "calculator", "display_name": "Calculator", - "exec": "calculator" + "exec": "calculator", + "launch_kind": "auto" } diff --git a/base/include/system/hardware_tier/hardware_tier_data.h b/base/include/system/hardware_tier/hardware_tier_data.h index 706343016..0e5100ca2 100644 --- a/base/include/system/hardware_tier/hardware_tier_data.h +++ b/base/include/system/hardware_tier/hardware_tier_data.h @@ -188,6 +188,7 @@ struct HardwareTierCapabilities { bool use_hardware_decode = false; ///< Use hardware video decoding. bool use_eglfs = false; ///< Use EGLFS platform plugin. bool use_linuxfb = false; ///< Use linuxfb platform plugin. + bool prefer_inprocess_apps = false; ///< Prefer in-process apps (saves RAM on Low tier). }; // ───────────────────────────────────────────────────────── diff --git a/base/system/hardware_tier/default/default_policy.cpp b/base/system/hardware_tier/default/default_policy.cpp index 2439be2a5..7eab48426 100644 --- a/base/system/hardware_tier/default/default_policy.cpp +++ b/base/system/hardware_tier/default/default_policy.cpp @@ -42,12 +42,14 @@ class DefaultPolicy : public IHardwarePolicy { caps.use_software_render = true; caps.use_hardware_decode = false; caps.use_linuxfb = true; + caps.prefer_inprocess_apps = true; break; case HardwareTierLevel::Unknown: default: caps.use_software_render = true; caps.use_linuxfb = true; + caps.prefer_inprocess_apps = true; break; } diff --git a/desktop/ui/CFDesktopEntity.cpp b/desktop/ui/CFDesktopEntity.cpp index 1d44e723a..6806d9bfa 100644 --- a/desktop/ui/CFDesktopEntity.cpp +++ b/desktop/ui/CFDesktopEntity.cpp @@ -11,6 +11,8 @@ #include "components/PanelManager.h" #include "components/WindowManager.h" #include "components/builtin_apps/about_panel.h" +#include "components/builtin_apps/builtin_panel_registry.h" +#include "components/builtin_apps/calculator_builtin_panel.h" #include "components/launcher/app_discoverer.h" #include "components/launcher/app_launch_service.h" #include "components/launcher/app_launcher.h" @@ -21,6 +23,7 @@ #include "platform/display_backend_helper.h" #include "platform/shell_layer_helper.h" #include "qt_format.h" +#include "system/hardware_tier/hardware_tier.h" #include #include #include @@ -33,38 +36,93 @@ namespace cf::desktop { namespace { -/// Loads the app list with a fallback chain: auto-discovered apps -/// (/../apps//app.json) first, then /../apps.json manual list, -/// then defaultApps(). A board can drop app manifests or apps.json to +/// Loads the app list by merging builtin in-process panels with auto- +/// discovered standalone manifests, resolving Auto launch_kind against the +/// hardware tier. Falls back to /../apps.json then defaultApps() when +/// nothing is discovered. A board can drop app manifests or apps.json to /// customize the launcher/taskbar without a recompile. -QList loadAppsConfig() { - // Prefer auto-discovered apps (/../apps//app.json manifests). - auto discovered = desktop_component::AppDiscoverer::discover(); - if (!discovered.isEmpty()) { - return discovered; +QList loadAppsConfig(bool prefer_inprocess) { + // Upsert by app_id: builtin panels first (in-process implementations), + // then discovered manifests (icon/exec/display_name metadata) which may + // override a builtin entry when a standalone app exists for the same id. + QList result; + const auto upsert = [&](desktop_component::AppEntry entry) { + for (auto& existing : result) { + if (existing.app_id == entry.app_id) { + existing = entry; + return; + } + } + result.append(entry); + }; + + // 1. Builtin in-process panels (always surfaced). + for (auto* panel : desktop_component::BuiltinPanelRegistry::instance().all()) { + desktop_component::AppEntry e; + e.app_id = panel->appId(); + e.display_name = panel->displayName(); + e.launch_kind = desktop_component::LaunchKind::BuiltinPanel; + upsert(e); } - // Fallback: /../apps.json manual list. Kept out of the board-written - // settings/ dir so it stays owner-writable on the NFS root. - const QString path = QCoreApplication::applicationDirPath() + QStringLiteral("/../apps.json"); - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - return desktop_component::defaultApps(); + + // 2. Auto-discovered standalone app manifests. + auto discovered = desktop_component::AppDiscoverer::discover(); + for (auto& entry : discovered) { + if (entry.launch_kind == desktop_component::LaunchKind::Auto) { + // Resolve Auto: prefer in-process only when a builtin implementation + // exists; otherwise detach (never silently fall back to builtin). + const bool has_builtin = + desktop_component::BuiltinPanelRegistry::instance().contains(entry.app_id); + if (prefer_inprocess && has_builtin) { + entry.launch_kind = desktop_component::LaunchKind::BuiltinPanel; + entry.exec_command.clear(); + } else { + if (prefer_inprocess && !has_builtin) { + cf::log::infoftag("CFDesktopEntity", + "App '{}' auto->detached (no builtin impl)", + entry.app_id.toStdString()); + } + entry.launch_kind = desktop_component::LaunchKind::DetachedProcess; + } + } + upsert(entry); } - const QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); - const auto arr = doc.object().value(QStringLiteral("apps")).toArray(); - QList apps; - for (const auto& value : arr) { - const auto o = value.toObject(); - desktop_component::AppEntry entry; - entry.app_id = o.value(QStringLiteral("app_id")).toString(); - entry.display_name = o.value(QStringLiteral("display_name")).toString(); - entry.icon_path = o.value(QStringLiteral("icon_path")).toString(); - entry.exec_command = o.value(QStringLiteral("exec_command")).toString(); - if (!entry.app_id.isEmpty() && !entry.exec_command.isEmpty()) { - apps.append(entry); + + // 3. Legacy fallback when nothing is discovered: /../apps.json, + // then defaultApps() placeholder entries. + if (discovered.isEmpty()) { + const QString path = + QCoreApplication::applicationDirPath() + QStringLiteral("/../apps.json"); + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + for (const auto& d : desktop_component::defaultApps()) { + upsert(d); + } + return result; + } + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + const auto arr = doc.object().value(QStringLiteral("apps")).toArray(); + bool added_any = false; + for (const auto& value : arr) { + const auto o = value.toObject(); + desktop_component::AppEntry entry; + entry.app_id = o.value(QStringLiteral("app_id")).toString(); + entry.display_name = o.value(QStringLiteral("display_name")).toString(); + entry.icon_path = o.value(QStringLiteral("icon_path")).toString(); + entry.exec_command = o.value(QStringLiteral("exec_command")).toString(); + if (!entry.app_id.isEmpty() && !entry.exec_command.isEmpty()) { + upsert(entry); + added_any = true; + } + } + if (!added_any) { + for (const auto& d : desktop_component::defaultApps()) { + upsert(d); + } } } - return apps.isEmpty() ? desktop_component::defaultApps() : apps; + + return result; } } // namespace @@ -261,50 +319,62 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) { } } + // ── Builtin in-process panels: register before loadAppsConfig so the + // merged app list can surface them and resolve Auto launch_kind. ── + auto& builtin_registry = cf::desktop::desktop_component::BuiltinPanelRegistry::instance(); + auto* about_panel = new cf::desktop::desktop_component::AboutPanel(desktop_entity_); + builtin_registry.registerPanel(about_panel); + auto* calc_builtin = + new cf::desktop::desktop_component::CalculatorBuiltinPanel(desktop_entity_); + builtin_registry.registerPanel(calc_builtin); + + // Hardware tier decides whether Auto apps run in-process (Low tier) or + // detached (Mid/High). setDeviceConfigOverride (env/tests) takes precedence. + bool prefer_inprocess = false; + if (auto res = cf::assessHardware(); res.has_value()) { + if (auto caps = cf::getHardwareTierCapabilities(); caps.has_value()) { + prefer_inprocess = caps->prefer_inprocess_apps; + } + } + // ── Taskbar: bottom-edge panel (centered app icons) ── - // apps is captured by the click handler to resolve app_id -> exec_command. - // Loaded from settings/apps.json (per-board app list) if present. - const QList apps = loadAppsConfig(); + // apps is captured by the click handler to resolve app_id -> entry. + const QList apps = loadAppsConfig(prefer_inprocess); auto* taskbar = new cf::desktop::desktop_component::CenteredTaskbar(desktop_entity_); taskbar->setApps(apps); taskbar->setBackdropSource(shell); panel_mgr->registerPanel(taskbar->GetWeak()); - // Shared launch path: resolve app_id -> exec, launch, capture PID. Used by - // both the taskbar tile click and the launcher popup so the running-state - // indicator lights for either entry point. - // Builtin in-process apps live as hidden child widgets of the desktop and - // are shown when their "builtin:*" exec_command is launched (no QProcess, - // so no framebuffer fight with the desktop on linuxfb). - auto* about_panel = new cf::desktop::desktop_component::AboutPanel(desktop_entity_); - - std::function launch_app = [apps, app_pid, about_panel, + // Shared launch path: resolve app_id -> entry, dispatch by launch_kind. + // BuiltinPanel entries render in-process (registry lookup); DetachedProcess + // entries spawn a QProcess. Used by both taskbar click and launcher popup + // so the running-state indicator lights for either entry point. + std::function launch_app = [apps, app_pid, panel_mgr](const QString& app_id) { - QString exec; + const cf::desktop::desktop_component::AppEntry* found = nullptr; for (const auto& app : apps) { if (app.app_id == app_id) { - exec = app.exec_command; + found = &app; break; } } - if (exec.isEmpty()) { - cf::log::warningftag("CFDesktopEntity", "No exec for app_id '{}'", + if (found == nullptr) { + cf::log::warningftag("CFDesktopEntity", "No entry for app_id '{}'", app_id.toStdString()); return; } - // Builtin apps render in-process. External apps are launched - // detached (Stage 2 will add hide-desktop + managed-QProcess for - // GUI apps that need the full framebuffer). - if (exec.startsWith(QStringLiteral("builtin:"))) { - const auto id = exec.mid(QStringLiteral("builtin:").size()); - if (id == QStringLiteral("about") && about_panel != nullptr) { - about_panel->popup(panel_mgr->availableGeometry()); + if (found->launch_kind == cf::desktop::desktop_component::LaunchKind::BuiltinPanel) { + auto* panel = + cf::desktop::desktop_component::BuiltinPanelRegistry::instance().find(app_id); + if (panel != nullptr) { + panel->popup(panel_mgr->availableGeometry()); } else { - cf::log::warningftag("CFDesktopEntity", "Unknown builtin app '{}'", - id.toStdString()); + cf::log::warningftag("CFDesktopEntity", "No builtin panel for '{}'", + app_id.toStdString()); } return; } - const auto launched = cf::desktop::desktop_component::AppLaunchService::launch(exec); + const auto launched = + cf::desktop::desktop_component::AppLaunchService::launch(found->exec_command); if (launched.has_value()) { (*app_pid)[app_id] = *launched; } diff --git a/desktop/ui/components/app_entry.h b/desktop/ui/components/app_entry.h index 2d03c7554..4da6075cd 100644 --- a/desktop/ui/components/app_entry.h +++ b/desktop/ui/components/app_entry.h @@ -3,13 +3,15 @@ * @brief Application entry data model for the taskbar. * * AppEntry describes a single launchable application shown as an icon in the - * CenteredTaskbar. defaultApps() returns a small set of placeholder entries - * (file manager, terminal, settings, browser) used until a real app registry - * exists. + * CenteredTaskbar. Each entry carries a LaunchKind that decides whether it is + * brought on screen as a detached process (exec_command) or as an in-process + * builtin panel (looked up by app_id). defaultApps() returns a small set of + * placeholder entries (file manager, terminal, settings, browser) used until a + * real app registry exists. * * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) * @date 2026-06-16 - * @version 0.1 + * @version 0.2 * @since 0.19 * @ingroup components */ @@ -21,16 +23,28 @@ namespace cf::desktop::desktop_component { +/** + * @brief How an AppEntry is brought on screen. + * + * @ingroup components + */ +enum class LaunchKind { + Auto, ///< Hardware tier decides at load time (default for manifests). + DetachedProcess, ///< Launched as a separate process via QProcess (exec_command). + BuiltinPanel, ///< Shown in-process as a builtin panel (looked up by app_id). +}; + /** * @brief Describes one launchable application for the taskbar. * * @ingroup components */ struct AppEntry { - QString app_id; ///< Stable unique identifier (e.g. "terminal"). - QString display_name; ///< Human-readable label (initial is drawn on the tile). - QString icon_path; ///< Icon resource path; empty leaves the tile blank (no fallback). - QString exec_command; ///< Launch command consumed later by QProcess. + QString app_id; ///< Stable unique identifier (e.g. "terminal"). + QString display_name; ///< Human-readable label (initial is drawn on the tile). + QString icon_path; ///< Icon resource path; empty leaves the tile blank (no fallback). + QString exec_command; ///< Program path used only when launch_kind == DetachedProcess. + LaunchKind launch_kind{LaunchKind::DetachedProcess}; ///< How this app is loaded. bool is_running{false}; ///< Whether the app currently has a live window. }; @@ -44,21 +58,31 @@ struct AppEntry { * @throws None * @note None * @warning None - * @since 0.19 + * @since 0.19 * @ingroup components */ inline QList defaultApps() { return { - {QStringLiteral("files"), QStringLiteral("Files"), - QStringLiteral(":/cfdesktop/taskbar/files.png"), QStringLiteral("xdg-open ."), false}, - {QStringLiteral("terminal"), QStringLiteral("Terminal"), - QStringLiteral(":/cfdesktop/taskbar/terminal.png"), QStringLiteral("xterm"), false}, - {QStringLiteral("settings"), QStringLiteral("Settings"), - QStringLiteral(":/cfdesktop/taskbar/settings.png"), QStringLiteral("cfdesktop-settings"), - false}, - {QStringLiteral("browser"), QStringLiteral("Browser"), - QStringLiteral(":/cfdesktop/taskbar/browser.png"), - QStringLiteral("xdg-open https://example.com"), false}, + {.app_id = QStringLiteral("files"), + .display_name = QStringLiteral("Files"), + .icon_path = QStringLiteral(":/cfdesktop/taskbar/files.png"), + .exec_command = QStringLiteral("xdg-open ."), + .is_running = false}, + {.app_id = QStringLiteral("terminal"), + .display_name = QStringLiteral("Terminal"), + .icon_path = QStringLiteral(":/cfdesktop/taskbar/terminal.png"), + .exec_command = QStringLiteral("xterm"), + .is_running = false}, + {.app_id = QStringLiteral("settings"), + .display_name = QStringLiteral("Settings"), + .icon_path = QStringLiteral(":/cfdesktop/taskbar/settings.png"), + .exec_command = QStringLiteral("cfdesktop-settings"), + .is_running = false}, + {.app_id = QStringLiteral("browser"), + .display_name = QStringLiteral("Browser"), + .icon_path = QStringLiteral(":/cfdesktop/taskbar/browser.png"), + .exec_command = QStringLiteral("xdg-open https://example.com"), + .is_running = false}, }; } diff --git a/desktop/ui/components/builtin_apps/CMakeLists.txt b/desktop/ui/components/builtin_apps/CMakeLists.txt index c66492895..af9035446 100644 --- a/desktop/ui/components/builtin_apps/CMakeLists.txt +++ b/desktop/ui/components/builtin_apps/CMakeLists.txt @@ -4,6 +4,12 @@ # fight the desktop for /dev/fb0. add_library(cfdesktop_builtin_apps STATIC about_panel.cpp + builtin_panel_registry.cpp + calculator_builtin_panel.cpp + # Architecture exception: compile the standalone Calculator's panel source + # into the builtin lib so the same panel runs in-process. The standalone + # executable (apps/calculator/) compiles its own copy independently. + ${CMAKE_SOURCE_DIR}/apps/calculator/calculator_panel.cpp ) target_include_directories(cfdesktop_builtin_apps PUBLIC @@ -11,7 +17,15 @@ target_include_directories(cfdesktop_builtin_apps PUBLIC $ ) +# calculator_panel.h lives under apps/calculator/. +target_include_directories(cfdesktop_builtin_apps PRIVATE + ${CMAKE_SOURCE_DIR}/apps/calculator +) + target_link_libraries(cfdesktop_builtin_apps PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets + cfdesktop_calculator_parser + QuarkWidgets::quarkwidgets + cflogger # Diagnostic logging for the registry ) diff --git a/desktop/ui/components/builtin_apps/about_panel.cpp b/desktop/ui/components/builtin_apps/about_panel.cpp index 424c310bf..16d99b39a 100644 --- a/desktop/ui/components/builtin_apps/about_panel.cpp +++ b/desktop/ui/components/builtin_apps/about_panel.cpp @@ -33,8 +33,7 @@ constexpr int kMaxHeight = 340; ///< Maximum card height (px). } // namespace AboutPanel::AboutPanel(QWidget* parent) - : QWidget(parent), - version_text_(QStringLiteral("CFDesktop v0.19.0")), + : QWidget(parent), version_text_(QStringLiteral("CFDesktop v0.19.0")), target_text_(QStringLiteral("Running on i.MX6ULL (Qt linuxfb)")) { setWindowFlags(Qt::FramelessWindowHint); setAttribute(Qt::WA_TranslucentBackground); @@ -56,7 +55,8 @@ void AboutPanel::popup(const QRect& available) { } const int w = std::clamp(static_cast(avail.width() * kWidthRatio), kMinWidth, kMaxWidth); - const int h = std::clamp(static_cast(avail.height() * kHeightRatio), kMinHeight, kMaxHeight); + const int h = + std::clamp(static_cast(avail.height() * kHeightRatio), kMinHeight, kMaxHeight); const int x = avail.center().x() - w / 2; const int y = avail.center().y() - h / 2; @@ -69,6 +69,14 @@ void AboutPanel::hidePanel() { hide(); } +QString AboutPanel::appId() const { + return QStringLiteral("about"); +} + +QString AboutPanel::displayName() const { + return QStringLiteral("About"); +} + void AboutPanel::paintEvent(QPaintEvent* /*event*/) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); diff --git a/desktop/ui/components/builtin_apps/about_panel.h b/desktop/ui/components/builtin_apps/about_panel.h index 5ffd3e3fb..fda6ba79d 100644 --- a/desktop/ui/components/builtin_apps/about_panel.h +++ b/desktop/ui/components/builtin_apps/about_panel.h @@ -17,6 +17,8 @@ #pragma once +#include "ibuiltin_panel.h" + #include #include @@ -35,7 +37,7 @@ namespace cf::desktop::desktop_component { * * @ingroup components */ -class AboutPanel final : public QWidget { +class AboutPanel final : public QWidget, public IBuiltinPanel { Q_OBJECT public: /** @@ -73,7 +75,7 @@ class AboutPanel final : public QWidget { * @since 0.20 * @ingroup components */ - void popup(const QRect& available); + void popup(const QRect& available) override; /** * @brief Hides the panel. @@ -84,7 +86,29 @@ class AboutPanel final : public QWidget { * @since 0.20 * @ingroup components */ - void hidePanel(); + void hidePanel() override; + + /** + * @brief Returns the stable launcher app_id. + * + * @return The id "about". + * + * @throws None + * @since 0.20 + * @ingroup components + */ + QString appId() const override; + + /** + * @brief Returns the icon label. + * + * @return The display name "About". + * + * @throws None + * @since 0.20 + * @ingroup components + */ + QString displayName() const override; protected: /** diff --git a/desktop/ui/components/builtin_apps/builtin_panel_registry.cpp b/desktop/ui/components/builtin_apps/builtin_panel_registry.cpp new file mode 100644 index 000000000..2212fa386 --- /dev/null +++ b/desktop/ui/components/builtin_apps/builtin_panel_registry.cpp @@ -0,0 +1,67 @@ +/** + * @file builtin_panel_registry.cpp + * @brief Implementation of BuiltinPanelRegistry. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-07-01 + * @version 0.1 + * @since 0.20 + * @ingroup components + */ + +#include "builtin_panel_registry.h" + +#include "cflog.h" + +namespace cf::desktop::desktop_component { + +namespace { +/// Tag for BuiltinPanelRegistry log lines. +constexpr const char* kLogTag = "BuiltinPanelRegistry"; +} // namespace + +BuiltinPanelRegistry& BuiltinPanelRegistry::instance() { + static BuiltinPanelRegistry registry; + return registry; +} + +void BuiltinPanelRegistry::registerPanel(IBuiltinPanel* panel) { + if (panel == nullptr) { + cf::log::warningftag(kLogTag, "registerPanel ignored null panel"); + return; + } + const QString id = panel->appId(); + if (panels_.contains(id)) { + cf::log::warningftag(kLogTag, "Duplicate builtin panel id '{}' overwritten", + id.toStdString()); + } + panels_.insert(id, panel); +} + +IBuiltinPanel* BuiltinPanelRegistry::find(const QString& id) const { + const auto it = panels_.constFind(id); + if (it == panels_.constEnd()) { + cf::log::warningftag(kLogTag, "No builtin panel for id '{}'", id.toStdString()); + return nullptr; + } + return it.value(); +} + +bool BuiltinPanelRegistry::contains(const QString& id) const { + return panels_.contains(id); +} + +std::vector BuiltinPanelRegistry::all() const { + std::vector result; + result.reserve(static_cast(panels_.size())); + for (IBuiltinPanel* panel : panels_) { + result.push_back(panel); + } + return result; +} + +void BuiltinPanelRegistry::clear() { + panels_.clear(); +} + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/builtin_apps/builtin_panel_registry.h b/desktop/ui/components/builtin_apps/builtin_panel_registry.h new file mode 100644 index 000000000..eae71472f --- /dev/null +++ b/desktop/ui/components/builtin_apps/builtin_panel_registry.h @@ -0,0 +1,113 @@ +/** + * @file builtin_panel_registry.h + * @brief Registry of in-process builtin application panels by app_id. + * + * BuiltinPanelRegistry maps app_id -> IBuiltinPanel instance so the launcher + * can resolve a BuiltinPanel launch_kind entry to its panel without a + * hardcoded if-chain. The registry holds non-owning pointers; panel lifetime + * is managed by the desktop (panels are Qt children of the desktop surface). + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-07-01 + * @version 0.1 + * @since 0.20 + * @ingroup components + */ + +#pragma once + +#include "ibuiltin_panel.h" + +#include +#include + +#include + +namespace cf::desktop::desktop_component { + +/** + * @brief Maps app_id to its in-process IBuiltinPanel instance. + * + * Singleton accessed via instance(). Registration happens at desktop startup + * (each builtin panel calls registerPanel this); lookup happens at launch + * time. Misses are logged as warnings (no silent fallback). + * + * @ingroup components + */ +class BuiltinPanelRegistry { + public: + /** + * @brief Accesses the process-wide registry singleton. + * + * @return Reference to the singleton instance. + * + * @throws None + * @since 0.20 + * @ingroup components + */ + static BuiltinPanelRegistry& instance(); + + /** + * @brief Registers a builtin panel under its appId(). + * + * @param[in] panel Non-owning pointer; caller retains ownership. + * + * @throws None + * @note Re-registering an existing id logs a warning and replaces. + * @since 0.20 + * @ingroup components + */ + void registerPanel(IBuiltinPanel* panel); + + /** + * @brief Looks up a panel by app_id. + * + * @param[in] id The app_id to find. + * + * @return The panel, or nullptr if unregistered (logged as warning). + * @throws None + * @since 0.20 + * @ingroup components + */ + IBuiltinPanel* find(const QString& id) const; + + /** + * @brief Tests whether a panel is registered (silent). + * + * @param[in] id The app_id to test. + * + * @return True if registered. + * @throws None + * @note Unlike find(), a miss is not logged. + * @since 0.20 + * @ingroup components + */ + bool contains(const QString& id) const; + + /** + * @brief Returns all registered panels (for launcher grid population). + * + * @return Vector of registered panel pointers (insertion order). + * + * @throws None + * @since 0.20 + * @ingroup components + */ + std::vector all() const; + + /** + * @brief Removes all registered panels. + * + * @throws None + * @note Test/reset only; production registers once and never clears. + * @since 0.20 + * @ingroup components + */ + void clear(); + + private: + BuiltinPanelRegistry() = default; + QHash panels_; +}; + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/builtin_apps/calculator_builtin_panel.cpp b/desktop/ui/components/builtin_apps/calculator_builtin_panel.cpp new file mode 100644 index 000000000..193161bad --- /dev/null +++ b/desktop/ui/components/builtin_apps/calculator_builtin_panel.cpp @@ -0,0 +1,69 @@ +/** + * @file calculator_builtin_panel.cpp + * @brief Implementation of CalculatorBuiltinPanel. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-07-01 + * @version 0.1 + * @since 0.20 + * @ingroup components + */ + +#include "calculator_builtin_panel.h" + +#include "calculator_panel.h" + +#include +#include +#include + +namespace cf::desktop::desktop_component { + +namespace { +/// Builtin calculator size (matches the standalone window default). +constexpr int kPanelWidth = 360; +constexpr int kPanelHeight = 520; +} // namespace + +CalculatorBuiltinPanel::CalculatorBuiltinPanel(QWidget* parent) : QWidget(parent), panel_(nullptr) { + setWindowFlags(Qt::FramelessWindowHint); + setAttribute(Qt::WA_TranslucentBackground); + setAttribute(Qt::WA_OpaquePaintEvent, false); + setAutoFillBackground(false); + + panel_ = new CalculatorPanel(this); + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(panel_); + hide(); // Hidden until popup(); Qt would otherwise auto-show this child. +} + +CalculatorBuiltinPanel::~CalculatorBuiltinPanel() = default; + +QString CalculatorBuiltinPanel::appId() const { + return QStringLiteral("calculator"); +} + +QString CalculatorBuiltinPanel::displayName() const { + return QStringLiteral("Calculator"); +} + +void CalculatorBuiltinPanel::popup(const QRect& available) { + QRect avail = available; + if (!avail.isValid() || avail.width() <= 0 || avail.height() <= 0) { + if (const auto* screen = QGuiApplication::primaryScreen()) { + avail = screen->availableGeometry(); + } + } + const int x = avail.center().x() - kPanelWidth / 2; + const int y = avail.center().y() - kPanelHeight / 2; + setGeometry(x, y, kPanelWidth, kPanelHeight); + show(); + raise(); +} + +void CalculatorBuiltinPanel::hidePanel() { + hide(); +} + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/builtin_apps/calculator_builtin_panel.h b/desktop/ui/components/builtin_apps/calculator_builtin_panel.h new file mode 100644 index 000000000..96360d122 --- /dev/null +++ b/desktop/ui/components/builtin_apps/calculator_builtin_panel.h @@ -0,0 +1,113 @@ +/** + * @file calculator_builtin_panel.h + * @brief In-process builtin adapter for the Calculator app. + * + * Wraps the standalone CalculatorPanel (apps/calculator/) as an in-process + * IBuiltinPanel so the Calculator can run either detached (its own process + * via the apps/calculator executable) or in-process (this adapter, when the + * hardware tier prefers in-process apps to save RAM). The CalculatorPanel + * source is shared between both builds; only this adapter adds the builtin + * lifecycle (popup/hidePanel over the desktop). + * + * @note Architecture exception: desktop links the apps/calculator panel + * source. Tracked for migration to a neutral shared lib in + * milestone_05. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-07-01 + * @version 0.1 + * @since 0.20 + * @ingroup components + */ + +#pragma once + +#include "ibuiltin_panel.h" + +#include + +class QRect; + +namespace cf::desktop::desktop_component { + +class CalculatorPanel; // forward — full definition needed only in the .cpp + +/** + * @brief In-process builtin adapter wrapping the standalone CalculatorPanel. + * + * Holds a CalculatorPanel child and exposes it through IBuiltinPanel so the + * launcher grid can surface a "calculator" entry that renders in-process. + * + * @ingroup components + */ +class CalculatorBuiltinPanel final : public QWidget, public IBuiltinPanel { + public: + /** + * @brief Constructs the builtin Calculator adapter. + * + * @param[in] parent Owning widget (the desktop surface). + * + * @throws None + * @note Starts hidden; call popup() to show. + * @since 0.20 + * @ingroup components + */ + explicit CalculatorBuiltinPanel(QWidget* parent = nullptr); + + /** + * @brief Destructs the adapter. + * + * @throws None + * @since 0.20 + * @ingroup components + */ + ~CalculatorBuiltinPanel() override; + + /** + * @brief Returns the stable launcher app_id. + * + * @return The id "calculator". + * + * @throws None + * @since 0.20 + * @ingroup components + */ + QString appId() const override; + + /** + * @brief Returns the icon label. + * + * @return The display name "Calculator". + * + * @throws None + * @since 0.20 + * @ingroup components + */ + QString displayName() const override; + + /** + * @brief Sizes/centers the panel over the area and shows it. + * + * @param[in] available Free screen geometry (excludes docked panels). + * + * @throws None + * @note A null/empty rect centers on the primary screen. + * @since 0.20 + * @ingroup components + */ + void popup(const QRect& available) override; + + /** + * @brief Hides the panel. + * + * @throws None + * @since 0.20 + * @ingroup components + */ + void hidePanel() override; + + private: + CalculatorPanel* panel_; ///< The wrapped panel (Qt-child owned). +}; + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/builtin_apps/ibuiltin_panel.h b/desktop/ui/components/builtin_apps/ibuiltin_panel.h new file mode 100644 index 000000000..94732c81e --- /dev/null +++ b/desktop/ui/components/builtin_apps/ibuiltin_panel.h @@ -0,0 +1,87 @@ +/** + * @file ibuiltin_panel.h + * @brief Interface for in-process builtin application panels. + * + * IBuiltinPanel is the contract for apps that render inside the desktop + * process (as child widgets over the desktop surface) rather than as a + * detached QProcess. This suits memory-constrained targets (i.MX6ULL) where + * each extra Qt process costs ~30-50MB, and single-framebuffer linuxfb where + * an external GUI app would fight the desktop for /dev/fb0. + * + * Panels are registered with BuiltinPanelRegistry and surfaced to the launcher + * grid by app_id; launching one resolves the id to its panel instance and + * calls popup(). Ownership stays with the desktop (panels are Qt children of + * the desktop surface); the registry only holds non-owning pointers. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-07-01 + * @version 0.1 + * @since 0.20 + * @ingroup components + */ + +#pragma once + +#include + +class QRect; + +namespace cf::desktop::desktop_component { + +/** + * @brief Contract for an in-process builtin application panel. + * + * Concrete panels (AboutPanel, CalculatorPanel as builtin, ...) implement + * this interface so the launcher can show them by app_id without QProcess. + * + * @ingroup components + */ +class IBuiltinPanel { + public: + virtual ~IBuiltinPanel() = default; + + /** + * @brief Returns the stable app identifier matching the launcher grid. + * + * @return The app_id (e.g. "about", "calculator"). + * + * @throws None + * @since 0.20 + * @ingroup components + */ + virtual QString appId() const = 0; + + /** + * @brief Returns the human-readable name shown under the icon. + * + * @return The display name. + * + * @throws None + * @since 0.20 + * @ingroup components + */ + virtual QString displayName() const = 0; + + /** + * @brief Sizes/centers the panel over the area and shows it. + * + * @param[in] available Free screen geometry (excludes docked panels). + * + * @throws None + * @note A null/empty rect centers on the primary screen. + * @since 0.20 + * @ingroup components + */ + virtual void popup(const QRect& available) = 0; + + /** + * @brief Hides the panel. + * + * @throws None + * @since 0.20 + * @ingroup components + */ + virtual void hidePanel() = 0; +}; + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/launcher/app_discoverer.cpp b/desktop/ui/components/launcher/app_discoverer.cpp index 2a9e1578d..c4da9a6df 100644 --- a/desktop/ui/components/launcher/app_discoverer.cpp +++ b/desktop/ui/components/launcher/app_discoverer.cpp @@ -67,6 +67,22 @@ QList AppDiscoverer::discoverFrom(const QString& apps_dir) { entry.icon_path = icon.isEmpty() ? QString() : app_dir + QLatin1Char('/') + icon; entry.exec_command = app_dir + QLatin1Char('/') + exec; + + // launch_kind defaults to Auto so the hardware tier decides at load + // time; detached/builtin force a path, unknown values fall back to Auto. + const QString kind_str = obj.value(QStringLiteral("launch_kind")).toString(); + if (kind_str.isEmpty() || kind_str == QStringLiteral("auto")) { + entry.launch_kind = LaunchKind::Auto; + } else if (kind_str == QStringLiteral("detached")) { + entry.launch_kind = LaunchKind::DetachedProcess; + } else if (kind_str == QStringLiteral("builtin")) { + entry.launch_kind = LaunchKind::BuiltinPanel; + } else { + log::warningftag(kLogTag, "Unknown launch_kind '{}' in '{}', defaulting to auto", + kind_str.toStdString(), manifest_path.toStdString()); + entry.launch_kind = LaunchKind::Auto; + } + result.append(entry); } return result; diff --git a/document/status/current.md b/document/status/current.md index e899ab0e5..39f07ee40 100644 --- a/document/status/current.md +++ b/document/status/current.md @@ -49,7 +49,7 @@ description: CFDesktop 项目进度的唯一事实来源与全局导航。 1. **MS2 状态栏**(顶部时间 + 系统图标)— ✅ 功能落地(`StatusBar` 实现 + 注册 PanelManager + 主题跟随 + MD3 美化;offscreen 启动通过,待真机视觉确认) 2. **MS3 任务栏**(底部居中图标条 + hover 动画)— ✅ 最小切片完成(`CenteredTaskbar` 注册 Bottom 面板 + `TaskbarIcon` 居中图标/hover 放大/自绘 ripple/运行指示器;`appClicked` 已接 `AppLaunchService::launch` 真启动并记 PID,运行指示器经 `WindowManager` 窗口追踪联动——见 `desktop/ui/CFDesktopEntity.cpp:133-180`) -3. **MS4 应用启动器**(应用网格 + QProcess 启动)— 🚧 进行中(最小启动闭环跑通:`AppLaunchService` 用 `QProcess::startDetached` 启动 taskbar 图标对应应用,点 Files/Browser 可真打开;开始菜单弹窗网格待做) +3. **MS4 应用启动器**(应用网格 + QProcess 启动 + 双态框架)— 🚧 进行中(启动闭环 + 双态框架落地:`AppLaunchService` 真启动;`LaunchKind`/`IBuiltinPanel`/`BuiltinPanelRegistry` 把 `builtin:` hack 正式化、消灭 if 链,calculator 双态,`HardwareTier::prefer_inprocess_apps` 按 Low/High 裁决 manifest `launch_kind:"auto"`;开始菜单弹窗网格待做。详见 [milestone_04 §六](../todo/desktop/milestone_04_app_launcher.md)) 4. **MS5 窗口管理**(窗口装饰 + 任务栏联动)— 🚧 进行中(追踪+联动切片跑通:`WindowManager` 追踪外部窗口 + `IWindow::pid()` + Taskbar 运行指示器联动;靠 PID 匹配,直接启动(xterm)可靠、间接启动(xdg-open)受限;**窗口装饰已定策→overlay 渲染**〔策略 A:CFDesktop 自绘 overlay 层呈现标题栏+控制按钮作纯视觉指示,因 WSL X11 客户端模式下外部窗口由 XWayland 管理无法直接装饰;X11 WM〔B〕/ Wayland Compositor〔C〕延后到 EGLFS/Wayland 后端,详见 [milestone_05](../todo/desktop/milestone_05_window_management.md):157-160〕) 闭环达成后按需推进:CrashHandler、IPC、EGLFS 嵌入式后端、输入抽象层、P2/P3 控件。 @@ -62,6 +62,7 @@ description: CFDesktop 项目进度的唯一事实来源与全局导航。 - **2026-06(壁纸资源包发现)**:壁纸层支持运行时动态发现第三方资源包——新增 `Wallpapers` PathType(`/Wallpapers/`)+ `filter_target_recursive` 递归扫描;`make_layer()` 改为「首个有图的源胜出」(config → Pictures 平铺,空则回退 Wallpapers 递归)。CF-Gallery 等包安装到 `Wallpapers//` 即被发现,**CFDesktop 零编译期耦合**(无 submodule/无 `#ifdef`/无 CMake option)。详见 [aels_cross_repo_deps.md](../todo/desktop/aels_cross_repo_deps.md)。动画轮播引擎(Gradient 交叉淡入 / Movement 平移 + QTimer 定时 + Sequential/Random 选择器)**已落地**(2026-06-30,`feat/wallpaper-animation-engine`)——新增 `WallPaperEngine` + `TransitionComposer`,strategy 过渡状态机 + 逐帧 QImage 合成,后端无关;6 个 `switch_*` 配置 key + 顺手接 `scaling`/`background_color`;16 例单测全过。详见 [wallpaper_animation_engine.md](../todo/desktop/wallpaper_animation_engine.md) 末尾「实施记录」。 - **2026-06(首个独立 App:Calculator)**:移植 CCIMXDesktop `Caculator` 为**独立可执行**(`apps/calculator/`)—— parser(递归下降 AST)保留 QString 建 `cfdesktop_calculator_parser` lib(53 例单测);UI cfui MD3(`Button` 网格 + `Label`)重写;CFDesktop 经 `AppLaunchService::launch`(QProcess)启动(隔离进程),`AppLaunchService` 加 `applicationDirPath()` 解析(自家 app 同 `bin/` 优先)。确立「工具型 App → 独立可执行」范式(展示型 about 仍 builtin)。 - **2026-06(App 发现机制)**:`AppDiscoverer` 扫描 `/../apps//app.json` 自动发现注册 app(manifest:app_id/display_name/icon/exec);`loadAppsConfig` fallback 链 **discover → apps.json → defaultApps**;calculator 改首个 manifest app(包自包含 `apps/calculator/{calculator, app.json}`)。App 即插即用,无需 recompile。 +- **2026-07(双态框架正式化)**:独立进程 + 进程内 builtin 两条腿**正式化**——`LaunchKind{Auto,DetachedProcess,BuiltinPanel}` 替代 `builtin:` 字符串 hack;`IBuiltinPanel` 接口 + `BuiltinPanelRegistry`(map-based 自写,aex 无 named 变体)消灭 `if(id==about)` 链;`loadAppsConfig` 合并 builtin+discovered 两源,`HardwareTierCapabilities::prefer_inprocess_apps`(Low=true)把 manifest `launch_kind:"auto"` 按 tier 裁决(Low→builtin,High→detached,无 builtin 实现则降级 detached+记日志);calculator 双态验证(`CalculatorBuiltinPanel` 组合适配器,同一份 panel 源码两种加载);补 AboutPanel 缺失入口(原 `builtin:about` 点不出)。架构债:desktop 引用 apps/calculator 源文件,待迁中立层([milestone_07](../todo/desktop/milestone_07_app_ecosystem.md))。 - **已达成**:Milestone 1「桌面骨架可见」;Phase 0 / 1 / 2 / A(CI) / 6 / G(Widget) / H(显示后端)(详见 [SUMMARY.md](../todo/done/SUMMARY.md)) ## 新人入门 diff --git a/document/todo/desktop/milestone_04_app_launcher.md b/document/todo/desktop/milestone_04_app_launcher.md index c4cf31ef6..0078817ad 100644 --- a/document/todo/desktop/milestone_04_app_launcher.md +++ b/document/todo/desktop/milestone_04_app_launcher.md @@ -233,4 +233,33 @@ description: "预计周期: 5-7 天,前置依赖: Milestone 3: 任务栏" --- -*最后更新: 2026-06-29(核对代码现状:app_launcher/launcher_tile/app_launch_service 已落地;搜索框从"可选"升级为明确待办)* +## 六、builtin 正式化 + 双态框架(2026-07 落地) + +> **状态**: ✅ 已落地(`feat/app-dual-mode` 分支) + +把 calculator 路线的「独立进程 App」与 about 的「进程内 builtin」**正式化为统一双态框架**,由硬件档位裁决。 + +### 落地点 + +- **`LaunchKind` 枚举**([app_entry.h](../../../desktop/ui/components/app_entry.h)):`Auto`/`DetachedProcess`/`BuiltinPanel`,替代原 `builtin:` 字符串前缀 hack +- **`IBuiltinPanel` 接口**([ibuiltin_panel.h](../../../desktop/ui/components/builtin_apps/ibuiltin_panel.h)):照 IPanel 范式(无导出宏/纯虚) +- **`BuiltinPanelRegistry`**([builtin_panel_registry.{h,cpp}](../../../desktop/ui/components/builtin_apps/)):map-based 自写(aex 无 named factory 变体),按 app_id 查 panel 单例,未命中记 warning(不静默) +- **AboutPanel 实现 IBuiltinPanel** + **补网格入口**(原 `builtin:about` 入口未接——点不出 About,现已补) +- **消灭 `if (id=="about")` 链**([CFDesktopEntity.cpp](../../../desktop/ui/CFDesktopEntity.cpp)):改按 `entry.launch_kind` 分发 +- **calculator 双态**:`CalculatorBuiltinPanel` 组合适配器包住 CalculatorPanel,同一份 panel 源码两种加载方式 +- **`HardwareTierCapabilities::prefer_inprocess_apps`**([hardware_tier_data.h](../../../base/include/system/hardware_tier/hardware_tier_data.h) + [default_policy.cpp](../../../base/system/hardware_tier/default/default_policy.cpp)):Low/Unknown 档=true +- **manifest `launch_kind`**([app_discoverer.cpp](../../../desktop/ui/components/launcher/app_discoverer.cpp)):`auto`(默认,tier 裁决)/`detached`/`builtin` +- **`loadAppsConfig` 合并 builtin + discovered**,Auto 按 `prefer_inprocess && registry 有实现` 裁决,无实现则降级 detached 并记日志(不静默 builtin) +- **单测**:BuiltinPanelRegistry 5 例、AppDiscoverer launch_kind 3 例,全过 + +### ⚠️ 架构例外(已登记) + +desktop 编译期引用 `apps/calculator/calculator_panel.cpp` 源文件 + `cfdesktop_calculator_parser` lib(desktop→apps 反向依赖,违反分层 spirit)。顶层 CMake 已调 `apps` 在 `desktop` 前配置。**架构债待还**:迁 calculator 核心到中立层,见 [milestone_07 §依赖与架构债](milestone_07_app_ecosystem.md)。 + +### 验证 + +点 About 弹出 panel(补入口);calculator 在 High 档走独立进程、`setDeviceConfigOverride(Low)` 下走 builtin;builtin 不产生额外进程(linuxfb/6ULL 心智模型成立)。 + +--- + +*最后更新: 2026-07-01(builtin 正式化 + 双态框架 + auto 裁决落地,`feat/app-dual-mode` 分支;第三方 App 平台路线见 milestone_07)* diff --git a/document/todo/desktop/milestone_07_app_ecosystem.md b/document/todo/desktop/milestone_07_app_ecosystem.md new file mode 100644 index 000000000..15125d308 --- /dev/null +++ b/document/todo/desktop/milestone_07_app_ecosystem.md @@ -0,0 +1,96 @@ +--- +title: "Milestone 7: 第三方 App 平台" +description: "预计周期: 分阶段演进;前置依赖: Milestone 4 双态地基" +--- + +# Milestone 7: 第三方 App 平台 + +> **状态**: 📋 规划中(未启动) +> **前置依赖**: [Milestone 4: 应用启动器](milestone_04_app_launcher.md) 双态地基已落地(LaunchKind/IBuiltinPanel/BuiltinPanelRegistry/auto 裁决) +> **目标**: 让外部贡献者/独立仓按规范写 App,运行时被 CFDesktop 发现并集成为一等公民 + +--- + +## 一、定位与跨硬件取舍 + +第三方 App **只能走独立进程**(`LaunchKind::DetachedProcess`)——进程内 builtin 是 shell 维护者的特权(源码编译期链进 shell 二进制),第三方代码不应进 shell 进程(ABI 耦合 + 崩溃 + 安全)。 + +跨硬件谱系下的现实: + +| 档位 | 第三方独立进程 App 的处境 | +|------|---------------------------| +| i.MX6ULL(Low) | 每个 ~30-50MB,shell 自己在内存红线上挣扎——**第三方基本跑不动/不该跑**。6ULL App 生态靠内置 builtin | +| RK3568(Mid) | 勉强能开少量,需克制 | +| RK3588/上位机(High) | **第三方主战场**,内存宽裕,独立进程随便开 | + +**结论**:本里程碑是 **RK/上位机阶段**的特性,不阻塞 6ULL North Star。但 manifest 规范与发现路径**现在就要前瞻预留**(内置 App 用同一份 manifest,定了后面是"开放"而非"重做")。 + +--- + +## 二、分阶段实现 + +### Phase 1 — manifest 规范扩展 + +`app.json` 在现有 4 字段(app_id/display_name/exec/launch_kind)基础上加: + +| 字段 | 必要性 | 用途 | +|------|--------|------| +| `icon` | 建议 | 网格图标(calculator 现缺,补) | +| `version` | 建议 | App 版本 | +| `min_shell_version` | 第三方特需 | shell 版本低于此值不加载 + 记日志(对齐 no-silent-fallback) | +| `single_instance` | 第三方特需 | 第二实例启动时 shell 把已有窗口拉前台 | +| `categories` | 可选 | 网格分组 | + +**刻意不做** capabilities/permissions 沙箱:第三方是独立进程,跑在用户权限下跟 shell 平级,OS 级权限本就是它自己的事。沙箱是 OS 层(cgroup/seccomp),不是桌面 shell 现阶段该背的。 + +### Phase 2 — 多路径发现 + +现 [app_discoverer.cpp](../../../desktop/ui/components/launcher/app_discoverer.cpp) 只扫 `/../apps/`。加 XDG 语义路径: + +| 路径 | 用途 | 优先级 | +|------|------|--------| +| `/../apps/`(内置) | 主仓自带 | 最高 | +| `~/.local/share/cfdesktop/apps/` | 单用户安装 | 中 | +| `/usr/share/cfdesktop/apps/` | 发行版打包 | 低 | + +`app_id` 冲突时高优先级覆盖。可选吃一层系统 `.desktop`(让 CFDesktop 自动发现 firefox 等),作为兼容层。 + +### Phase 3 — cfapp SDK + +第三方独立进程 App 想跟 shell 集成(主题跟随/读配置/单实例/任务栏指示),需 Runtime SDK。**全部走 QLocalSocket**(对齐 6ULL 铁律:文件/socket IPC,不引 D-Bus)。 + +| 能力 | 干什么 | +|------|--------| +| Theme bridge | shell 推当前主题 token,App 用 QuarkWidgets 应用 | +| Single-instance | 启动握手,已有实例则拉前台 | +| Launch handshake | App 注册("id=X,pid=Y,开窗了")→ 任务栏运行指示(补 `AppEntry::is_running`) | +| Config bridge | 读写 `cfdesktop` 命名空间共享配置 | + +**cfapp 依赖边界**:只依赖 aex + QuarkWidgets + Qt,**不碰 cfui/cfbase/desktop**。**可选**:第三方不链接 cfapp 也能跑(失去集成),平缓接入曲线。 + +**位置**:独立 lib(主仓 `sdk/cfapp/` 或独立仓),实现时定。是 [[desktop-buildout-handoff]] 提及的"desktop-extension-tools"的演进。 + +### Phase 4 — 最低限度安全 + +manifest 解析加防御:字段长度上限、exec 路径不逃逸 apps 目录、JSON 体积上限、启动失败可见(已有)。更深沙箱是 OS 层,不背。 + +--- + +## 三、依赖与架构债 + +- **依赖 milestone_04 双态地基**:LaunchKind/Registry/auto 裁决已就位,本里程碑在其上扩展。 +- **架构债(待还)**:milestone_04 步骤 7 让 desktop 编译期引用 `apps/calculator` 源文件 + parser lib(desktop→apps 反向依赖,违反分层 spirit)。本里程碑 **Phase 3 前置任务**:把 calculator 核心(parser+panel)迁到中立层(新 shared lib 或 `third_party`),desktop 与 apps exe 都向下依赖它,彻底解耦。迁移完成后 `grep -r '#include.*apps/' desktop/` 应为空。 + +--- + +## 四、验收标准(完成时) + +- [ ] manifest 规范文档化,calculator manifest 补 icon +- [ ] 多路径发现:用户级/系统级 App 被发现 +- [ ] cfapp SDK:主题跟随 + 单实例 + 任务栏指示(可选接入) +- [ ] calculator 核心迁中立层,desktop→apps 依赖清零 +- [ ] 安全:manifest 解析防御 + 失败可见 + +--- + +*创建: 2026-07-01(随 milestone_04 builtin 正式化一并规划,未实现)* diff --git a/test/desktop/CMakeLists.txt b/test/desktop/CMakeLists.txt index 0ed473bee..7c095a734 100644 --- a/test/desktop/CMakeLists.txt +++ b/test/desktop/CMakeLists.txt @@ -18,3 +18,6 @@ add_subdirectory(calculator) # Add launcher (app discovery) tests subdirectory add_subdirectory(launcher) + +# Add builtin apps registry tests subdirectory +add_subdirectory(builtin_apps) diff --git a/test/desktop/builtin_apps/CMakeLists.txt b/test/desktop/builtin_apps/CMakeLists.txt new file mode 100644 index 000000000..06a6c6de7 --- /dev/null +++ b/test/desktop/builtin_apps/CMakeLists.txt @@ -0,0 +1,17 @@ +# BuiltinPanelRegistry unit tests. +add_gtest_executable( + TEST_NAME builtin_panel_registry_test + SOURCE_FILE builtin_panel_registry_test.cpp + LINK_LIBRARIES + cfdesktop_builtin_apps + cflogger + cfbase + Qt6::Core + Qt6::Gui + Qt6::Widgets + GTest::gtest + GTest::gtest_main + INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/desktop/ui/components + LABELS "desktop;unit;builtin_apps" + LOG_MODULE builtin_apps_tests +) diff --git a/test/desktop/builtin_apps/builtin_panel_registry_test.cpp b/test/desktop/builtin_apps/builtin_panel_registry_test.cpp new file mode 100644 index 000000000..d2419a350 --- /dev/null +++ b/test/desktop/builtin_apps/builtin_panel_registry_test.cpp @@ -0,0 +1,93 @@ +/** + * @file builtin_panel_registry_test.cpp + * @brief Unit tests for BuiltinPanelRegistry. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-07-01 + * @version 0.2 + * @since 0.20 + * @ingroup components + */ + +#include "builtin_apps/builtin_panel_registry.h" + +#include +#include +#include + +namespace { + +/// Minimal IBuiltinPanel stub for registry tests (no real rendering). +class FakePanel : public cf::desktop::desktop_component::IBuiltinPanel { + public: + FakePanel(QString id, QString name) : id_(std::move(id)), name_(std::move(name)) {} + + QString appId() const override { return id_; } + QString displayName() const override { return name_; } + void popup(const QRect&) override {} + void hidePanel() override {} + + private: + QString id_; + QString name_; +}; + +} // namespace + +// BuiltinPanelRegistry is a process-wide singleton holding non-owning +// pointers. Each test registers stack-local FakePanels; clearing in SetUp +// avoids dangling pointers accumulating across tests (which crashes Clang/MSVC +// when all() iterates them after the FakePanels went out of scope). +class BuiltinPanelRegistryTest : public ::testing::Test { + protected: + void SetUp() override { + cf::desktop::desktop_component::BuiltinPanelRegistry::instance().clear(); + } +}; + +TEST_F(BuiltinPanelRegistryTest, FindUnknownReturnsNull) { + auto& reg = cf::desktop::desktop_component::BuiltinPanelRegistry::instance(); + EXPECT_EQ(reg.find(QStringLiteral("definitely_unknown_id")), nullptr); +} + +TEST_F(BuiltinPanelRegistryTest, RegisterAndFind) { + auto& reg = cf::desktop::desktop_component::BuiltinPanelRegistry::instance(); + FakePanel panel(QStringLiteral("register_and_find"), QStringLiteral("RegisterAndFind")); + reg.registerPanel(&panel); + + auto* found = reg.find(QStringLiteral("register_and_find")); + ASSERT_NE(found, nullptr); + EXPECT_EQ(found->appId().toStdString(), "register_and_find"); + EXPECT_EQ(found->displayName().toStdString(), "RegisterAndFind"); +} + +TEST_F(BuiltinPanelRegistryTest, ContainsMatchesRegistered) { + auto& reg = cf::desktop::desktop_component::BuiltinPanelRegistry::instance(); + EXPECT_FALSE(reg.contains(QStringLiteral("contains_match"))); + FakePanel panel(QStringLiteral("contains_match"), QStringLiteral("Contains")); + reg.registerPanel(&panel); + EXPECT_TRUE(reg.contains(QStringLiteral("contains_match"))); +} + +TEST_F(BuiltinPanelRegistryTest, DuplicateIdReplaces) { + auto& reg = cf::desktop::desktop_component::BuiltinPanelRegistry::instance(); + FakePanel first(QStringLiteral("dup_id"), QStringLiteral("First")); + FakePanel second(QStringLiteral("dup_id"), QStringLiteral("Second")); + reg.registerPanel(&first); + reg.registerPanel(&second); + + auto* found = reg.find(QStringLiteral("dup_id")); + ASSERT_NE(found, nullptr); + EXPECT_EQ(found->displayName().toStdString(), "Second"); +} + +TEST_F(BuiltinPanelRegistryTest, AllIncludesRegistered) { + auto& reg = cf::desktop::desktop_component::BuiltinPanelRegistry::instance(); + FakePanel panel(QStringLiteral("in_all"), QStringLiteral("InAll")); + reg.registerPanel(&panel); + + const auto all = reg.all(); + EXPECT_EQ(all.size(), 1u); + ASSERT_NE(all[0], nullptr); + EXPECT_EQ(all[0]->appId().toStdString(), "in_all"); +} diff --git a/test/desktop/launcher/app_discoverer_test.cpp b/test/desktop/launcher/app_discoverer_test.cpp index 0c865661c..c8375a2a4 100644 --- a/test/desktop/launcher/app_discoverer_test.cpp +++ b/test/desktop/launcher/app_discoverer_test.cpp @@ -22,6 +22,7 @@ #include using cf::desktop::desktop_component::AppDiscoverer; +using cf::desktop::desktop_component::LaunchKind; namespace { /// Writes a manifest body to //app.json. @@ -83,3 +84,30 @@ TEST(AppDiscoverer, EmptyIconYieldsEmptyIconPath) { ASSERT_EQ(apps.size(), 1); EXPECT_TRUE(apps[0].icon_path.isEmpty()); } + +TEST(AppDiscoverer, DefaultLaunchKindIsAuto) { + QTemporaryDir tmp; + writeManifest(tmp.path(), QStringLiteral("calc"), + R"({"app_id":"calc","exec":"calc"})"); // no launch_kind field + const auto apps = AppDiscoverer::discoverFrom(tmp.path()); + ASSERT_EQ(apps.size(), 1); + EXPECT_EQ(apps[0].launch_kind, LaunchKind::Auto); +} + +TEST(AppDiscoverer, ExplicitLaunchKindDetached) { + QTemporaryDir tmp; + writeManifest(tmp.path(), QStringLiteral("calc"), + R"({"app_id":"calc","exec":"calc","launch_kind":"detached"})"); + const auto apps = AppDiscoverer::discoverFrom(tmp.path()); + ASSERT_EQ(apps.size(), 1); + EXPECT_EQ(apps[0].launch_kind, LaunchKind::DetachedProcess); +} + +TEST(AppDiscoverer, ExplicitLaunchKindBuiltin) { + QTemporaryDir tmp; + writeManifest(tmp.path(), QStringLiteral("calc"), + R"({"app_id":"calc","exec":"calc","launch_kind":"builtin"})"); + const auto apps = AppDiscoverer::discoverFrom(tmp.path()); + ASSERT_EQ(apps.size(), 1); + EXPECT_EQ(apps[0].launch_kind, LaunchKind::BuiltinPanel); +}