From 0de918d7bcf7efd3ee9d79f5cae41fa41d5b9f5a Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Tue, 30 Jun 2026 22:29:27 +0800 Subject: [PATCH] feat(calculator): standalone Calculator app launched via QProcess Establishes the "tool app -> standalone executable + QProcess launch" pattern for Phase 13 apps, complementing the builtin child-panel pattern used by About. The Calculator runs in its own process, isolated from the desktop shell. - apps/calculator: ported CCIMXDesktop Caculator. Parser (recursive-descent AST + ExpressionEvaluator) kept QString-based as the cfdesktop_calculator_parser static lib; UI rewritten with cfui MD3 (Button grid + Label display); main.cpp runs it as a top-level window. - AppLaunchService: resolve a bare program name against applicationDirPath() first (so "calculator" finds /calculator), then fall back to PATH for external commands (xdg-open, xterm). - defaultApps: calculator entry with exec_command "calculator". - Top-level CMakeLists adds apps/; calculator binary placed next to CFDesktop so the launch resolution finds it. - Tests: parser_test ports 53 upstream cases to GoogleTest (15 grouped). --- CMakeLists.txt | 5 + apps/CMakeLists.txt | 5 + apps/calculator/CMakeLists.txt | 40 +++++ apps/calculator/calculator_panel.cpp | 151 ++++++++++++++++++ apps/calculator/calculator_panel.h | 91 +++++++++++ apps/calculator/core/BinaryOpTreeNode.cpp | 50 ++++++ apps/calculator/core/BinaryOpTreeNode.h | 58 +++++++ apps/calculator/core/ExpressionEvaluator.cpp | 32 ++++ apps/calculator/core/ExpressionEvaluator.h | 38 +++++ apps/calculator/core/FunctorTreeNode.cpp | 56 +++++++ apps/calculator/core/FunctorTreeNode.h | 56 +++++++ apps/calculator/core/NumberNode.cpp | 22 +++ apps/calculator/core/NumberNode.h | 60 +++++++ apps/calculator/core/ParseExceptions.h | 139 ++++++++++++++++ apps/calculator/core/Parser.cpp | 149 +++++++++++++++++ apps/calculator/core/Parser.h | 100 ++++++++++++ apps/calculator/core/TreeNodeBase.h | 41 +++++ apps/calculator/core/UnaryOpTreeNode.cpp | 31 ++++ apps/calculator/core/UnaryOpTreeNode.h | 56 +++++++ apps/calculator/main.cpp | 27 ++++ desktop/ui/components/app_entry.h | 2 + .../launcher/app_launch_service.cpp | 16 +- document/status/current.md | 1 + document/todo/desktop/13_widget_apps.md | 1 + test/desktop/CMakeLists.txt | 3 + test/desktop/calculator/CMakeLists.txt | 8 + test/desktop/calculator/parser_test.cpp | 130 +++++++++++++++ 27 files changed, 1367 insertions(+), 1 deletion(-) create mode 100644 apps/CMakeLists.txt create mode 100644 apps/calculator/CMakeLists.txt create mode 100644 apps/calculator/calculator_panel.cpp create mode 100644 apps/calculator/calculator_panel.h create mode 100644 apps/calculator/core/BinaryOpTreeNode.cpp create mode 100644 apps/calculator/core/BinaryOpTreeNode.h create mode 100644 apps/calculator/core/ExpressionEvaluator.cpp create mode 100644 apps/calculator/core/ExpressionEvaluator.h create mode 100644 apps/calculator/core/FunctorTreeNode.cpp create mode 100644 apps/calculator/core/FunctorTreeNode.h create mode 100644 apps/calculator/core/NumberNode.cpp create mode 100644 apps/calculator/core/NumberNode.h create mode 100644 apps/calculator/core/ParseExceptions.h create mode 100644 apps/calculator/core/Parser.cpp create mode 100644 apps/calculator/core/Parser.h create mode 100644 apps/calculator/core/TreeNodeBase.h create mode 100644 apps/calculator/core/UnaryOpTreeNode.cpp create mode 100644 apps/calculator/core/UnaryOpTreeNode.h create mode 100644 apps/calculator/main.cpp create mode 100644 test/desktop/calculator/CMakeLists.txt create mode 100644 test/desktop/calculator/parser_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c26ac2f39..059ed2645 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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") diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt new file mode 100644 index 000000000..f009adf28 --- /dev/null +++ b/apps/CMakeLists.txt @@ -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) diff --git a/apps/calculator/CMakeLists.txt b/apps/calculator/CMakeLists.txt new file mode 100644 index 000000000..3cd7845e0 --- /dev/null +++ b/apps/calculator/CMakeLists.txt @@ -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 + $ + $ +) + +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" -> /calculator). +set_target_properties(calculator PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) diff --git a/apps/calculator/calculator_panel.cpp b/apps/calculator/calculator_panel.cpp new file mode 100644 index 000000000..5f65895cd --- /dev/null +++ b/apps/calculator/calculator_panel.cpp @@ -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 +#include +#include +#include +#include + +#include + +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 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 diff --git a/apps/calculator/calculator_panel.h b/apps/calculator/calculator_panel.h new file mode 100644 index 000000000..a60f0ccea --- /dev/null +++ b/apps/calculator/calculator_panel.h @@ -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 +#include + +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 diff --git a/apps/calculator/core/BinaryOpTreeNode.cpp b/apps/calculator/core/BinaryOpTreeNode.cpp new file mode 100644 index 000000000..8cc4544e6 --- /dev/null +++ b/apps/calculator/core/BinaryOpTreeNode.cpp @@ -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 +#include +#include + +namespace cf::desktop::desktop_component::calculator_core { + +namespace { +/// Mapping of operator character to the binary arithmetic lambda. +const QMap> 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 diff --git a/apps/calculator/core/BinaryOpTreeNode.h b/apps/calculator/core/BinaryOpTreeNode.h new file mode 100644 index 000000000..5486dd008 --- /dev/null +++ b/apps/calculator/core/BinaryOpTreeNode.h @@ -0,0 +1,58 @@ +/** + * @file desktop/ui/components/builtin_apps/calculator/core/BinaryOpTreeNode.h + * @brief AST node for a binary operation (+, -, *, /, ^). + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#pragma once + +#include "ParseExceptions.h" +#include "TreeNodeBase.h" +#include + +namespace cf::desktop::desktop_component::calculator_core { + +/** + * @brief AST node applying a binary operator to two subtrees. + * + * Owns its operand nodes and deletes them on destruction. + * + * @ingroup calculator + */ +struct BinaryOpTreeNode : TreeNodeBase { + /** + * @brief Constructs the node. + * @param[in] op The operator character (+, -, *, /, ^). + * @param[in] left_hand The left operand subtree. Ownership transfers. + * @param[in] right_hand The right operand subtree. Ownership transfers. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + BinaryOpTreeNode(QChar op, TreeNodeBase* left_hand, TreeNodeBase* right_hand); + + BinaryOpTreeNode() = delete; + ~BinaryOpTreeNode() override; + + /** + * @brief Evaluates both operands and applies the operator. + * @return The computed value. + * @throws DivideZeroException on division by zero; UnSupportedSymbol on + * an unknown operator. + * @since 0.20 + * @ingroup calculator + */ + double evaluate() const override; + + private: + QChar op; ///< The operator character. + TreeNodeBase* left_hand; ///< Left operand subtree (owned). + TreeNodeBase* right_hand; ///< Right operand subtree (owned). +}; + +} // namespace cf::desktop::desktop_component::calculator_core diff --git a/apps/calculator/core/ExpressionEvaluator.cpp b/apps/calculator/core/ExpressionEvaluator.cpp new file mode 100644 index 000000000..18c80604a --- /dev/null +++ b/apps/calculator/core/ExpressionEvaluator.cpp @@ -0,0 +1,32 @@ +/** + * @file desktop/ui/components/builtin_apps/calculator/core/ExpressionEvaluator.cpp + * @brief Implementation of the expression evaluator facade. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#include "ExpressionEvaluator.h" + +#include "Parser.h" +#include "TreeNodeBase.h" + +#include + +namespace cf::desktop::desktop_component::calculator_core { + +namespace ExpressionEvaluator { + +double evalute_expression(const QString& expr) { + Parser parser; + parser.setParserString(expr); + std::unique_ptr root(parser.parse()); + return root->evaluate(); +} + +} // namespace ExpressionEvaluator + +} // namespace cf::desktop::desktop_component::calculator_core diff --git a/apps/calculator/core/ExpressionEvaluator.h b/apps/calculator/core/ExpressionEvaluator.h new file mode 100644 index 000000000..213ff7839 --- /dev/null +++ b/apps/calculator/core/ExpressionEvaluator.h @@ -0,0 +1,38 @@ +/** + * @file desktop/ui/components/builtin_apps/calculator/core/ExpressionEvaluator.h + * @brief High-level facade that parses and evaluates an expression string. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#pragma once + +#include + +namespace cf::desktop::desktop_component::calculator_core { + +/** + * @brief High-level facade for parsing and evaluating an expression. + * + * @ingroup calculator + */ +namespace ExpressionEvaluator { + +/** + * @brief Parses and evaluates @p expr in one shot. + * @param[in] expr The expression string (e.g. "1 + 2 * sin(0)"). + * @return The computed value. + * @throws Parser exceptions on malformed input or evaluation errors + * (div-by-zero, unsupported symbol/function, bad sqrt, ...). + * @since 0.20 + * @ingroup calculator + */ +double evalute_expression(const QString& expr); + +} // namespace ExpressionEvaluator + +} // namespace cf::desktop::desktop_component::calculator_core diff --git a/apps/calculator/core/FunctorTreeNode.cpp b/apps/calculator/core/FunctorTreeNode.cpp new file mode 100644 index 000000000..26f75e2ce --- /dev/null +++ b/apps/calculator/core/FunctorTreeNode.cpp @@ -0,0 +1,56 @@ +/** + * @file desktop/ui/components/builtin_apps/calculator/core/FunctorTreeNode.cpp + * @brief Implementation of FunctorTreeNode. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#include "FunctorTreeNode.h" + +#include "ParseExceptions.h" + +#include +#include +#include + +namespace cf::desktop::desktop_component::calculator_core { + +namespace { +/// Mapping of function name to the single-argument math lambda. +const QMap> kFunctors = { + {"sin", [](double x) { return std::sin(x); }}, + {"cos", [](double x) { return std::cos(x); }}, + {"tan", [](double x) { return std::tan(x); }}, + {"sqrt", + [](double x) { + if (x < 0) { + throw BadSqrtValue(); + } + return std::sqrt(x); + }}, + {"log", [](double x) { return std::log(x); }}, + {"exp", [](double x) { return std::exp(x); }}, +}; +} // namespace + +FunctorTreeNode::FunctorTreeNode(QString name, TreeNodeBase* arg) + : name(std::move(name)), argument(arg) {} + +FunctorTreeNode::~FunctorTreeNode() { + delete argument; +} + +double FunctorTreeNode::evaluate() const { + const double val = argument->evaluate(); + const auto it = kFunctors.find(name); + if (it != kFunctors.end()) { + return it.value()(val); + } + throw UnSupportiveFunction(name); +} + +} // namespace cf::desktop::desktop_component::calculator_core diff --git a/apps/calculator/core/FunctorTreeNode.h b/apps/calculator/core/FunctorTreeNode.h new file mode 100644 index 000000000..a306ae630 --- /dev/null +++ b/apps/calculator/core/FunctorTreeNode.h @@ -0,0 +1,56 @@ +/** + * @file desktop/ui/components/builtin_apps/calculator/core/FunctorTreeNode.h + * @brief AST node for a named function call (sin, cos, sqrt, ...). + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#pragma once + +#include "TreeNodeBase.h" +#include + +namespace cf::desktop::desktop_component::calculator_core { + +/** + * @brief AST node applying a named function to one argument subtree. + * + * Supports sin, cos, tan, sqrt, log, exp. Owns its argument node and + * deletes it on destruction. + * + * @ingroup calculator + */ +class FunctorTreeNode : public TreeNodeBase { + public: + /** + * @brief Constructs the node. + * @param[in] name The function name (e.g. "sin"). + * @param[in] arg The argument subtree. Ownership transfers. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + FunctorTreeNode(QString name, TreeNodeBase* arg); + + ~FunctorTreeNode() override; + + /** + * @brief Evaluates the argument and applies the function. + * @return The computed value. + * @throws UnSupportiveFunction if the name is unknown; BadSqrtValue if + * sqrt receives a negative value. + * @since 0.20 + * @ingroup calculator + */ + double evaluate() const override; + + private: + QString name; ///< The function name. + TreeNodeBase* argument; ///< The argument subtree (owned). +}; + +} // namespace cf::desktop::desktop_component::calculator_core diff --git a/apps/calculator/core/NumberNode.cpp b/apps/calculator/core/NumberNode.cpp new file mode 100644 index 000000000..940f193d1 --- /dev/null +++ b/apps/calculator/core/NumberNode.cpp @@ -0,0 +1,22 @@ +/** + * @file desktop/ui/components/builtin_apps/calculator/core/NumberNode.cpp + * @brief Implementation of NumberNode. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#include "NumberNode.h" + +namespace cf::desktop::desktop_component::calculator_core { + +NumberNode::NumberNode(const double val) : stored_value(val) {} + +double NumberNode::evaluate() const { + return stored_value; +} + +} // namespace cf::desktop::desktop_component::calculator_core diff --git a/apps/calculator/core/NumberNode.h b/apps/calculator/core/NumberNode.h new file mode 100644 index 000000000..70f986828 --- /dev/null +++ b/apps/calculator/core/NumberNode.h @@ -0,0 +1,60 @@ +/** + * @file desktop/ui/components/builtin_apps/calculator/core/NumberNode.h + * @brief AST leaf node holding a numeric literal. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#pragma once + +#include "TreeNodeBase.h" + +namespace cf::desktop::desktop_component::calculator_core { + +/** + * @brief AST leaf node holding a numeric literal. + * + * @ingroup calculator + */ +struct NumberNode : TreeNodeBase { + /** + * @brief Constructs the node with value @p val. + * @param[in] val The numeric value to store. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + explicit NumberNode(double val); + + NumberNode() = delete; + NumberNode(const NumberNode&) = delete; + NumberNode& operator=(const NumberNode&) = delete; + NumberNode(NumberNode&&) = delete; + NumberNode& operator=(NumberNode&&) = delete; + + /** + * @brief Destructs the node. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + ~NumberNode() override = default; + + /** + * @brief Returns the stored value. + * @return The numeric value. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + double evaluate() const override; + + private: + double stored_value{0.0}; ///< The literal value. +}; + +} // namespace cf::desktop::desktop_component::calculator_core diff --git a/apps/calculator/core/ParseExceptions.h b/apps/calculator/core/ParseExceptions.h new file mode 100644 index 000000000..19e8da9a6 --- /dev/null +++ b/apps/calculator/core/ParseExceptions.h @@ -0,0 +1,139 @@ +/** + * @file desktop/ui/components/builtin_apps/calculator/core/ParseExceptions.h + * @brief Exception types thrown by the calculator expression parser. + * + * Ported from CCIMXDesktop's Caculator; parse logic unchanged, wrapped in + * the calculator_core namespace. Each exception forwards its message to + * std::runtime_error, so what() returns the constructed string directly. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#pragma once + +#include +#include + +namespace cf::desktop::desktop_component::calculator_core { + +/** + * @brief Thrown when a division by zero is attempted. + * + * @ingroup calculator + */ +class DivideZeroException : public std::runtime_error { + public: + /** + * @brief Constructs the exception with a fixed message. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + DivideZeroException() : std::runtime_error("Divide Zero is not permitted!") {} +}; + +/** + * @brief Thrown when an unsupported symbol is encountered during parsing. + * + * @ingroup calculator + */ +class UnSupportedSymbol : public std::runtime_error { + public: + /** + * @brief Constructs the exception for @p symbol. + * @param[in] symbol The unsupported symbol. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + explicit UnSupportedSymbol(const QString& symbol) + : std::runtime_error(("Meeting unsolved symbol: " + symbol).toStdString()) {} +}; + +/** + * @brief Thrown when a numeric token cannot be parsed. + * + * @ingroup calculator + */ +class InvalidNumber : public std::runtime_error { + public: + /** + * @brief Constructs the exception with a fixed message. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + InvalidNumber() : std::runtime_error("Meeting InvalidNumber!") {} +}; + +/** + * @brief Thrown when an expression has unbalanced parentheses or structure. + * + * @ingroup calculator + */ +class UnSymmetryExpression : public std::runtime_error { + public: + /** + * @brief Constructs the exception with a fixed message. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + UnSymmetryExpression() : std::runtime_error("UnSymmetry Expression!") {} +}; + +/** + * @brief General-purpose exception for parse errors not covered by other types. + * + * @ingroup calculator + */ +class GeneralParseError : public std::runtime_error { + public: + /** + * @brief Constructs the exception with a fixed message. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + GeneralParseError() : std::runtime_error("Some Parse Error!") {} +}; + +/** + * @brief Thrown when an unsupported function name is encountered. + * + * @ingroup calculator + */ +class UnSupportiveFunction : public std::runtime_error { + public: + /** + * @brief Constructs the exception for @p function. + * @param[in] function The unsupported function name. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + explicit UnSupportiveFunction(const QString& function) + : std::runtime_error(("Meeting unsolved function: " + function).toStdString()) {} +}; + +/** + * @brief Thrown when sqrt receives a negative value. + * + * @ingroup calculator + */ +class BadSqrtValue : public std::runtime_error { + public: + /** + * @brief Constructs the exception with a fixed message. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + BadSqrtValue() : std::runtime_error("value sqrt is less then 0, which is not allowed!") {} +}; + +} // namespace cf::desktop::desktop_component::calculator_core diff --git a/apps/calculator/core/Parser.cpp b/apps/calculator/core/Parser.cpp new file mode 100644 index 000000000..b03a04fa9 --- /dev/null +++ b/apps/calculator/core/Parser.cpp @@ -0,0 +1,149 @@ +/** + * @file desktop/ui/components/builtin_apps/calculator/core/Parser.cpp + * @brief Implementation of the recursive-descent parser. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#include "Parser.h" + +#include "BinaryOpTreeNode.h" +#include "FunctorTreeNode.h" +#include "NumberNode.h" +#include "ParseExceptions.h" +#include "UnaryOpTreeNode.h" + +namespace cf::desktop::desktop_component::calculator_core { + +void Parser::setParserString(const QString& p) { + handle_expression = p; + parse_pos = 0; +} + +QString Parser::parserString() const { + return handle_expression; +} + +TreeNodeBase* Parser::parse() { + TreeNodeBase* node = parseExpression(); + if (parse_pos < handle_expression.length()) { + throw GeneralParseError(); + } + return node; +} + +QChar Parser::peekPos() { + return parse_pos < handle_expression.length() ? handle_expression[parse_pos] : QChar(); +} + +QChar Parser::getChar() { + return parse_pos < handle_expression.length() ? handle_expression[parse_pos++] : QChar(); +} + +void Parser::skipIgnored() { + while (peekPos().isSpace()) { + ++parse_pos; + } +} + +TreeNodeBase* Parser::parseExpression() { + TreeNodeBase* node = parseTerm(); + while (true) { + skipIgnored(); + const QChar op = peekPos(); + if (op == '+' || op == '-') { + getChar(); + TreeNodeBase* rhs = parseTerm(); + node = new BinaryOpTreeNode(op, node, rhs); + } else { + break; + } + } + return node; +} + +TreeNodeBase* Parser::parseTerm() { + TreeNodeBase* node = parseFactor(); + while (true) { + skipIgnored(); + const QChar op = peekPos(); + if (op == '*' || op == '/' || op == '^') { + getChar(); + TreeNodeBase* rhs = parseFactor(); + node = new BinaryOpTreeNode(op, node, rhs); + } else { + break; + } + } + return node; +} + +TreeNodeBase* Parser::parseFactor() { + skipIgnored(); + const QChar ch = peekPos(); + + if (ch == '-') { + getChar(); + TreeNodeBase* node = parseFactor(); + return new UnaryOpTreeNode('-', node); + } + + if (ch.isLetter()) { + const QString func = parseIdentifier(); + skipIgnored(); + if (getChar() != '(') { + throw UnSymmetryExpression(); + } + TreeNodeBase* arg = parseExpression(); + skipIgnored(); + if (getChar() != ')') { + throw UnSymmetryExpression(); + } + return new FunctorTreeNode(func, arg); + } + + if (ch == '(') { + getChar(); + TreeNodeBase* node = parseExpression(); + skipIgnored(); + if (getChar() != ')') { + throw UnSymmetryExpression(); + } + return node; + } + + if (ch.isDigit() || ch == '.') { + return parseNumber(); + } + + throw UnSupportedSymbol(QString(ch)); +} + +TreeNodeBase* Parser::parseNumber() { + skipIgnored(); + const int start = parse_pos; + while (peekPos().isDigit() || peekPos() == '.') { + ++parse_pos; + } + bool ok = false; + const double val = handle_expression.mid(start, parse_pos - start).toDouble(&ok); + if (!ok) { + throw InvalidNumber(); + } + return new NumberNode(val); +} + +QString Parser::parseIdentifier() { + skipIgnored(); + const int start = parse_pos; + while (peekPos().isLetter()) { + ++parse_pos; + } + return handle_expression.mid(start, parse_pos - start); +} + +} // namespace cf::desktop::desktop_component::calculator_core diff --git a/apps/calculator/core/Parser.h b/apps/calculator/core/Parser.h new file mode 100644 index 000000000..ac918a143 --- /dev/null +++ b/apps/calculator/core/Parser.h @@ -0,0 +1,100 @@ +/** + * @file desktop/ui/components/builtin_apps/calculator/core/Parser.h + * @brief Recursive-descent parser that builds the calculator AST. + * + * Ported from CCIMXDesktop's Caculator; parse logic unchanged, wrapped in + * the calculator_core namespace. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#pragma once + +#include + +namespace cf::desktop::desktop_component::calculator_core { + +class TreeNodeBase; + +/** + * @brief Parses a mathematical expression string into an AST. + * + * Grammar (lowest to highest precedence): expression = term (('+'|'-') term)*; + * term = factor (('*'|'/'|'^') factor)*; factor = '-' factor | func '(' expr + * ')' | '(' expr ')' | number. + * + * @ingroup calculator + */ +class Parser { + public: + /** + * @brief Default-constructs the parser. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + Parser() = default; + + /** + * @brief Destructs the parser. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + ~Parser() = default; + + /** + * @brief Sets the expression string to parse. + * @param[in] p The input expression. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + void setParserString(const QString& p); + + /** + * @brief Returns the currently set expression string. + * @return The expression string. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + QString parserString() const; + + /** + * @brief Parses the full expression and returns the AST root. + * @return Pointer to the root node; caller owns the tree. + * @throws GeneralParseError on trailing input; UnSupportedSymbol, + * UnSymmetryExpression, InvalidNumber on malformed input. + * @since 0.20 + * @ingroup calculator + */ + TreeNodeBase* parse(); + + private: + /// @brief Peeks the character at the current position without advancing. + QChar peekPos(); + /// @brief Returns the current character and advances the position. + QChar getChar(); + /// @brief Skips whitespace at the current position. + void skipIgnored(); + /// @brief Parses an expression (handles +, -). + TreeNodeBase* parseExpression(); + /// @brief Parses a term (handles *, /, ^). + TreeNodeBase* parseTerm(); + /// @brief Parses a factor (unary, function, parenthesized, number). + TreeNodeBase* parseFactor(); + /// @brief Parses a numeric literal. + TreeNodeBase* parseNumber(); + /// @brief Parses an identifier (function name). + QString parseIdentifier(); + + QString handle_expression; ///< The expression being parsed. + int parse_pos{0}; ///< Current parse position. +}; + +} // namespace cf::desktop::desktop_component::calculator_core diff --git a/apps/calculator/core/TreeNodeBase.h b/apps/calculator/core/TreeNodeBase.h new file mode 100644 index 000000000..50106cfde --- /dev/null +++ b/apps/calculator/core/TreeNodeBase.h @@ -0,0 +1,41 @@ +/** + * @file desktop/ui/components/builtin_apps/calculator/core/TreeNodeBase.h + * @brief Abstract base node of the calculator expression AST. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#pragma once + +namespace cf::desktop::desktop_component::calculator_core { + +/** + * @brief Abstract base node of the calculator expression AST. + * + * Concrete nodes (NumberNode, BinaryOpTreeNode, UnaryOpTreeNode, + * FunctorTreeNode) implement evaluate() to compute their subtree. + * + * @ingroup calculator + */ +struct TreeNodeBase { + virtual ~TreeNodeBase() = default; + + /** + * @brief Evaluates the expression subtree. + * + * @return The computed value. + * + * @throws Parser exceptions on evaluation errors (div-by-zero, + * unsupported symbol/function, bad sqrt, etc.). + * + * @since 0.20 + * @ingroup calculator + */ + virtual double evaluate() const = 0; +}; + +} // namespace cf::desktop::desktop_component::calculator_core diff --git a/apps/calculator/core/UnaryOpTreeNode.cpp b/apps/calculator/core/UnaryOpTreeNode.cpp new file mode 100644 index 000000000..9a952ba15 --- /dev/null +++ b/apps/calculator/core/UnaryOpTreeNode.cpp @@ -0,0 +1,31 @@ +/** + * @file desktop/ui/components/builtin_apps/calculator/core/UnaryOpTreeNode.cpp + * @brief Implementation of UnaryOpTreeNode. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#include "UnaryOpTreeNode.h" + +namespace cf::desktop::desktop_component::calculator_core { + +UnaryOpTreeNode::UnaryOpTreeNode(const QChar oper, TreeNodeBase* child) + : op(oper), operand(child) {} + +UnaryOpTreeNode::~UnaryOpTreeNode() { + delete operand; +} + +double UnaryOpTreeNode::evaluate() const { + const double val = operand->evaluate(); + if (op == '-') { + return -val; + } + throw UnSupportedSymbol(QString(op)); +} + +} // namespace cf::desktop::desktop_component::calculator_core diff --git a/apps/calculator/core/UnaryOpTreeNode.h b/apps/calculator/core/UnaryOpTreeNode.h new file mode 100644 index 000000000..42d580e29 --- /dev/null +++ b/apps/calculator/core/UnaryOpTreeNode.h @@ -0,0 +1,56 @@ +/** + * @file desktop/ui/components/builtin_apps/calculator/core/UnaryOpTreeNode.h + * @brief AST node for a unary operation (negation). + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#pragma once + +#include "ParseExceptions.h" +#include "TreeNodeBase.h" +#include + +namespace cf::desktop::desktop_component::calculator_core { + +/** + * @brief AST node applying a unary operator to one subtree. + * + * Currently only negation ('-') is supported. Owns its operand and deletes + * it on destruction. + * + * @ingroup calculator + */ +class UnaryOpTreeNode : public TreeNodeBase { + public: + /** + * @brief Constructs the node. + * @param[in] oper The operator character (only '-' is supported). + * @param[in] child The operand subtree. Ownership transfers. + * @throws None. + * @since 0.20 + * @ingroup calculator + */ + UnaryOpTreeNode(QChar oper, TreeNodeBase* child); + + ~UnaryOpTreeNode() override; + + /** + * @brief Evaluates the operand and applies the unary operator. + * @return The computed value. + * @throws UnSupportedSymbol if the operator is not '-'. + * @since 0.20 + * @ingroup calculator + */ + double evaluate() const override; + + private: + QChar op; ///< The operator character. + TreeNodeBase* operand; ///< The operand subtree (owned). +}; + +} // namespace cf::desktop::desktop_component::calculator_core diff --git a/apps/calculator/main.cpp b/apps/calculator/main.cpp new file mode 100644 index 000000000..f8dc085bf --- /dev/null +++ b/apps/calculator/main.cpp @@ -0,0 +1,27 @@ +/** + * @file apps/calculator/main.cpp + * @brief Standalone Calculator application entry point. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#include "calculator_panel.h" + +#include +#include + +int main(int argc, char* argv[]) { + QApplication app(argc, argv); + QCoreApplication::setApplicationName(QStringLiteral("CFDesktop Calculator")); + + cf::desktop::desktop_component::CalculatorPanel panel; + panel.setWindowTitle(QStringLiteral("Calculator")); + panel.resize(360, 520); + panel.show(); + + return app.exec(); +} diff --git a/desktop/ui/components/app_entry.h b/desktop/ui/components/app_entry.h index 2d03c7554..6a05684e4 100644 --- a/desktop/ui/components/app_entry.h +++ b/desktop/ui/components/app_entry.h @@ -59,6 +59,8 @@ inline QList 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}, }; } diff --git a/desktop/ui/components/launcher/app_launch_service.cpp b/desktop/ui/components/launcher/app_launch_service.cpp index 1ae0b1373..f35ae4a28 100644 --- a/desktop/ui/components/launcher/app_launch_service.cpp +++ b/desktop/ui/components/launcher/app_launch_service.cpp @@ -18,6 +18,8 @@ #include "cflog.h" +#include +#include #include #include @@ -43,8 +45,20 @@ aex::expected AppLaunchService::launch(const QString& ex const QString program = parts.first(); const QStringList args = parts.mid(1); + // Resolve CFDesktop's own apps (next to the desktop binary) before PATH: + // an exec_command like "calculator" finds /calculator without a + // system install. External commands (xdg-open, xterm) still resolve via + // PATH since they are absent from applicationDirPath(). + QString resolved = program; + if (!program.contains(QLatin1Char('/'))) { + const QString local = QCoreApplication::applicationDirPath() + '/' + program; + if (QFileInfo::exists(local)) { + resolved = local; + } + } + qint64 pid = 0; - if (QProcess::startDetached(program, args, QString{}, &pid)) { + if (QProcess::startDetached(resolved, args, QString{}, &pid)) { cf::log::infoftag(kTag, "launched '{}' (pid {})", exec_command.toStdString(), pid); return pid; } diff --git a/document/status/current.md b/document/status/current.md index 1381ddb32..0a197afdc 100644 --- a/document/status/current.md +++ b/document/status/current.md @@ -60,6 +60,7 @@ description: CFDesktop 项目进度的唯一事实来源与全局导航。 - **2026-06**:`refactor: refactor the ui subsystem`;`hwtier system enabled`;文档清理 - **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)。 - **已达成**:Milestone 1「桌面骨架可见」;Phase 0 / 1 / 2 / A(CI) / 6 / G(Widget) / H(显示后端)(详见 [SUMMARY.md](../todo/done/SUMMARY.md)) ## 新人入门 diff --git a/document/todo/desktop/13_widget_apps.md b/document/todo/desktop/13_widget_apps.md index 63975c1e9..535750839 100644 --- a/document/todo/desktop/13_widget_apps.md +++ b/document/todo/desktop/13_widget_apps.md @@ -14,6 +14,7 @@ description: "预计周期: 4~5 周,依赖阶段: Phase 9, Phase 12" > - **壁纸引擎已落地**(`desktop/ui/components/wallpaper/`:`ImageWallPaperLayer`/`WallPaperAccessStorage`/`WallPaperToken`/`WallpaperShellLayerStrategy`),本 Phase 壁纸部分只需补轮播/生成式/资源,引擎不必重做。 > - **资源包运行时发现已合入**(PR #13,2026-06):第三方壁纸包安装到 `/Wallpapers//` 即被递归发现;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)。 > - **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)。 diff --git a/test/desktop/CMakeLists.txt b/test/desktop/CMakeLists.txt index 2c85eb325..e72d1e857 100644 --- a/test/desktop/CMakeLists.txt +++ b/test/desktop/CMakeLists.txt @@ -12,3 +12,6 @@ add_subdirectory(window_placement) # Add wallpaper animation tests subdirectory add_subdirectory(wallpaper_animation) + +# Add calculator parser tests subdirectory +add_subdirectory(calculator) diff --git a/test/desktop/calculator/CMakeLists.txt b/test/desktop/calculator/CMakeLists.txt new file mode 100644 index 000000000..8f0942621 --- /dev/null +++ b/test/desktop/calculator/CMakeLists.txt @@ -0,0 +1,8 @@ +# Calculator parser unit tests (ported from CCIMXDesktop's test_parser). +add_gtest_executable( + TEST_NAME parser_test + SOURCE_FILE parser_test.cpp + LINK_LIBRARIES cfdesktop_calculator_parser;Qt6::Core;GTest::gtest;GTest::gtest_main + LABELS "desktop;unit;calculator" + LOG_MODULE calculator_tests +) diff --git a/test/desktop/calculator/parser_test.cpp b/test/desktop/calculator/parser_test.cpp new file mode 100644 index 000000000..f779538fc --- /dev/null +++ b/test/desktop/calculator/parser_test.cpp @@ -0,0 +1,130 @@ +/** + * @file test/desktop/calculator/parser_test.cpp + * @brief GoogleTest port of CCIMXDesktop's Caculator test_parser. + * + * Covers the same cases as the upstream test_parser.cpp (basic arithmetic, + * precedence, unary negation, functions, nesting, edge cases, and error + * throws) to confirm the port is behaviorally equivalent. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup calculator + */ + +#include "ExpressionEvaluator.h" + +#include +#include + +#include + +namespace { +namespace eval = cf::desktop::desktop_component::calculator_core::ExpressionEvaluator; + +/// Evaluates @p expr and expects it near @p expected; fails on any throw. +void expectEq(const QString& expr, double expected, double tol = 1e-3) { + double result = 0; + try { + result = eval::evalute_expression(expr); + } catch (const std::exception& e) { + FAIL() << expr.toStdString() << " threw: " << e.what(); + } + EXPECT_NEAR(result, expected, tol) << expr.toStdString(); +} +} // namespace + +TEST(CalculatorParser, BasicArithmetic) { + expectEq("1 + 2", 3); + expectEq("5 - 3", 2); + expectEq("2 * 4", 8); + expectEq("8 / 2", 4); +} + +TEST(CalculatorParser, Precedence) { + expectEq("2 + 3 * 4", 14); + expectEq("(2 + 3) * 4", 20); + expectEq("10 - 2 - 3", 5); +} + +TEST(CalculatorParser, UnaryNegation) { + expectEq("-5", -5); + expectEq("-(2 + 3)", -5); + expectEq("4 + -2", 2); +} + +TEST(CalculatorParser, Functions) { + expectEq("sin(3.1415926 / 2)", 1.0); + expectEq("cos(0)", 1.0); + expectEq("sqrt(16)", 4.0); + expectEq("log(1)", 0.0); +} + +TEST(CalculatorParser, NestedExpressions) { + expectEq("((2+3)*(4+1))", 25.0); + expectEq("sqrt(4 + sqrt(16))", 2.828); +} + +TEST(CalculatorParser, BoundaryValues) { + expectEq("0", 0.0); + expectEq("0.00001 + 1", 1.00001); + expectEq("999999 + 1", 1000000.0); +} + +TEST(CalculatorParser, DeepNesting) { + expectEq("(((((((1+2)))))))", 3); + expectEq("(((((3+5)*2)-4)/2)+1)", 7); +} + +TEST(CalculatorParser, FunctionNesting) { + expectEq("sqrt(sin(3.1415926/2) + cos(0))", std::sqrt(1.0 + 1.0)); + expectEq("sqrt(sqrt(16))", 2.0); +} + +TEST(CalculatorParser, UnaryWithFunction) { + expectEq("-sin(3.1415926)", -std::sin(3.1415926)); + expectEq("-sqrt(9)", -3.0); + expectEq("sqrt(-3 * -3)", 3.0); +} + +TEST(CalculatorParser, MultipleNegation) { + expectEq("--2", 2.0); + expectEq("---2", -2.0); + expectEq("4 + --1", 5.0); +} + +TEST(CalculatorParser, LargeAndSmallNumbers) { + expectEq("1.0000000001 + 1", 2.0000000001); +} + +TEST(CalculatorParser, DecimalTolerance) { + expectEq(".5 + .5", 1.0); + expectEq("5.", 5.0); + expectEq("0.1 + 0.2", 0.3); +} + +TEST(CalculatorParser, MultiDivision) { + expectEq("100 / 2 / 5", 10.0); + expectEq("8 / 2 * (2 + 2)", 16.0); +} + +TEST(CalculatorParser, TrigIdentity) { + expectEq("sin(0)^2 + cos(0)^2", 1.0); + expectEq("log(exp(3))", 3.0); +} + +TEST(CalculatorParser, ThrowsOnMalformed) { + EXPECT_ANY_THROW(eval::evalute_expression("2 +")); + EXPECT_ANY_THROW(eval::evalute_expression("((3+2)")); + EXPECT_ANY_THROW(eval::evalute_expression("abc(3)")); + EXPECT_ANY_THROW(eval::evalute_expression("2 + * 3")); + EXPECT_ANY_THROW(eval::evalute_expression("3 / 0")); + EXPECT_ANY_THROW(eval::evalute_expression("10 / (5 - 5)")); + EXPECT_ANY_THROW(eval::evalute_expression("sqrt(-1)")); + EXPECT_ANY_THROW(eval::evalute_expression("sin")); + EXPECT_ANY_THROW(eval::evalute_expression("sin()")); + EXPECT_ANY_THROW(eval::evalute_expression("()")); + EXPECT_ANY_THROW(eval::evalute_expression("..2 + 1")); + EXPECT_ANY_THROW(eval::evalute_expression("1 + 2 3")); +}