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
13 changes: 12 additions & 1 deletion base/device/console/console_helper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,18 @@ std::optional<console_size_t> ConsoleHelper::size() const {
if (!size_querier) {
return {};
}
return size_querier->execute();
auto result = size_querier->execute();
if (!result) {
return {};
}
// A degenerate size (non-positive width or height) means the console size
// is unknown -- e.g. a serial console that reports no window size. Treat it
// as unknown so callers fall back to fallback_size() instead of rendering
// into a zero-sized buffer (which throws std::length_error downstream).
if (result->first <= 0 || result->second <= 0) {
return {};
}
return result;
}

bool ConsoleHelper::query_property(const std::string_view property, std::any* value) {
Expand Down
107 changes: 87 additions & 20 deletions desktop/ui/CFDesktopEntity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "components/IDisplayServerBackend.h"
#include "components/PanelManager.h"
#include "components/WindowManager.h"
#include "components/builtin_apps/about_panel.h"
#include "components/launcher/app_launch_service.h"
#include "components/launcher/app_launcher.h"
#include "components/statusbar/status_bar.h"
Expand All @@ -19,12 +20,49 @@
#include "platform/display_backend_helper.h"
#include "platform/shell_layer_helper.h"
#include "qt_format.h"
#include <QCoreApplication>
#include <QFile>
#include <QHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <functional>
#include <memory>

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.
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");
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
return desktop_component::defaultApps();
}
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
const auto arr = doc.object().value(QStringLiteral("apps")).toArray();
QList<desktop_component::AppEntry> 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);
}
}
return apps.isEmpty() ? desktop_component::defaultApps() : apps;
}
} // namespace

std::unique_ptr<CFDesktopEntity> CFDesktopEntity::global_instance_;

CFDesktopEntity& CFDesktopEntity::instance() {
Expand Down Expand Up @@ -220,33 +258,52 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) {

// ── Taskbar: bottom-edge panel (centered app icons) ──
// apps is captured by the click handler to resolve app_id -> exec_command.
const QList<cf::desktop::desktop_component::AppEntry> apps =
cf::desktop::desktop_component::defaultApps();
// Loaded from settings/apps.json (per-board app list) if present.
const QList<cf::desktop::desktop_component::AppEntry> apps = loadAppsConfig();
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.
std::function<void(const QString&)> launch_app = [apps, app_pid](const QString& app_id) {
QString exec;
for (const auto& app : apps) {
if (app.app_id == app_id) {
exec = app.exec_command;
break;
// 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<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;
}
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 @@ -257,7 +314,17 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) {
launch_app);
QObject::connect(
taskbar, &cf::desktop::desktop_component::CenteredTaskbar::launcherRequested, this,
[app_launcher, panel_mgr]() { app_launcher->popup(panel_mgr->availableGeometry()); });
[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: 2 additions & 0 deletions desktop/ui/components/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ add_subdirectory(window_placement)
add_subdirectory(statusbar)
add_subdirectory(taskbar)
add_subdirectory(launcher)
add_subdirectory(builtin_apps)

# Create interface library for convenience
add_library(cf_desktop_components STATIC)
Expand Down Expand Up @@ -38,6 +39,7 @@ PRIVATE
cfdesktop_statusbar
cfdesktop_taskbar
cfdesktop_launcher
cfdesktop_builtin_apps
cfdesktop_window_placement
Qt6::Core Qt6::Gui Qt6::Widgets
)
17 changes: 17 additions & 0 deletions desktop/ui/components/builtin_apps/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Builtin in-process apps (child widgets over the desktop, e.g. About).
# These are NOT launched via QProcess; CFDesktop shows them directly, which
# suits single-framebuffer targets (linuxfb) where an external GUI app would
# fight the desktop for /dev/fb0.
add_library(cfdesktop_builtin_apps STATIC
about_panel.cpp
)

target_include_directories(cfdesktop_builtin_apps PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..>
)

target_link_libraries(cfdesktop_builtin_apps
PRIVATE
Qt6::Core Qt6::Gui Qt6::Widgets
)
110 changes: 110 additions & 0 deletions desktop/ui/components/builtin_apps/about_panel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* @file about_panel.cpp
* @brief In-process "About CFDesktop" panel implementation.
*
* @author Charliechen114514 (chengh1922@mails.jlu.edu.cn)
* @date 2026-06-30
* @version 0.1
* @since 0.20
* @ingroup components
*/

#include "about_panel.h"

#include <QGuiApplication>
#include <QMouseEvent>
#include <QPaintEvent>
#include <QPainter>
#include <QPainterPath>
#include <QScreen>

#include <algorithm>

namespace cf::desktop::desktop_component {

namespace {
constexpr qreal kCornerRadius = 16.0; ///< Card corner radius (px).
constexpr qreal kWidthRatio = 0.42; ///< Card width as a fraction of available.
constexpr qreal kHeightRatio = 0.36; ///< Card height as a fraction of available.
constexpr int kMinWidth = 360; ///< Minimum card width (px).
constexpr int kMaxWidth = 560; ///< Maximum card width (px).
constexpr int kMinHeight = 220; ///< Minimum card height (px).
constexpr int kMaxHeight = 340; ///< Maximum card height (px).
} // namespace

AboutPanel::AboutPanel(QWidget* parent)
: 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);
setAttribute(Qt::WA_OpaquePaintEvent, false);
setAutoFillBackground(false);
// Stay hidden until popup(). As a child of the desktop, Qt would otherwise
// auto-show this widget when the desktop becomes visible.
hide();
}

AboutPanel::~AboutPanel() = default;

void AboutPanel::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 w = std::clamp(static_cast<int>(avail.width() * kWidthRatio), kMinWidth, kMaxWidth);
const int h = std::clamp(static_cast<int>(avail.height() * kHeightRatio), kMinHeight, kMaxHeight);
const int x = avail.center().x() - w / 2;
const int y = avail.center().y() - h / 2;

setGeometry(x, y, w, h);
show();
raise();
}

void AboutPanel::hidePanel() {
hide();
}

void AboutPanel::paintEvent(QPaintEvent* /*event*/) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing, true);

// Hardcoded Material-ish palette; this demo panel does not track the theme.
const QColor surface_color(0xF7, 0xF5, 0xF3);
const QColor text_color(0x1C, 0x1B, 0x1F);
const QColor hint_color(0x49, 0x45, 0x4E);

QPainterPath surface;
surface.addRoundedRect(QRectF(rect()), kCornerRadius, kCornerRadius);
p.fillPath(surface, surface_color);

// Version (title), centered in the upper portion.
QFont title_font = font();
title_font.setPointSize(18);
title_font.setBold(true);
p.setFont(title_font);
p.setPen(text_color);
p.drawText(QRect(0, 0, width(), height() * 2 / 3), Qt::AlignCenter, version_text_);

// Target line + dismiss hint, centered in the lower portion.
QFont body_font = font();
body_font.setPointSize(11);
p.setFont(body_font);
p.setPen(hint_color);
p.drawText(QRect(0, height() / 2, width(), height() / 4), Qt::AlignCenter, target_text_);
p.drawText(QRect(0, height() * 3 / 4, width(), height() / 6), Qt::AlignCenter,
QStringLiteral("(tap to close)"));
}

void AboutPanel::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
hidePanel();
}
QWidget::mouseReleaseEvent(event);
}

} // namespace cf::desktop::desktop_component
Loading
Loading