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
52 changes: 33 additions & 19 deletions desktop/ui/CFDesktopEntity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
#include "components/PanelManager.h"
#include "components/WindowManager.h"
#include "components/launcher/app_launch_service.h"
#include "components/launcher/app_launcher.h"
#include "components/statusbar/status_bar.h"
#include "components/taskbar/centered_taskbar.h"
#include "platform/DesktopPropertyStrategyFactory.h"
#include "platform/display_backend_helper.h"
#include "platform/shell_layer_helper.h"
#include "qt_format.h"
#include <QHash>
#include <functional>
#include <memory>

namespace cf::desktop {
Expand Down Expand Up @@ -130,26 +132,38 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) {
auto* taskbar = new cf::desktop::desktop_component::CenteredTaskbar(desktop_entity_);
taskbar->setApps(apps);
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;
}
}
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;
}
};
QObject::connect(taskbar, &cf::desktop::desktop_component::CenteredTaskbar::appClicked, this,
[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;
}
}
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;
}
});
launch_app);

// ── App launcher popup (Start-menu), opened from the taskbar start button ──
auto* app_launcher = new cf::desktop::desktop_component::AppLauncher(desktop_entity_);
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]() { app_launcher->popup(panel_mgr->availableGeometry()); });
taskbar->show();
panel_mgr->relayout();

Expand Down
6 changes: 5 additions & 1 deletion desktop/ui/components/launcher/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# App launcher service (QProcess-based external application launching).
add_library(cfdesktop_launcher STATIC
app_launch_service.cpp
app_launcher.cpp
launcher_tile.cpp
)

target_include_directories(cfdesktop_launcher PUBLIC
Expand All @@ -13,6 +15,8 @@ target_link_libraries(
cfdesktop_launcher
PRIVATE
Qt6::Core # QProcess, QStringList
cfbase # aex::expected (propagated via cfbase -> aex::aex)
Qt6::Widgets # QWidget, QGridLayout, QPainter
QuarkWidgets::quarkwidgets # ThemeManager + Material tokens for the popup/tiles
cfbase # aex::expected / WeakPtrFactory (propagated via cfbase -> aex::aex)
cflogger # Diagnostic logging for launch success/failure
)
166 changes: 166 additions & 0 deletions desktop/ui/components/launcher/app_launcher.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* @file app_launcher.cpp
* @brief Start-menu style application launcher popup implementation.
*
* Renders a frameless rounded Material surface holding a grid of LauncherTile
* entries. popup() sizes and centers it above the taskbar; tile clicks forward
* appLaunched() and dismiss the popup; ESC and outside clicks (Qt::Popup) also
* dismiss it. All rendering is QPainter-native.
*
* @author Charliechen114514 (chengh1922@mails.jlu.edu.cn)
* @date 2026-06-26
* @version 0.1
* @since 0.20
* @ingroup components
*/

#include "app_launcher.h"

#include "launcher_tile.h"

#include "core/theme_manager.h"
#include "core/token/material_scheme/cfmaterial_token_literals.h"

#include <QGridLayout>
#include <QGuiApplication>
#include <QKeyEvent>
#include <QPaintEvent>
#include <QPainter>
#include <QPainterPath>
#include <QScreen>

#include <algorithm>

namespace cf::desktop::desktop_component {

using namespace qw::core::token::literals;

namespace {
constexpr int kMaxColumns = 5; ///< Maximum tiles per grid row.
constexpr int kGridSpacing = 12; ///< Gap between tiles (px).
constexpr int kMargin = 24; ///< Popup inner margin (px).
constexpr qreal kCornerRadius = 16; ///< Popup corner radius (px).
constexpr qreal kWidthRatio = 0.5; ///< Popup width as a fraction of available.
constexpr qreal kHeightRatio = 0.55; ///< Popup height as a fraction of available.
constexpr int kMinWidth = 480; ///< Minimum popup width (px).
constexpr int kMaxWidth = 720; ///< Maximum popup width (px).
constexpr int kMinHeight = 360; ///< Minimum popup height (px).
constexpr int kMaxHeight = 540; ///< Maximum popup height (px).
} // namespace

AppLauncher::AppLauncher(QWidget* parent) : QWidget(parent) {
setWindowFlags(Qt::Popup | Qt::FramelessWindowHint);
setAttribute(Qt::WA_TranslucentBackground);
setAttribute(Qt::WA_OpaquePaintEvent, false);
setAutoFillBackground(false);
setupUi();
applyTheme();

// Follow live theme switches (ThemeManager is the canonical source).
connect(&qw::core::ThemeManager::instance(), &qw::core::ThemeManager::themeChanged, this,
[this](const qw::core::ICFTheme&) { applyTheme(); });
}

AppLauncher::~AppLauncher() = default;

void AppLauncher::setApps(const QList<AppEntry>& apps) {
apps_ = apps;
rebuildGrid();
}

void AppLauncher::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.bottom() - h; // Bottom-aligned: sits just above the taskbar.

setFixedSize(w, h);
move(x, y);
show();
raise();
activateWindow();
}

void AppLauncher::hideLauncher() {
hide();
}

bool AppLauncher::isShowing() const noexcept {
return isVisible();
}

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

// Rounded Material surface; the area outside the path stays transparent.
QPainterPath surface;
surface.addRoundedRect(QRectF(rect()), kCornerRadius, kCornerRadius);
p.fillPath(surface, surface_color_);
}

void AppLauncher::keyPressEvent(QKeyEvent* event) {
if (event->key() == Qt::Key_Escape) {
hideLauncher();
return;
}
QWidget::keyPressEvent(event);
}

// -- Internal --------------------------------------------------------------
void AppLauncher::setupUi() {
grid_ = new QGridLayout(this);
grid_->setSpacing(kGridSpacing);
grid_->setContentsMargins(kMargin, kMargin, kMargin, kMargin);
grid_->setAlignment(Qt::AlignTop | Qt::AlignHCenter);
}

void AppLauncher::applyTheme() {
try {
auto& tm = qw::core::ThemeManager::instance();
const auto& theme = tm.theme(tm.currentThemeName());
auto& cs = theme.color_scheme();
surface_color_ = cs.queryColor(SURFACE);
outline_color_ = cs.queryColor(OUTLINE_VARIANT);
} catch (...) {
// Fallback palette when no theme is registered yet.
surface_color_ = QColor(0xF7, 0xF5, 0xF3);
outline_color_ = QColor(0xCA, 0xC4, 0xD0);
}
update();
}

void AppLauncher::rebuildGrid() {
qDeleteAll(tiles_);
tiles_.clear();

const int n = apps_.size();
const int cols = std::max(1, std::min(kMaxColumns, n));
int row = 0;
int col = 0;
for (const auto& app : apps_) {
auto* tile = new LauncherTile(app, this);
connect(tile, &LauncherTile::clicked, this, [this](const QString& app_id) {
emit appLaunched(app_id);
hideLauncher();
});
grid_->addWidget(tile, row, col);
tiles_.append(tile);
++col;
if (col >= cols) {
col = 0;
++row;
}
}
}

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