Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions apps/calculator/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@ target_link_libraries(calculator PRIVATE
Qt6::Widgets
)

# Place next to the CFDesktop binary so AppLaunchService's applicationDirPath()
# resolution finds it (exec_command "calculator" -> <bin>/calculator).
# App package is self-contained under <bin>/../apps/calculator/ (executable +
# manifest in the same dir). AppDiscoverer scans <bin>/../apps/<id>/app.json,
# resolves exec to the co-located absolute path, and AppLaunchService launches
# it directly — no working-directory dependency, no PATH search needed.
set(CALC_APP_DIR "${CMAKE_BINARY_DIR}/apps/calculator")
set_target_properties(calculator PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
RUNTIME_OUTPUT_DIRECTORY "${CALC_APP_DIR}"
)

# Deploy the manifest next to the executable (at configure time).
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/app.json" "${CALC_APP_DIR}/app.json" COPYONLY)
5 changes: 5 additions & 0 deletions apps/calculator/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"app_id": "calculator",
"display_name": "Calculator",
"exec": "calculator"
}
106 changes: 55 additions & 51 deletions desktop/ui/CFDesktopEntity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "components/PanelManager.h"
#include "components/WindowManager.h"
#include "components/builtin_apps/about_panel.h"
#include "components/launcher/app_discoverer.h"
#include "components/launcher/app_launch_service.h"
#include "components/launcher/app_launcher.h"
#include "components/statusbar/status_bar.h"
Expand All @@ -32,15 +33,19 @@
namespace cf::desktop {

namespace {
/// Loads the per-board app list from <bin>/settings/apps.json. Falls back to
/// defaultApps() when the file is missing, unreadable, empty, or has no valid
/// entries -- so a board only needs to drop an apps.json to customize the
/// launcher/taskbar without a recompile.
/// Loads the app list with a fallback chain: auto-discovered apps
/// (<bin>/../apps/<id>/app.json) first, then <bin>/../apps.json manual list,
/// then defaultApps(). A board can drop app manifests or apps.json to
/// customize the launcher/taskbar without a recompile.
QList<desktop_component::AppEntry> loadAppsConfig() {
// Read from the desktop root (<bin>/../apps.json). 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");
// Prefer auto-discovered apps (<bin>/../apps/<id>/app.json manifests).
auto discovered = desktop_component::AppDiscoverer::discover();
if (!discovered.isEmpty()) {
return discovered;
}
// Fallback: <bin>/../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();
Expand Down Expand Up @@ -272,38 +277,38 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) {
// so no framebuffer fight with the desktop on linuxfb).
auto* about_panel = new cf::desktop::desktop_component::AboutPanel(desktop_entity_);

std::function<void(const QString&)> launch_app =
[apps, app_pid, about_panel, panel_mgr](const QString& app_id) {
QString exec;
for (const auto& app : apps) {
if (app.app_id == app_id) {
exec = app.exec_command;
break;
}
}
if (exec.isEmpty()) {
cf::log::warningftag("CFDesktopEntity", "No exec for app_id '{}'",
app_id.toStdString());
return;
std::function<void(const QString&)> launch_app = [apps, app_pid, about_panel,
panel_mgr](const QString& app_id) {
QString exec;
for (const auto& app : apps) {
if (app.app_id == app_id) {
exec = app.exec_command;
break;
}
// 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());
} else {
cf::log::warningftag("CFDesktopEntity", "Unknown builtin app '{}'",
id.toStdString());
}
return;
}
const auto launched = cf::desktop::desktop_component::AppLaunchService::launch(exec);
if (launched.has_value()) {
(*app_pid)[app_id] = *launched;
}
if (exec.isEmpty()) {
cf::log::warningftag("CFDesktopEntity", "No exec 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());
} else {
cf::log::warningftag("CFDesktopEntity", "Unknown builtin app '{}'",
id.toStdString());
}
};
return;
}
const auto launched = cf::desktop::desktop_component::AppLaunchService::launch(exec);
if (launched.has_value()) {
(*app_pid)[app_id] = *launched;
}
};
QObject::connect(taskbar, &cf::desktop::desktop_component::CenteredTaskbar::appClicked, this,
launch_app);

Expand All @@ -312,19 +317,18 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) {
app_launcher->setApps(apps);
QObject::connect(app_launcher, &cf::desktop::desktop_component::AppLauncher::appLaunched, this,
launch_app);
QObject::connect(
taskbar, &cf::desktop::desktop_component::CenteredTaskbar::launcherRequested, this,
[app_launcher, panel_mgr]() {
// Toggle: clicking Start while the launcher is open dismisses it,
// otherwise pop it up. The start button used to only ever call
// popup(), so a second click was a no-op while the menu was already
// visible.
if (app_launcher->isShowing()) {
app_launcher->hideLauncher();
} else {
app_launcher->popup(panel_mgr->availableGeometry());
}
});
QObject::connect(taskbar, &cf::desktop::desktop_component::CenteredTaskbar::launcherRequested,
this, [app_launcher, panel_mgr]() {
// Toggle: clicking Start while the launcher is open dismisses it,
// otherwise pop it up. The start button used to only ever call
// popup(), so a second click was a no-op while the menu was already
// visible.
if (app_launcher->isShowing()) {
app_launcher->hideLauncher();
} else {
app_launcher->popup(panel_mgr->availableGeometry());
}
});
taskbar->show();
panel_mgr->relayout();

Expand Down
2 changes: 0 additions & 2 deletions desktop/ui/components/app_entry.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@ inline QList<AppEntry> defaultApps() {
{QStringLiteral("browser"), QStringLiteral("Browser"),
QStringLiteral(":/cfdesktop/taskbar/browser.png"),
QStringLiteral("xdg-open https://example.com"), false},
{QStringLiteral("calculator"), QStringLiteral("Calculator"), QStringLiteral(""),
QStringLiteral("calculator"), false},
};
}

Expand Down
1 change: 1 addition & 0 deletions desktop/ui/components/launcher/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# App launcher service (QProcess-based external application launching).
add_library(cfdesktop_launcher STATIC
app_discoverer.cpp
app_launch_service.cpp
app_launcher.cpp
launcher_tile.cpp
Expand Down
75 changes: 75 additions & 0 deletions desktop/ui/components/launcher/app_discoverer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* @file desktop/ui/components/launcher/app_discoverer.cpp
* @brief Implementation of AppDiscoverer.
*
* @author Charliechen114514 (chengh1922@mails.jlu.edu.cn)
* @date 2026-06-30
* @version 0.1
* @since 0.20
* @ingroup components
*/

#include "app_discoverer.h"

#include "cflog.h"

#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>

namespace cf::desktop::desktop_component {

namespace {
/// Tag for AppDiscoverer log lines.
constexpr const char* kLogTag = "AppDiscoverer";
/// Manifest filename inside each <id>/ app directory.
constexpr const char* kManifestName = "app.json";
} // namespace

QList<AppEntry> AppDiscoverer::discover() {
const QString apps_dir = QCoreApplication::applicationDirPath() + QStringLiteral("/../apps");
return discoverFrom(apps_dir);
}

QList<AppEntry> AppDiscoverer::discoverFrom(const QString& apps_dir) {
QList<AppEntry> result;
QDir dir(apps_dir);
if (!dir.exists()) {
return result;
}

const QFileInfoList subdirs = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const auto& sub : subdirs) {
const QString app_dir = sub.absoluteFilePath();
const QString manifest_path =
app_dir + QLatin1Char('/') + QString::fromLatin1(kManifestName);

QFile file(manifest_path);
if (!file.open(QIODevice::ReadOnly)) {
continue; // No app.json — not an app directory, skip silently.
}

const QJsonObject obj = QJsonDocument::fromJson(file.readAll()).object();
AppEntry entry;
entry.app_id = obj.value(QStringLiteral("app_id")).toString();
entry.display_name = obj.value(QStringLiteral("display_name")).toString();
const QString icon = obj.value(QStringLiteral("icon")).toString();
const QString exec = obj.value(QStringLiteral("exec")).toString();

if (entry.app_id.isEmpty() || exec.isEmpty()) {
log::warningftag(kLogTag, "Skipping '{}': missing app_id/exec",
manifest_path.toStdString());
continue;
}

entry.icon_path = icon.isEmpty() ? QString() : app_dir + QLatin1Char('/') + icon;
entry.exec_command = app_dir + QLatin1Char('/') + exec;
result.append(entry);
}
return result;
}

} // namespace cf::desktop::desktop_component
72 changes: 72 additions & 0 deletions desktop/ui/components/launcher/app_discoverer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @file desktop/ui/components/launcher/app_discoverer.h
* @brief Discovers standalone apps via per-app manifest files.
*
* Each app ships an @c app.json manifest in @c <apps_dir>/<app_id>/.
* AppDiscoverer scans the apps directory, parses each manifest, and returns
* AppEntry values with absolute @c icon_path / @c exec_command (so launch and
* icon rendering do not depend on the working directory).
*
* @author Charliechen114514 (chengh1922@mails.jlu.edu.cn)
* @date 2026-06-30
* @version 0.1
* @since 0.20
* @ingroup components
*/

#pragma once

#include "../app_entry.h"

#include <QList>
#include <QString>

namespace cf::desktop::desktop_component {

/**
* @brief Discovers standalone apps from per-app @c app.json manifests.
*
* The discovery contract:
* - @c <apps_dir>/<id>/app.json is the manifest (JSON object).
* - Fields: @c app_id, @c display_name, @c icon (relative to manifest),
* @c exec (relative to manifest).
* - @c icon_path / @c exec_command are resolved to absolute paths
* (@c <app_dir>/<icon|exec>).
* - Subdirs without @c app.json are skipped silently; manifests missing
* @c app_id or @c exec are skipped with a warning (no silent fallback).
*
* @ingroup components
*/
class AppDiscoverer {
public:
AppDiscoverer() = delete;

/**
* @brief Discovers apps under @c <bin>/../apps/<id>/app.json.
*
* @return Parsed AppEntry list (icon/exec resolved to absolute paths);
* empty if the apps directory is absent or has no manifests.
*
* @throws None.
*
* @since 0.20
* @ingroup components
*/
static QList<AppEntry> discover();

/**
* @brief Discovers apps under @p apps_dir.
*
* @param[in] apps_dir Directory containing <id>/ subdirs with app.json.
*
* @return Parsed AppEntry list.
*
* @throws None.
* @note Used by discover(); exposed for unit tests.
* @since 0.20
* @ingroup components
*/
static QList<AppEntry> discoverFrom(const QString& apps_dir);
};

} // namespace cf::desktop::desktop_component
1 change: 1 addition & 0 deletions document/status/current.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ description: CFDesktop 项目进度的唯一事实来源与全局导航。
- **2026-06**:`refactor: refactor the ui subsystem`;`hwtier system enabled`;文档清理
- **2026-06(壁纸资源包发现)**:壁纸层支持运行时动态发现第三方资源包——新增 `Wallpapers` PathType(`<desktop_active_root>/Wallpapers/`)+ `filter_target_recursive` 递归扫描;`make_layer()` 改为「首个有图的源胜出」(config → Pictures 平铺,空则回退 Wallpapers 递归)。CF-Gallery 等包安装到 `Wallpapers/<pack>/` 即被发现,**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` 扫描 `<bin>/../apps/<id>/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。
- **已达成**:Milestone 1「桌面骨架可见」;Phase 0 / 1 / 2 / A(CI) / 6 / G(Widget) / H(显示后端)(详见 [SUMMARY.md](../todo/done/SUMMARY.md))

## 新人入门
Expand Down
1 change: 1 addition & 0 deletions document/todo/desktop/13_widget_apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ description: "预计周期: 4~5 周,依赖阶段: Phase 9, Phase 12"
> - **资源包运行时发现已合入**(PR #13,2026-06):第三方壁纸包安装到 `<desktop_active_root>/Wallpapers/<pack>/` 即被递归发现;CF-Gallery 提供 `install-to-cfdesktop.sh`。
> - **轮播/动画引擎已落地**(2026-06-30,`feat/wallpaper-animation-engine`):新增 `WallPaperEngine` + `TransitionComposer`,strategy 过渡状态机逐帧合成;Gradient/Movement + Sequential/Random,配置驱动(`switch_mode`/`switch_selector`/`switch_interval_ms`/`animation_duration_ms`/`switch_easing`/`disable_animation`),顺手接 `scaling`/`background_color`;16 例单测。详见 [wallpaper_animation_engine.md](wallpaper_animation_engine.md) 末尾「实施记录」。
> - **首个独立 App(Calculator)已落地**(2026-06-30,`feat/calculator-app`):移植 CCIMXDesktop `Caculator` —— parser(递归下降 AST)保留 QString、独立 `cfdesktop_calculator_parser` lib(53 例单测);UI cfui MD3(`Button` 网格 + `Label`)重写;**独立可执行** `apps/calculator/`,CFDesktop 经 `AppLaunchService::launch`(QProcess)启动(隔离进程)。**确立「工具型 App → 独立可执行 + QProcess」范式**:展示型(about)走 builtin child panel,工具型(calculator/Noter/FileRamber)走独立可执行;`AppLaunchService` 加了 `applicationDirPath()` 解析(自家 app 同 `bin/` 优先,extern 回退 PATH)。
> - **App 发现机制(manifest + AppDiscoverer)已落地**(2026-06-30,`feat/calculator-app`):每 app 一个 `app.json` manifest 放 `<bin>/../apps/<id>/`(自包含:可执行 + manifest + icon 同目录);`AppDiscoverer` 启动扫描 `apps/<id>/app.json` 自动发现注册,icon/exec 解析为绝对路径;`loadAppsConfig` 改 fallback 链 **discover → apps.json → defaultApps**。calculator 改作首个 manifest app。**App 即插即用**(丢 app 包目录即可,无需 recompile)。
> - **Widget 框架尺度调和**:本 Phase(Phase J)画的是**全量愿景**(沙箱/进程隔离/API);近期落地以 [milestone_06](milestone_06_widget_control_center.md) **最小版**(ClockWidget + ControlCenter)为准。沙箱依赖 IPC(0%)+CrashHandler;低 RAM 退化同进程崩溃隔离。
> - **FileManagerApp 现实**:① 依赖 P2 控件(ToolBar/ContextMenu/TreeView 等,当前 0%);② 本仓 `desktop/base/file_operations/file_op.h` **只是路径拼接微工具**(copy/move/rename/delete/trash 全无),**不能当文件管理器底座**,需新建;③ 建议参照 CCIMX `FileRamber`(QtConcurrent 异步刷新)移植。
> - **数据源前置**:WeatherWidget 依赖 `aels-network`;ResourceMonitorWidget 依赖 `base/system` 探针(已 90%);均见 [aels_cross_repo_deps.md](aels_cross_repo_deps.md)。
Expand Down
3 changes: 3 additions & 0 deletions test/desktop/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ add_subdirectory(wallpaper_animation)

# Add calculator parser tests subdirectory
add_subdirectory(calculator)

# Add launcher (app discovery) tests subdirectory
add_subdirectory(launcher)
21 changes: 21 additions & 0 deletions test/desktop/launcher/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# AppDiscoverer unit tests.
# cfdesktop_launcher is STATIC with PRIVATE deps, so re-link every transitive
# dependency pulled in through app_discoverer.cpp (cflogger for log macros,
# cfbase/aex/QuarkWidgets via the launcher target).
add_gtest_executable(
TEST_NAME app_discoverer_test
SOURCE_FILE app_discoverer_test.cpp
LINK_LIBRARIES
cfdesktop_launcher
cflogger
cfbase
QuarkWidgets::quarkwidgets
aex::aex
Qt6::Core
Qt6::Widgets
GTest::gtest
GTest::gtest_main
INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/desktop/ui/components
LABELS "desktop;unit;launcher"
LOG_MODULE launcher_tests
)
Loading
Loading