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
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ 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")
Expand Down
5 changes: 5 additions & 0 deletions apps/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Standalone desktop apps.
# Each entry is an independent executable that CFDesktop launches via
# AppLaunchService (QProcess) — they are not linked into the desktop shell,
# so a crash or hang in an app cannot take down the desktop.
add_subdirectory(calculator)
40 changes: 40 additions & 0 deletions apps/calculator/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Calculator standalone application.
# Ported from CCIMXDesktop's Caculator; UI uses cfui MD3 widgets.
# CFDesktop launches this binary via AppLaunchService (QProcess), so it runs in
# its own process (isolated from the desktop shell).

# Expression parser library (QString-based; lives outside base/ because base/
# must not depend on Qt).
add_library(cfdesktop_calculator_parser STATIC
core/Parser.cpp
core/NumberNode.cpp
core/BinaryOpTreeNode.cpp
core/UnaryOpTreeNode.cpp
core/FunctorTreeNode.cpp
core/ExpressionEvaluator.cpp
)

target_include_directories(cfdesktop_calculator_parser PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/core>
)

target_link_libraries(cfdesktop_calculator_parser PUBLIC Qt6::Core)

# Standalone Calculator executable (consumed by CFDesktop via QProcess launch).
qt_add_executable(calculator
main.cpp
calculator_panel.cpp
)

target_link_libraries(calculator PRIVATE
cfdesktop_calculator_parser
QuarkWidgets::quarkwidgets
Qt6::Widgets
)

# Place next to the CFDesktop binary so AppLaunchService's applicationDirPath()
# resolution finds it (exec_command "calculator" -> <bin>/calculator).
set_target_properties(calculator PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
151 changes: 151 additions & 0 deletions apps/calculator/calculator_panel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* @file apps/calculator/calculator_panel.cpp
* @brief Implementation of the Calculator panel.
*
* @author Charliechen114514 (chengh1922@mails.jlu.edu.cn)
* @date 2026-06-30
* @version 0.1
* @since 0.20
* @ingroup calculator
*/

#include "calculator_panel.h"

#include "core/ExpressionEvaluator.h"

#include "ui/widget/material/widget/button/button.h"
#include "ui/widget/material/widget/label/label.h"

#include <QGridLayout>
#include <QKeyEvent>
#include <QPainter>
#include <QPainterPath>
#include <QVBoxLayout>

#include <exception>

namespace cf::desktop::desktop_component {

namespace {
using calculator_core::ExpressionEvaluator::evalute_expression;
using qw::widget::material::Button;
using qw::widget::material::Label;
using qw::widget::material::TypographyStyle;
using Variant = Button::ButtonVariant;

constexpr qreal kCornerRadius = 16.0; ///< Card corner radius (px).

/// @brief Button definition: text + MD3 variant, laid out row-major in 4 cols.
struct BtnDef {
QString text;
Variant variant;
};

/// Keypad layout: digits + operators + scientific functions + CLR/DEL.
const QVector<BtnDef> kButtons = {
{"7", Variant::Filled}, {"8", Variant::Filled}, {"9", Variant::Filled},
{"/", Variant::Tonal}, {"4", Variant::Filled}, {"5", Variant::Filled},
{"6", Variant::Filled}, {"*", Variant::Tonal}, {"1", Variant::Filled},
{"2", Variant::Filled}, {"3", Variant::Filled}, {"-", Variant::Tonal},
{"0", Variant::Filled}, {".", Variant::Filled}, {"=", Variant::Filled},
{"+", Variant::Tonal}, {"sin", Variant::Outlined}, {"cos", Variant::Outlined},
{"tan", Variant::Outlined}, {"sqrt", Variant::Outlined}, {"log", Variant::Outlined},
{"exp", Variant::Outlined}, {"(", Variant::Tonal}, {")", Variant::Tonal},
{"^", Variant::Tonal}, {"CLR", Variant::Text}, {"DEL", Variant::Text},
{"", Variant::Text},
};

/// @brief Returns true if @p token is a named function (auto-appends '(').
bool isFunction(const QString& token) {
return token == "sin" || token == "cos" || token == "tan" || token == "sqrt" ||
token == "log" || token == "exp";
}
} // namespace

CalculatorPanel::CalculatorPanel(QWidget* parent) : QWidget(parent) {
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(16, 16, 16, 16);
layout->setSpacing(12);

display_ = new Label("0", TypographyStyle::DisplayMedium, this);
display_->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
layout->addWidget(display_);

auto* grid = new QGridLayout;
grid->setSpacing(8);
for (int i = 0; i < kButtons.size(); ++i) {
const auto& def = kButtons[i];
if (def.text.isEmpty()) {
continue;
}
auto* btn = new Button(def.text, def.variant, this);
const QString token = def.text;
connect(btn, &QPushButton::clicked, this, [this, token]() { onButton(token); });
grid->addWidget(btn, i / 4, i % 4);
}
layout->addLayout(grid);
}

CalculatorPanel::~CalculatorPanel() = default;

void CalculatorPanel::onButton(const QString& text) {
if (text == "=") {
try {
const double result = evalute_expression(expression_);
expression_ = QString::number(result);
} catch (const std::exception& e) {
// No silent fallback: show the parser's error message.
expression_ = QString::fromUtf8(e.what());
}
} else if (text == "CLR") {
expression_.clear();
} else if (text == "DEL") {
expression_.chop(1);
} else if (isFunction(text)) {
expression_ += text + "(";
} else {
expression_ += text;
}
refreshDisplay();
}

void CalculatorPanel::refreshDisplay() {
display_->setText(expression_.isEmpty() ? QStringLiteral("0") : expression_);
}

void CalculatorPanel::paintEvent(QPaintEvent* /*event*/) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing, true);
const QColor surface_color(0xF7, 0xF5, 0xF3);
QPainterPath card;
card.addRoundedRect(QRectF(rect()), kCornerRadius, kCornerRadius);
p.fillPath(card, surface_color);
}

void CalculatorPanel::keyPressEvent(QKeyEvent* event) {
const int key = event->key();
if (key == Qt::Key_Backspace) {
onButton("DEL");
return;
}
if (key == Qt::Key_Return || key == Qt::Key_Enter) {
onButton("=");
return;
}
if (key == Qt::Key_Escape) {
close();
return;
}
const QString ch = event->text();
if (ch.length() == 1) {
const QChar c = ch[0];
if (c.isDigit() || QString("+-*/^().").contains(c)) {
expression_ += ch;
refreshDisplay();
return;
}
}
QWidget::keyPressEvent(event);
}

} // namespace cf::desktop::desktop_component
91 changes: 91 additions & 0 deletions apps/calculator/calculator_panel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* @file apps/calculator/calculator_panel.h
* @brief Calculator main panel (standalone app).
*
* The root widget of the standalone Calculator executable. Renders the
* keypad with cfui MD3 widgets (Button grid + Label display) and reuses the
* ported calculator_core expression parser. CFDesktop launches this binary
* via AppLaunchService (QProcess), so it runs in its own process.
*
* @author Charliechen114514 (chengh1922@mails.jlu.edu.cn)
* @date 2026-06-30
* @version 0.1
* @since 0.20
* @ingroup calculator
*/

#pragma once

#include <QString>
#include <QWidget>

class QKeyEvent;
class QPaintEvent;

namespace qw::widget::material {
class Label;
}

namespace cf::desktop::desktop_component {

/**
* @brief Root widget of the standalone Calculator application.
*
* @ingroup calculator
*/
class CalculatorPanel final : public QWidget {
Q_OBJECT
public:
/**
* @brief Constructs the Calculator panel.
* @param[in] parent Parent widget (nullptr for a top-level window).
* @throws None.
* @since 0.20
* @ingroup calculator
*/
explicit CalculatorPanel(QWidget* parent = nullptr);

/**
* @brief Destructs the panel.
* @throws None.
* @since 0.20
* @ingroup calculator
*/
~CalculatorPanel() override;

protected:
/**
* @brief Paints the rounded Material card background.
* @throws None.
* @since 0.20
* @ingroup calculator
*/
void paintEvent(QPaintEvent* event) override;

/**
* @brief Dispatches keyboard input to the calculator.
* @param[in] event The key event.
* @throws None.
* @since 0.20
* @ingroup calculator
*/
void keyPressEvent(QKeyEvent* event) override;

private:
/**
* @brief Handles a button token (append, evaluate, clear, etc.).
* @param[in] text The button text.
* @throws None internally; parser exceptions are caught and shown.
* @since 0.20
* @ingroup calculator
*/
void onButton(const QString& text);

/// @brief Syncs the display label with the current expression.
void refreshDisplay();

qw::widget::material::Label* display_{nullptr}; ///< Read-out display.
QString expression_; ///< Current expression string.
};

} // namespace cf::desktop::desktop_component
50 changes: 50 additions & 0 deletions apps/calculator/core/BinaryOpTreeNode.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* @file desktop/ui/components/builtin_apps/calculator/core/BinaryOpTreeNode.cpp
* @brief Implementation of BinaryOpTreeNode.
*
* @author Charliechen114514 (chengh1922@mails.jlu.edu.cn)
* @date 2026-06-30
* @version 0.1
* @since 0.20
* @ingroup calculator
*/

#include "BinaryOpTreeNode.h"

#include <QMap>
#include <cmath>
#include <functional>

namespace cf::desktop::desktop_component::calculator_core {

namespace {
/// Mapping of operator character to the binary arithmetic lambda.
const QMap<QChar, std::function<double(double, double)>> kMappings = {
{'+', [](double a, double b) { return a + b; }},
{'-', [](double a, double b) { return a - b; }},
{'*', [](double a, double b) { return a * b; }},
{'/', [](double a, double b) { return b != 0 ? a / b : throw DivideZeroException(); }},
{'^', [](double a, double b) { return std::pow(a, b); }},
};
} // namespace

BinaryOpTreeNode::BinaryOpTreeNode(const QChar op, TreeNodeBase* left_hand,
TreeNodeBase* right_hand)
: op(op), left_hand(left_hand), right_hand(right_hand) {}

BinaryOpTreeNode::~BinaryOpTreeNode() {
delete left_hand;
delete right_hand;
}

double BinaryOpTreeNode::evaluate() const {
const double l = left_hand->evaluate();
const double r = right_hand->evaluate();
const auto it = kMappings.find(op);
if (it != kMappings.end()) {
return it.value()(l, r);
}
throw UnSupportedSymbol(QString(op));
}

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